Development record
`.work/issues.md` stays the live operational ledger; the structured per-issue store is deferred until the native spec layer schedules the migration (historical)
Context
abcd records issues discovered during any work in .work/issues.md (historical) — the
operational working ledger, append-as-you-find, per the workspace's
issue-recording discipline. .work/ (historical) is git-ignored, so this ledger is a
local-only operational surface, not a committed durable record.
Two things pull against that:
- A structured per-issue store is designed. A migration spec plans the move
of the flat
.work/issues.md(historical) ledger into structured per-issue files under.abcd/development/activity/issues/{open,resolved,wontfix}/iss-*.md(historical), withiss-NIDs and YAML frontmatter — the substrate the/abcd:capturepromote bridge (itd-46) and thePR000/PR001/PR002promote-backlink lint family already reference.
- AC evidence routed only to
.work/(historical) is invisible to PR reviewers (the gitignore makes the ledger unreviewable from a diff). That tension is what makes "is.work/issues.md(historical) the durable record, or a staging surface?" a real decision rather than a content edit.
The spc-33 Phase-3→4 cleanup sweep (I13) forced the question to a head: either
run the spc-22 migration now, or record a durable decision that the flat ledger
stays the live operational surface — with the durable record being a committed
ADR, not .work/issues.md (historical) itself (which, being git-ignored, can never be the
checker-assertable artefact).
Current-state evidence:
- The structured-ledger root does not exist:
.abcd/development/activity/issues/(historical) is absent (noactivity/tree at all). Nothing under the structured path is populated — the migration has not run. - The migration spec is designed but unrun: no migration has executed and
no
iss-*.mdfiles have been produced. - The flat ledger is live and in active use:
.work/issues.md(historical) carries current-session operational entries.
So the structured path is designed but empty, and the flat path is the only ledger actually carrying issues today.
Decision
We will keep .work/issues.md (historical) as the live operational issue ledger, and
defer the structured per-issue migration until the native spec layer schedules
it (adr-26).
The deferral trigger is concrete, not vague: the structured-ledger cutover
(/abcd:capture → iss-N plus the .abcd/development/activity/issues/ (historical) store)
happens when, and only when, a future native spec explicitly schedules the
.work/issues.md (historical) → iss-N migration into a phase and runs it. Absent that
named trigger, the flat ledger remains canonical for operational issue capture.
This ADR is the durable, checker-assertable record of that choice. It does
not legitimise .work/ (historical) as a durable routing target: .work/issues.md
remains an operational working surface only. Every future routed
follow-up — anything that must survive in the committed record — still needs a
committed home (itd-* intent, iss-* structured issue once the migration
runs, or an ADR). The decision here is narrow: the operational live-capture
ledger stays flat until the migration is scheduled; durable routing is
unchanged.
Alternatives Considered
- Run the structured migration now. Build the structured
.abcd/development/activity/issues/(historical) store, migrate every.work/issues.mdentry toiss-Nfiles, and wire the live ledger to the structured path. Rejected: it is a heavyweight, multi-task migration (legacy parse +iss-Nassignment + promote-bridge integration + lint-corpus wiring) that warrants its own plan-review rather than being smuggled into a cleanup sweep. The structured store being empty today (evidence above) means nothing is currently broken by deferring — there is no half-migrated state to rescue.
- Record the decision as a free-form
.work/(historical) note. Rejected:.work/is git-ignored, so a note there cannot be the durable, checker-assertable record. The whole point of this decision is that the durable artefact must be committed. A.work/(historical) note would re-commit the exact failure mode the decision is resolving (treating a git-ignored surface as the system of record).
- Vague deferral — "structured store deferred, TBD." Rejected: a deferral with no named trigger is indistinguishable from indefinite drift. The discipline here requires a concrete re-entry condition; "until spc-22 is re-planned" is that condition.
Consequences
Gains:
- The live issue-capture workflow is unchanged and unblocked — contributors and
agents keep appending to
.work/issues.md(historical) with no migration friction. - The decision is now committed and checker-assertable: this ADR (not a git-ignored file) is the artefact a reviewer or lint can point to for "why is the issue ledger still flat?"
- No half-migrated structured store exists to maintain — deferral keeps the
empty
activity/issues/path from becoming a second, partial ledger (the split-ledger failure mode).
Costs / obligations:
- AC evidence and routed follow-ups recorded only in
.work/issues.md(historical) remain invisible to PR reviewers (the gitignore). The obligation stands: any follow-up that must be durable still earns a committed home (itd-*/ ADR /iss-*once spc-22 runs) — this ADR does not relax that. - The promote-bridge lint family (
PR000/PR001/PR002, a Go implementation) and the/abcd:capture promotepath reference the structurediss-*store, which stays empty until the migration runs; those surfaces are designed-ahead but not yet exercised against a populated ledger.
Downstream decisions enabled:
- The native spec layer scheduling the migration is the single, named re-entry point for the flat-→-structured cutover. When a future spec schedules it, this ADR is superseded by the record of that migration decision.
Frontmatter
| id | adr-12 |
|---|---|
| type | adr |
| status | superseded |
| date | 2026-06-06 |
.abcd/development/decisions/adrs/0012-issue-ledger-live-vs-structured.md
open on GitHub ↗ · commit history ↗
References3
Referenced by1
- adr-32The issue ledger is working-tier data, not authored recordsuperseded by
mentions8
- dcn-26080200000000042026-08-02 — iss-39: record-lint gains `record_schema`, the first rule that reasons ACROSS the record's stores (ADRs, intents, specs, the issue ledger) rather than inside one, and the `superseded/` exemption narrows for the intent tree: both rules that read it (`intent_lifecycle`, `intent_impact_valid`) now run there too, while the spec-store rules (`spec_lifecycle`, `spec_id_unique`) still honour it as before and the cross-store `record_schema` never consulted it at all. Four invariants, one scan: a cross-reference field names a record the corpus has; a supersession is declared from BOTH sides; a filename and the id inside it agree; and every lifecycle directory is enumerated, so an undeclared bucket is a finding rather than a state no rule reads. Scope is deliberate on two axes. (1) Cross-references are checked in the machine-readable frontmatter fields (`supersedes`, `related_adrs`, `related_intents`, `builds_on`, `blocked_by`), not in bare prose: a frontmatter handle is a claim that the record exists and is a live input, whereas prose legitimately narrates ids that do not resolve ("itd-38's id was released, not reserved"; a test fixture's `itd-9999`; adr-29's account of what ADR-6 settled) — arming prose would have cost ten false positives against one true one, the same false-positive budget that scopes GL002. (2) A handle resolves either to a file or to a `supersedes:` declaration elsewhere, because the ADR lifecycle PRUNES a superseded record and keeps the trace in its successor — that is what distinguishes adr-4/adr-8/adr-14..18 (pruned, accounted for) from adr-6 (cited, never accounted for). The exemption narrowing is the root cause of the corpus defect it exposes: `intent_lifecycle` is a schema rule, but the `superseded/` bucket exempted its own records from the one rule that checks a supersession is recorded, so itd-47 and itd-49 sat there for months with a prose blockquote and no `superseded_by` field. Being historical excuses a record from how it is WRITTEN, never from being well-formed. `superseded_by` also widens from `^itd-\d+` to `^(itd|adr)-\d+`: an ADR that redecides the question an intent rested on retires that intent as surely as a successor intent does, and refusing to spell that forced those two records to say nothing at all. Corpus repairs in the same change: adr-6 is dropped from the two `related_adrs` lists and the two brief pages that cited it re-point at adr-29 (which records the two-stage redaction model and states in its own body that adr-6's decision stands — adr-29 does not supersede it, so claiming it does would have written a falsehood to clear a finding); adr-12 and adr-32 gain the bidirectional pair the ledger-location redecision always implied, adr-12's status moving to `superseded` and the index row with it; itd-47/itd-49 gain `superseded_by: adr-22`/`adr-26` with `kind_at_supersession`, and those ADRs name them back; and the pre-existing one-way links itd-32→itd-31 and itd-31→itd-48 gain their reverse halves. CHANGELOG impact `additive`, matching iss-38's disposition on the same axis: the rule ships in the lint engine `abcd docs lint` runs, and any abcd-managed repo gains it by declaring its own `record_stores`.
- dcn-26080200000000052026-08-02 — iss-39 (review pass): four correctness repairs to `record_schema`, each found by mutating the rule rather than reading it. (1) The "no lifecycle state escapes" invariant was implemented for bucketed stores only — a FLAT store (the ADRs) declares no buckets, so the walk read its files and never looked at its directories, and a whole `adrs/archive/` of records with mismatched ids and phantom successors was invisible to every check in the rule. A flat store declares zero buckets, so any subdirectory of it is undeclared by definition, and now says so. (2) The retirement escape hatch was self-attesting: `retired` was built from every record's `supersedes` before the resolution loop ran, so one record naming `adr-9999` there made that id resolve in every other record's `related_adrs` — reopening the exact phantom-reference class the rule exists to close. It is now bounded by the store's allocation high-water mark (an id above it was never issued, so nothing can have pruned it) and the declaration itself is reported; `supersedes` also leaves `recordRefFields`, where it could never fail its own check and read as coverage it did not provide. (3) The shared frontmatter scanner is a same-line scanner, so a block sequence (`supersedes:` then indented `- adr-12`) read as empty — and an empty read here did not merely under-report, it made the bidirectional check assert that ANOTHER file omitted a link that file plainly carried. Other rules degrade to "field missing on this file"; this one degraded to a confident false claim about a different file, which is worse, so the block spelling is folded in at the one place the rule parses handles. Local to `record_schema` rather than in the shared scanner: the canonical scanner has many consumers whose keys are scalars, and widening it for one rule's list fields would change what every other rule reads. (4) A dot-directory (`.obsidian`, `.vscode`) tripped a blocker as an undeclared bucket; tooling state is not a lifecycle the record authored. Also disclosed rather than fixed: widening `superseded_by` to `^(itd|adr)-` makes `intent_lifecycle` looser ALONE — it reads only the intent tree and cannot resolve an `adr-` target — so the CHANGELOG and the pattern's own comment now say the two rules are armed together and `record_schema` is what resolves a cross-store target. The surviving adr-6 prose citations are captured as iss-179 rather than papered over: adr-29 states in its own body that adr-6's decision stands and that adr-29 does not supersede it, so the supersession vocabulary cannot record it.
- dcn-26080200000000062026-08-02 — iss-39 (merge-gate pass): four more `record_schema` corrections, all of the same family as the previous round — a check that was right for one shape and absent for its sibling. (1) The undeclared-directory finding was implemented for store ROOTS only; a directory inside a declared bucket (`intents/shipped/archive/`) was skipped with no finding at all, so the previous round's flat-store fix closed the escape for one store of four. Both cases are now reported from the one place that reads a directory of records, because a flat store and a declared bucket hold records the same way and a directory in either is a lifecycle nobody declared. (2) `spc` was missing from the handle pattern although the spec store is indexed and armed: a bidirectional spec supersession tripped a false blocker whose message named the very prefixes it omitted, while the reverse direction parsed to zero handles and was never checked. The alternation now covers every store the rule indexes, and the message is composed from one constant so it cannot drift from the pattern again. (3) `superseded_by: []` was a false blocker: the empty flow sequence is this record's house spelling for an empty list (`related_rfcs: []` across six ADRs), so it means absence. Fixed with a local `isAbsentValue` rather than by widening the shared `isNull`, which also judges scalar fields (kind, impact, slug) where a list literal is a wrong value rather than an unset one. (4) The filename/id check compared strings, so `id: adr-0012` in `0012-*.md` was reported as a mismatch — contradicting the rule's own stated design that the padded and bare spellings are one handle; it now parses both sides and compares numerically, as every other comparison in the rule already did. Record-side: iss-179 now cites the recoverable evidence for what adr-6 was (`.abcd/work/reviews/2026-07-06-plan-consistency/05-link-structure.md` lines 47-49 name the original file, `03-cross-corpus-consistency.md` line 42 records it as voided by DECISIONS.md lines 8-9 together with adr-8 and adr-17 — the only other two ADRs that decision voids, both since pruned by a successor that names them (adr-25, adr-22), which is what leaves adr-6 alone — and observes that the unused `deprecated` status is the state it fits) so the disposition is a decision with evidence rather than an open question; the two brief re-pointings drop a reflexive attribution that had adr-29 reusing a model from itself; and adr-22, adr-26, and adr-32 gain one body clause each naming what their newly-declared `supersedes` handles retire, so the frontmatter and the prose say the same thing.
- iss-39record schema validation: adr-6 is cited as a live design input but does not exist and no successor records superseding it; adr-12 is not marked superseded by adr-32; itd-47 and itd-49 sit in superseded/ without supersession frontmatter; the superseded/ lint exemption is broad enough to hide content-rule violations. Detector: a mechanical record-schema check — every prose handle (adr-N, itd-N, iss-N) resolves to a file; supersession is bidirectional (superseded-by and supersedes both present); filenames match their patterns; every lifecycle directory is covered by a catch-all so no state escapes linting; the superseded/ exemption narrows to content rules only. Acceptance corpus: the four instances above.
- iss-2608211140410761abcd <id> ADR dispatch (record.go describeADR) confirms the frontmatter id with a byte-exact 'fields["id"].Value != id' compare (frontmatter.Field.Value is TrimSpace-only, quotes not stripped), while record-lint schema.go compares parsed handles (a tested tolerance, TestRecordSchemaFilenameIDComparesNumerically) and the citation resolver rebuilds ids from the parsed integer. So an ADR written id: "adr-12" (quoted), adr-0012 (padded), or ADR-12 (cased) is lint-green and citation-resolvable but abcd adr-12 reports 'not found'. A live facet against the shipped corpus: abcd adr-0003 (padded, admitted by IDRe) routes to the file then refuses. Second site under iss-285's quote-normalisation-split principle.
- iss-2608270500195431graveyard layer-2 keys ADR findings by the filename number, so two ADRs sharing it collapse to one finding and adr-012 vs adr-12 defeats cross-home dedup. GitHub mirror: #291
- spc-22a-stale-abcd-never-answers-silently-every-surface-that-runs
- spc-33abcd-mints-collision-proof-record-ids-across-parallel-agents