From 4d1c1a2e142613cb75f6123721e1eef80e562cb9 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:56:13 +0100 Subject: [PATCH 1/4] docs: record the ledger-canonical issue-store decision The .abcd/work/issues/ ledger is the single canonical issue store; forge issues become a derived, opt-in mirror (one-way mirror-out, human-gated import in). Grounded in the commissioned SOTA survey recorded alongside. Assisted-by: Claude:claude-fable-5 --- ...2026-08-19-issue-ledger-forge-sync-sota.md | 97 +++++++++++++++++++ .abcd/work/DECISIONS.md | 16 +++ 2 files changed, 113 insertions(+) create mode 100644 .abcd/development/research/notes/2026-08-19-issue-ledger-forge-sync-sota.md diff --git a/.abcd/development/research/notes/2026-08-19-issue-ledger-forge-sync-sota.md b/.abcd/development/research/notes/2026-08-19-issue-ledger-forge-sync-sota.md new file mode 100644 index 00000000..a26835b0 --- /dev/null +++ b/.abcd/development/research/notes/2026-08-19-issue-ledger-forge-sync-sota.md @@ -0,0 +1,97 @@ +# Issue ledger ↔ forge sync — SOTA survey (2026-08-19) + +Commissioned host-delegated research pass on reconciling an in-repo, file-based +issue ledger with forge-hosted issues (GitHub Issues), given two hard +constraints: (a) autonomous CI/cloud agents produce findings, (b) some users +have no forge access and must read/write the record with only a checkout. +Recency assessed against 2026-08-19. This note grounds the 2026-08-19 decision +in `../../../work/DECISIONS.md` (ledger-canonical, one-way mirror). + +## Ranked findings + +1. **In-repo file ledger canonical; forge issues a derived view.** [CONSENSUS] + The one team with a post-mortem-grade public rationale, CoreOS's + GitHub↔JIRA syncer, deliberately went one-way: "rather than require people + to keep up with both sources, we decided to make *one* the single source of + truth" (coreos/issue-sync, archived 2023). Fossil (the longest-lived + in-repo tracker) avoids reconciliation by having exactly one store. The + no-forge-users constraint forces the choice regardless: anything that + exists only on the forge is invisible to them. Plain committed markdown is + where the 2025-26 agent-tooling wave landed (Backlog.md: the same .md files + humans read are the agent API, "no translation layer or sync issues"). + +2. **One-way sync (canonical → mirror), external id written back into the + canonical record.** [EVIDENCE] Bidirectional sync has three documented + failure modes — echo/"vampire record" loops, origin ambiguity, + timestamp-only conflict detection under clock skew — and every mature + mitigation (origin tagging, idempotent upserts keyed on a stable external + id, per-field ownership) exists only to contain them. Mechanisms proven in + the survivors: git-bug bridges stamp exported operations with + `github-id`/`github-url` so re-import recognises its own exports; git-issue + (Spinellis) keeps an imports mapping directory plus a checkpoint SHA. The + file-ledger equivalent is one frontmatter field per record: a record + carrying a forge id is updated in place on the mirror, never re-created, + and a mirror event matching a known id with unchanged content is dropped. + +3. **Status truth lives in the canonical store; the mirror auto-closes on + canonical resolution.** [EVIDENCE] ClusterFuzz/OSS-Fuzz is the production + precedent: fully automatic filing, triage, and closing where the tracker + issue is a projection closed when the canonical evidence resolves. Forge + triage is never synced automatically — it generates an import proposal a + human accepts by an explicit import that writes the canonical file with + provenance (forge id, actor, timestamp). A forge-side close never imported + is reopened by the next mirror pass — self-healing, and the property that + makes the topology safe. + +4. **Agents enter findings as files-via-PR, validated by repo lint/CI; forge + issues are at most a fallback inbox.** [CONSENSUS, strong negative + evidence for the alternative] GitHub Advisory Database: finding records as + committed OSV files entering by reviewed PR at scale. GitHub Security Lab + Taskflow (2026-01) and Google Big Sleep gate every AI-found report on human + review; Taskflow feeds reviewer dismissal reasons back into later runs as + its cross-run suppression mechanism. The negative case is curl's tracker: + ungated agent-generated reports drove signal from ~1-in-6 to ~1-in-20/30 + and forced the bounty to halt (2025-07). Practical shape: the finding + file's id is a **stable content-derived fingerprint** (ClusterFuzz crash + signatures are the model), so a repeat run produces a no-op diff and lint + enforces fingerprint uniqueness. + +5. **Steal mechanisms from git-bug and git-issue; adopt neither as the + store.** [EVIDENCE on liveness; CONTESTED on viability] git-bug is the + healthiest survivor (releases through 2025-05) but stores issues as git + objects, not files — unreadable from a plain checkout. git-issue has the + best file-based external-id mapping but no maintenance signal since 2020. + Fossil and Radicle solve the problem only by replacing the VCS/forge + wholesale. BugsEverywhere and Simple Defects are dead; the post-mortem + consensus on what kills in-repo trackers: no shared schema, invisibility to + non-developers, and "90% of bug tracking is sending messages to other + people". + +## Not worth adopting + +- **True bidirectional sync** — enterprise-scale conflict-rule maintenance, + no payoff at this scale. [CONSENSUS] +- **git-bug as storage** — hidden git objects defeat checkout readability; + its bridge code remains the best reference implementation. [CONTESTED] +- **Fossil/Radicle migration** — correct architecture, wrong cost. [CONSENSUS] +- **CRDT/event-log issue formats** — they solve concurrent-edit merging that + one canonical repo with PR-mediated writes mostly doesn't have. [ANECDOTE] +- **Ungated autonomous filing** — curl is the controlled-ish experiment; + even the frontier labs gate on human review. [EVIDENCE] + +## Sources + +- +- +- +- and + +- +- +- +- +- +- +- (git-bug, 2025-05) +- (Epiq) +- and diff --git a/.abcd/work/DECISIONS.md b/.abcd/work/DECISIONS.md index c55d2a5b..89fecd1b 100644 --- a/.abcd/work/DECISIONS.md +++ b/.abcd/work/DECISIONS.md @@ -1352,3 +1352,19 @@ parallel-agent merge contention bites. warn ceiling is an absolute 2 rather than a 1% rate that permitted 9; matchesAny deleted as dead; and the three surfaces no longer list "launched through a wrapper outside the known set" as something an allow does not see, which is the case this change converts to a warn. +- 2026-08-19 — The `.abcd/work/issues/` ledger is the single canonical issue store; forge + issues (GitHub) are a derived, opt-in mirror surface — consistent with the host-delegated + boundary (forges are hosts). Per-field ownership: the ledger owns existence, content, and + resolution; the forge owns nothing. Sync is one-way mirror-out, the forge id written back + into the record as the echo suppressor; forge-side activity (triage labels, a close) is + never auto-synced — it generates an import proposal a human accepts via an explicit import + that writes the canonical file with provenance. A forge-side close never imported is + reopened by the next mirror pass: the mirror is self-healing by construction. Autonomous + hunts file findings as fingerprint-keyed iss-N records inside the gated PR they already + open, validated by record-lint (the armed detector) — never as forge issues. Grounded in + commissioned SOTA research (see + `../development/research/notes/2026-08-19-issue-ledger-forge-sync-sota.md`): one-way + canonical sync is the surviving pattern (coreos/issue-sync post-mortem); agent findings as + records-via-reviewed-PR is 2025-26 practice (GitHub advisory-database, ClusterFuzz + fingerprint dedup); ungated agent filing degrades the whole record (curl, 2025). + Architecture-shaping: graduate to an ADR. From 51e34591e1304a706df34900e22459c5d281862a Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:56:13 +0100 Subject: [PATCH 2/4] docs: record the pr-294 commissioned review Verdict and ten verified findings from the host-delegated review of the uppercase-null widening, with the routing table for each finding. Assisted-by: Claude:claude-fable-5 --- .../00-summary.md | 43 ++++++++ .../01-findings.md | 99 +++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 .abcd/work/reviews/2026-08-19-pr-294-null-predicate/00-summary.md create mode 100644 .abcd/work/reviews/2026-08-19-pr-294-null-predicate/01-findings.md diff --git a/.abcd/work/reviews/2026-08-19-pr-294-null-predicate/00-summary.md b/.abcd/work/reviews/2026-08-19-pr-294-null-predicate/00-summary.md new file mode 100644 index 00000000..bb6d5d43 --- /dev/null +++ b/.abcd/work/reviews/2026-08-19-pr-294-null-predicate/00-summary.md @@ -0,0 +1,43 @@ +# PR #294 — uppercase YAML nulls: commissioned review summary + +- **Subject**: PR #294 ("fix: IsNull resolves the uppercase YAML nulls"), an + outside contribution widening `frontmatter.IsNull` and lint's `isNull` to + the uppercase null spellings after GitHub #290. +- **Provenance**: commissioned host-delegated multi-agent code review, + 2026-08-19 — 8 finder angles (7 reported; the efficiency finder stalled on a + two-comparison diff), candidates deduped, recall-biased adversarial + verification with primary-source reads. Conducted outside abcd's own + command machinery, hence recorded here per this folder's charter. + +## Verdict + +Sound in its stated scope: the widening itself is correct, tests pass, +gofmt/vet clean, attribution trailer correct (the `Signed-off-by` is +defensible as the repo's first outside contribution). The findings cluster +around one theme: the PR widens two **hand-synced copies** of the null +predicate while the real divergence axis — **quoting** — is untouched, and +the two most severe findings show the PR's own "the gates can never disagree" +guarantee failing for `"NULL"` (accepted by capture, blocked by record-lint), +with the PR's own tests pinning both sides of the contradiction. + +Two candidate claims were refuted during verification and are **not** +findings: a lifeboat quoted-`"NULL"` "silent data loss" claim (the memory +dumper and the ADR scanner read disjoint trees, and the `status: superseded` +clause fires independently), and a DCO-trailer conventions claim (an outside +contribution is exactly the deferral endpoint the convention names). + +## Ranked actions + +Findings are detailed in `01-findings.md` (F1–F10, most severe first). + +| Action | Findings | Route | +| --- | --- | --- | +| Request changes on PR #294 (in scope): delegate lint's `isNull` to `frontmatter.IsNull` (kills the drift class and F4's mirrored-test gap), fix the doc-comment miscount, `t.Errorf` in the spelling matrix, widen the capture agreement test, cite the ledger handle in the CHANGELOG entry | F3, F4, F7, F9, F5, F10 (cite) | PR review comment | +| Capture: quoted-null verdict split between capture and record-lint (the PR's contradiction pair F2 is the pinned evidence) | F1, F2 | issues ledger | +| Capture: release-derivation vs record-lint diagnosis skew for null impact | F8 | issues ledger | +| Capture retroactively: the GitHub #290 bug itself, so the CHANGELOG cites a ledger handle | F10 | issues ledger | +| Intent: one canonical exported scalar resolver; the four independent decoders delegate | F6 | intent draft | + +Decision context recorded the same day in `../../DECISIONS.md` +(ledger-canonical issue store, one-way forge mirror), grounded in +`../../../development/research/notes/2026-08-19-issue-ledger-forge-sync-sota.md`. diff --git a/.abcd/work/reviews/2026-08-19-pr-294-null-predicate/01-findings.md b/.abcd/work/reviews/2026-08-19-pr-294-null-predicate/01-findings.md new file mode 100644 index 00000000..d421fdbd --- /dev/null +++ b/.abcd/work/reviews/2026-08-19-pr-294-null-predicate/01-findings.md @@ -0,0 +1,99 @@ +# PR #294 — verified findings (most severe first) + +## F1 — the "gates cannot disagree" guarantee is false for double-quoted nulls + +`internal/core/capture/parse.go:188`. An issue record carries +`impact: "NULL"`. capture's `decodeScalar` strips the double quotes before +`validateStrict` calls `frontmatter.IsNull` +(`internal/core/capture/validate.go:104`), so the bare `NULL` reads as absent +and `abcd capture resolve` accepts the record. record-lint's +`checkIssueImpact` (`internal/core/lint/lint.go:1817`) reads the raw value +with quotes intact via `frontmatterFields`, `isNull("\"NULL\"")` is false, +`changelog.ParseImpact` fails, and the `issue_impact_valid` blocker fires on +the record capture just accepted. Pre-PR this split existed only for +`"null"`; the PR adds `"Null"` and `"NULL"` while its own comment at +`internal/core/lint/lint.go:2397` claims the split is impossible. Fix at the +right depth: normalise quoting on one side (or unquote consistently in the +shared predicate's callers), not just widen both literal sets. + +## F2 — two tests added in this PR pin opposite verdicts for the same bytes + +`internal/core/lifeboat/graveyard_abandoned_test.go:167` asserts a file with +`superseded_by: "NULL"` yields no finding (`gvSupersededADRs` unquotes first, +`internal/core/lifeboat/graveyard_abandoned.go:112`), while +`internal/core/frontmatter/frontmatter_test.go:76` asserts +`IsNull("\"NULL\"")` must stay false ("a quoted scalar is not a YAML null"). +Post-PR the same bytes are null to `disembark pack`, but `abcd record +describe` (`internal/core/record/record.go:172`/`294`) publishes +`Links["superseded_by"] = "NULL"` and suggests reading a record named +`"NULL"`, and lint's schema check calls it "not a record handle". A future +unifying fix must break one of the two new tests. + +## F3 — lint's `isNull` is a hand-synced byte-identical copy + +`internal/core/lint/lint.go:2400`. lint already imports +`internal/core/frontmatter` (which has zero internal deps — no cycle), and +the same file already delegates its scanner for exactly this reason +(`frontmatterFields`, `lint.go:2357`). GitHub #290 happened because a +widening landed nowhere; keeping the copy re-arms the same bug shape. +Replace the body with `return frontmatter.IsNull(v)` and delete the +duplicated table test. + +## F4 — the mirrored tests cannot detect drift, and have already diverged + +`internal/core/lint/isnull_test.go:17`. frontmatter's negative table guards +quoted/case-folded forms; lint's stops at `nullish`. Nothing asserts the two +functions agree, so a future widening of lint's `isNull` to strip quotes +passes lint's test while only the unchanged package's test would catch it. +If the copy is not deleted (F3), add one equivalence test over a shared +table comparing `isNull(v) == frontmatter.IsNull(v)`. + +## F5 — capture's agreement test was not widened with the predicates + +`internal/core/capture/parse_test.go:126`. `TestValidateStrictImpact` still +iterates only `{"", "null", "~"}`. If `frontmatter.IsNull` is later narrowed +(or capture stops delegating), the GitHub #290 regression re-lands undetected +in the one package whose test should catch it. + +## F6 — two of four independent YAML scalar decoders patched; the class remains + +`internal/core/memory/yaml.go:526`. memory's `parseScalar` is the only +complete resolver (nulls, bools, quotes, numbers) but is unexported; capture's +`decodeScalar` strips quotes but knows no nulls or bools; lifeboat's +`gvUnquote` strips quotes only; `frontmatter.IsNull` knows nulls only. The +bool axis is untouched (`parseScalar` accepts true/True/TRUE while +`internal/core/memory/vintage.go` tests `== "true"` exactly) and +`yaml.go:949`'s dumper holds a fourth null-literal list on the write path. +Exporting one scalar-resolution helper (frontmatter is the dependency-free +home) fixes the class, not the instance. → routed to an intent. + +## F7 — the new doc comment miscounts its own enumeration + +`internal/core/frontmatter/frontmatter.go:66`: "an empty value and the four +YAML nulls ""/"null"/"Null"/"NULL"/"~"" names the empty scalar twice and +lists five items under "four"; the lint-side twin at `lint.go:2397` phrases +it correctly, so the two copies now document themselves differently. Also, +the CHANGELOG's "byte-identical" claim is true only of the boolean +expression, not the functions as written. + +## F8 — a third impact consumer diagnoses `impact: NULL` differently + +`internal/core/changelog/shipped.go:277`. `newRecord` calls `ParseImpact` +with no `IsNull` gate: record-lint reports "impact must be set explicitly" +(missing) while the release cut records "invalid impact" (malformed) for the +same line — operator-facing message inconsistency introduced by this diff. +An `IsNull` gate at the call site restores parity. + +## F9 — `t.Fatalf` inside the six-spelling matrix loop + +`internal/core/lifeboat/graveyard_abandoned_test.go:174`. A partial +regression reports as a single-spelling failure and the positive control at +lines 178–184 never runs. `t.Errorf` (or a subtest per spelling) reports the +whole matrix in one run. + +## F10 — the CHANGELOG entry cites a bare GitHub number + +`CHANGELOG.md:33` cites "(#290)" — the file's only non-ledger citation among +~70 record handles — and no corresponding iss-N exists in +`.abcd/work/issues/`, leaving the fix invisible to record-lint, capture, and +the graveyard readers. Capture the bug retroactively and cite the handle. From d818fe44cf7217760941dd5802760b7f2770b672 Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:56:13 +0100 Subject: [PATCH 3/4] docs: capture iss-285/286/287 from the pr-294 review iss-285 the quoted-null verdict split between capture and record-lint, iss-286 the release-derivation diagnosis skew, iss-287 the retroactive capture of the uppercase-null bug (GitHub #290) so the CHANGELOG can cite a ledger handle. Assisted-by: Claude:claude-fable-5 --- ...it-capture-and-record-lint-verdicts-cap.md | 26 +++++++++++++++++++ ...agnoses-a-null-impact-as-malformed-whil.md | 21 +++++++++++++++ ...pellings-unrecognised-by-the-null-predi.md | 21 +++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 .abcd/work/issues/open/iss-285-quoted-yaml-nulls-split-capture-and-record-lint-verdicts-cap.md create mode 100644 .abcd/work/issues/open/iss-286-release-derivation-diagnoses-a-null-impact-as-malformed-whil.md create mode 100644 .abcd/work/issues/open/iss-287-uppercase-yaml-null-spellings-unrecognised-by-the-null-predi.md diff --git a/.abcd/work/issues/open/iss-285-quoted-yaml-nulls-split-capture-and-record-lint-verdicts-cap.md b/.abcd/work/issues/open/iss-285-quoted-yaml-nulls-split-capture-and-record-lint-verdicts-cap.md new file mode 100644 index 00000000..9aa25103 --- /dev/null +++ b/.abcd/work/issues/open/iss-285-quoted-yaml-nulls-split-capture-and-record-lint-verdicts-cap.md @@ -0,0 +1,26 @@ +--- +schema_version: 1 +id: "iss-285" +slug: "quoted-yaml-nulls-split-capture-and-record-lint-verdicts-cap" +severity: "major" +category: "bug" +source: "impl-review" +found_during: "pr-294-review" +found_at: "internal/core/capture/parse.go" +--- + +Quoted YAML nulls split capture and record-lint verdicts: capture unquotes before IsNull, record-lint reads the raw value + +An issue record carrying `impact: "NULL"`: capture's `decodeScalar` +(`internal/core/capture/parse.go:188`) strips the double quotes before +`validateStrict` calls `frontmatter.IsNull`, so `abcd capture resolve` +accepts the record — while record-lint's `checkIssueImpact` reads the raw +value with quotes intact, `ParseImpact` fails, and the `issue_impact_valid` +blocker fires on the record capture just accepted. The comment at +`internal/core/lint/lint.go:2397` claims this split is impossible. PR #294 +widened the disagreement window from one spelling (`"null"`) to three; its +own tests pin both sides of the contradiction +(`internal/core/frontmatter/frontmatter_test.go:76` vs +`internal/core/lifeboat/graveyard_abandoned_test.go:167`). The fix is to +normalise quoting on one side, not to widen literal sets. Evidence: +`.abcd/work/reviews/2026-08-19-pr-294-null-predicate/` (F1, F2). \ No newline at end of file diff --git a/.abcd/work/issues/open/iss-286-release-derivation-diagnoses-a-null-impact-as-malformed-whil.md b/.abcd/work/issues/open/iss-286-release-derivation-diagnoses-a-null-impact-as-malformed-whil.md new file mode 100644 index 00000000..448b9367 --- /dev/null +++ b/.abcd/work/issues/open/iss-286-release-derivation-diagnoses-a-null-impact-as-malformed-whil.md @@ -0,0 +1,21 @@ +--- +schema_version: 1 +id: "iss-286" +slug: "release-derivation-diagnoses-a-null-impact-as-malformed-whil" +severity: "minor" +category: "inconsistency" +source: "impl-review" +found_during: "pr-294-review" +found_at: "internal/core/changelog/shipped.go" +--- + +Release derivation diagnoses a null impact as malformed while record-lint calls it missing + +`newRecord` (`internal/core/changelog/shipped.go:277`) calls `ParseImpact` +with no `IsNull` gate, so for a resolved issue carrying `impact: NULL` +record-lint reports "impact must be set explicitly" (missing) while the +release cut records "invalid impact" (malformed) — two diagnoses for one +line. Verdicts never diverge (both paths reject), so this is operator-facing +message inconsistency only; an `IsNull` gate at the call site restores +parity. Evidence: `.abcd/work/reviews/2026-08-19-pr-294-null-predicate/` +(F8). \ No newline at end of file diff --git a/.abcd/work/issues/open/iss-287-uppercase-yaml-null-spellings-unrecognised-by-the-null-predi.md b/.abcd/work/issues/open/iss-287-uppercase-yaml-null-spellings-unrecognised-by-the-null-predi.md new file mode 100644 index 00000000..d543d54e --- /dev/null +++ b/.abcd/work/issues/open/iss-287-uppercase-yaml-null-spellings-unrecognised-by-the-null-predi.md @@ -0,0 +1,21 @@ +--- +schema_version: 1 +id: "iss-287" +slug: "uppercase-yaml-null-spellings-unrecognised-by-the-null-predi" +severity: "major" +category: "bug" +source: "user-observation" +found_during: "pr-294-review" +found_at: "internal/core/frontmatter/frontmatter.go" +--- + +Uppercase YAML null spellings unrecognised by the null predicates (reported as GitHub #290) + +Retroactive capture of the bug reported as GitHub #290 so the ledger holds +the handle the CHANGELOG entry should cite: `frontmatter.IsNull` and lint's +`isNull` recognised `null` and `~` but not the YAML 1.2 core-schema +spellings `Null`/`NULL`, so records using an uppercase null were misread +(e.g. a superseded ADR's `superseded_by:` treated as a live handle). Fixed +in flight by PR #294, which widens both predicates with regression tests; +resolve this issue when that PR merges, citing its commit. Evidence: +`.abcd/work/reviews/2026-08-19-pr-294-null-predicate/` (F10). \ No newline at end of file From a794ad94397f887e70e22510f8641982b9e0477c Mon Sep 17 00:00:00 2001 From: REPPL <77722411+REPPL@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:56:13 +0100 Subject: [PATCH 4/4] docs: file itd-128, one canonical YAML scalar resolver The class fix behind iss-285/287: one exported frontmatter helper for scalar resolution, every decoder delegating. Indexed in the brief's later-phase-intents list. Assisted-by: Claude:claude-fable-5 --- .../brief/06-delivery/03-out-of-scope.md | 1 + ...alar-resolver-frontmatter-exports-the-s.md | 67 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 .abcd/development/intents/drafts/itd-128-one-canonical-yaml-scalar-resolver-frontmatter-exports-the-s.md diff --git a/.abcd/development/brief/06-delivery/03-out-of-scope.md b/.abcd/development/brief/06-delivery/03-out-of-scope.md index 79cc1c10..9e899272 100644 --- a/.abcd/development/brief/06-delivery/03-out-of-scope.md +++ b/.abcd/development/brief/06-delivery/03-out-of-scope.md @@ -62,6 +62,7 @@ gate. That is what keeps "not hand-counted" true after the day it was written. - `itd-77` — Relocatable user-level home - `itd-126` — Team bibliography share/ingest: citation data travels the repo, corpora never do - `itd-127` — Paper reconstruction from the provenance ledger +- `itd-128` — One canonical YAML scalar resolver: every decoder delegates to one exported frontmatter helper - `itd-78` — Intent-dependency graph: what to build first, even when it is something small - `itd-82` — Ledger drain: one verb triages the open ledger into work that ships itself and work a human must think about - `itd-83` — The review bar fires by itself, in every repo abcd manages diff --git a/.abcd/development/intents/drafts/itd-128-one-canonical-yaml-scalar-resolver-frontmatter-exports-the-s.md b/.abcd/development/intents/drafts/itd-128-one-canonical-yaml-scalar-resolver-frontmatter-exports-the-s.md new file mode 100644 index 00000000..3981f924 --- /dev/null +++ b/.abcd/development/intents/drafts/itd-128-one-canonical-yaml-scalar-resolver-frontmatter-exports-the-s.md @@ -0,0 +1,67 @@ +--- +id: itd-128 +slug: one-canonical-yaml-scalar-resolver-frontmatter-exports-the-s +spec_id: null +kind: null +suggested_kind: null +reclassification_history: [] +builds_on: [] +severity: minor +--- + +# One canonical YAML scalar resolver: frontmatter exports the single null/bool/quote resolution helper and the capture, lint, lifeboat, and memory decoders all delegate to it, so a cross-package verdict split on any scalar axis becomes unrepresentable + +## Press Release + +Alice writes `impact: NULL` in an issue record and every abcd surface reads +it the same way: capture accepts what lint accepts, the release cut +diagnoses what lint diagnoses, and the lifeboat graveyard sees the same null +the memory dumper writes. There is exactly one place in the codebase that +knows what a YAML scalar means — an exported resolution helper in +`internal/core/frontmatter` covering nulls, booleans, and quoting — and the +capture, lint, lifeboat, and memory decoders all delegate to it. When Bob +extends the scalar rules (a new spelling, whitespace trimming), he edits one +function and every consumer moves together; the class of bug where two +hand-synced copies drift apart is no longer expressible. + +## Why This Matters + +The uppercase-null bug (iss-287) happened because a widening landed in some +predicate copies and not others; its fix (PR #294) widened two of four +independent scalar decoders and left the quoting axis split (iss-285) and +the boolean axis untouched (`memory/parseScalar` accepts true/True/TRUE +while `memory/vintage.go` tests `== "true"` exactly). Each remaining +asymmetry is a future issue of the same shape. This is the +one-canonical-primitive principle applied to scalar resolution: fix the +class, not the instance. Evidence: +`.abcd/work/reviews/2026-08-19-pr-294-null-predicate/` (F6). + +## Acceptance Criteria + +- Given a YAML scalar value, When any of the capture, lint, lifeboat, or + memory decoders resolves its null/bool/quoting meaning, Then the answer + comes from one exported frontmatter helper and no package-local literal + set or hand-synced copy remains (grep proves zero duplicate null/bool + tables outside the helper and its tests). +- Given the frontmatter package, When its scalar rules are widened in one + edit, Then an equivalence/consumer test suite proves every delegating + package observes the change without further edits. +- Given a record carrying a quoted null (`impact: "NULL"`), When capture + validation and record-lint both judge it, Then their verdicts agree + (closes iss-285), and the two currently-contradictory tests + (`frontmatter_test.go` vs `graveyard_abandoned_test.go`) are reconciled to + the same stance. + +## Open Questions + +- Where does quoting normalisation live — inside the shared helper (callers + pass raw bytes) or as a documented pre-step every caller must apply? The + helper-owns-it answer makes the drift unrepresentable; the pre-step answer + preserves callers that need the raw form (lint's "not a record handle" + message wants the original bytes). +- Does the write path (`memory/yaml.go` dumper's own null-literal list) fold + into the same helper, or is emit a separate concern? + +## Audit Notes + +_Empty. Populated by intent-fidelity-reviewer when intent moves to shipped/._