Development record
Reading records and disposition records: two acts, two writes
Summary
spc-58 delivers itd-180's two record types. A reading record is what an instrument returned under a recorded visible world; a disposition record is the researcher's answer to one such item, written separately and keyed to it. The two are never one write, so the record can always show that a finding existed before it was answered.
Three packages move. internal/core/issueschema gains both schemas as data,
the position-typed body sets, and the reserved hold axes.
internal/core/capture refuses a malformed record of either kind and refuses to
promote an undispositioned item. internal/core/lint gains a report-only
outstanding report, teaches record_schema the new stores, and the RS gates
learn to scope to the three issue status directories rather than to the whole
ledger tree.
The policy layer is settled in dcn-2608280000000004 and is not this spec's
to reopen: rulings (2), (8), (9), (17) and (18), and ruling (19)'s slate,
disposition_grounds and position validation. This spec settles what they left
to the build.
Scope
In: the shared status-directory list, consolidated before anything is added to
it; the reading-record envelope and its four bodies; the disposition record and
its availability rule; the reserved hold axes and the reserved surprise shape;
the on-disk layout and the three new record families; capture's refusals and the
undispositioned-promote refusal; the outstanding report; the RS-gate and
record_schema scoping. Out: everything under ## Out of scope.
Approach
One canonical status list, before a fourth folder exists
The issue ledger's status directories are enumerated three times today:
issueStatusDirs in internal/core/lint/lint.go, and two []string{"open", "resolved", "wontfix"} literals in internal/core/capture/alloc.go
(ensureLedgerDirs and issPresent). A change that adds sibling directories to
that tree must not add a fourth spelling, so the consolidation lands first,
as its own commit.
The canonical home is internal/core/issueschema: already the one place both
core/capture and core/lint read the ledger's schema from, and a leaf that
imports only the standard library, so neither side gains an import. It gains
StatusDirs; lint's issueStatusDirs becomes that value, capture's two literals
iterate it, and statusDirs / statusDirName in capture.go are built from it
rather than restated, so the State projection and the directory names cannot
disagree. The shell gates cannot import Go, so
scripts/check-issue-resolution.sh holds the second and last spelling, and a Go
test reads its declared array and asserts equality with issueschema.StatusDirs.
The layout, and why status is a directory probe
.abcd/work/issues/readings/<run-id>/rdi-<N>.md: one reading record per item..abcd/work/issues/dispositions/<item-id>/dsp-<N>.md: the dispositions of one item, in a directory keyed by the item identifier..abcd/development/readings/<run-id>/rdg-<N>.mdplusmanifest.json: the durable run record and its manifest (ruling (9)).
Keying the directory on the item and the file on the disposition's own id
settles two requirements that pull against each other. The status signal is the
presence of the keyed directory, which is one probe and never a folder-membership
question. And a disposition still has an id of its own, so the only exit from a
held state, a superseding disposition that cites the one it replaces, has
something to cite: supersedes_disposition: dsp-<N>. The standing disposition of
an item is the one no sibling supersedes; the superseded records stay in place,
because a hold that vanished when it was answered would take its own exit
condition with it
(adr-3: the
location is the state, and git is the history).
Three families, minted, never derived
rdg (run), rdi (reading item) and dsp (disposition) mint through
recordid.Minter.Mint, which validates any lowercase family and consults no
maximum (adr-45).
Two runs returning the same tension therefore carry different ids for free:
the id is a UTC stamp plus four random digits, never content-derived, so a
re-raise stays distinguishable from its first appearance and the recurrence
signal survives. No new mint is written for any of the three.
One record type, four bodies, held as data
The envelope every reading record carries is schema_version, id, run,
manifest, position, regime and pattern. The pattern named sits in the
envelope, never in a body (ruling (18)): a universal core condition must not
live in a variant part.
itd-180 offers a discriminated union and names a fallback. The fallback is
chosen: one record type, an untyped body, and a per-position required-field
set held as data in issueschema.ReadingBodyFields. Go has no discriminated
union, and this package's whole idiom is already "the schema is a value both the
writer and the gate read"; four Go types would be four places for the schema to
drift. The four sets are registrative (tension, constraint_in_play,
why_a_tension), generative (configuration, what_admits_it), explicative
(claim_surfaced, claim_type, what_implies_it) and evaluative
(candidate_id, criterion, characterisation).
The disposition vocabulary, and one count the record must settle
The disposition carries id, item, state, disposition_grounds, an
exit_condition when the state is held, an optional supersedes_disposition,
and an optional recurs list of prior item ids. The recurs citation is the
recorded form of the researcher's warm recognition of a persistence: it lives
entirely on the ledger side, is never a fourth state, and no mechanical join
produces it.
Four states ship: accepted (all positions; at the widening position acceptance
IS admission), rejected (explicative, evaluative and registrative only),
declined (widening only), held (exit condition required). itd-180 and ruling
(19) both say five states and both enumerate four. The schema is data, so a
fifth is one line the day it is named, and naming it is a vocabulary judgement
belonging to the researcher rather than to this build: the shipped enum is the
four named states, and the discrepancy is carried, not resolved here.
disposition_grounds is free text, required on every state except held, and
what it must contain varies by state, enforced by lint rather than by four
fields. Nothing meaning "already covered" exists at any position: an
undispositioned item is reported as outstanding, never named as a state.
The refusing gate
Capture refuses, rather than accepting and flagging, in every case below. Each refusal names the rule it enforces:
- an unknown property, or a body field absent for the record's own position;
- a
disposition_groundsthat is empty, on every state exceptheld; - a
helddisposition whoseexit_conditionis empty; - a state unavailable at the item's position. The disposition reads
positionfrom the keyed reading record, so an orphan disposition (no such item) is refused by the same path; - a populated
hold_frame_locationorhold_moscow. The two-axis hold field is reserved and dormant: the value grammars are stated (frame-location is free text naming the frame element; MoSCoW ismust/should/could/wont) and a populated value is refused until activation is ruled, so the reservation is a behaviour rather than a comment.
capture promote gains the same posture. It accepts an rdi-N alongside an
iss-N; for an rdi-N it reads the item's STANDING disposition before anything
is minted and refuses every state but accepted, naming the rule the refusal
enforces. An absent disposition directory is the first of those refusals and the
one itd-180 states — it collapses the answer and the action into one act, so
nothing can show the finding was weighed before it was acted on — and the same
reasoning carries the rest: a rejected or declined item would put a refusal
and the admission it refused in one ledger, and a held one would settle by
action exactly what the hold left open. Where the answer needs to change, a
superseding disposition is what changes it. The standing state is read again
inside the write's own lock, so a disposition landing between the two cannot
leave a refusal beside a stamp. On success it stamps promoted_to (historical) on the reading
record and promoted_from (historical) in the minted draft, the forward-and-back join
itd-180's routing rule requires. Circumventing it is a lapse-log entry, not a
gate.
The outstanding report is a report
internal/core/lint gains reading_outstanding: for each run directory, every
rdi-*.md with no keyed disposition is reported, and every open held
disposition is rendered with its exit condition. Its severity is pinned to
info in code, not read from RuleConfig : a rule whose severity a config
could raise to blocker is a gate waiting to happen, and a reading must never
block an unrelated push. It surfaces on abcd lint's findings and on the bare
abcd capture status board beside the skipped roster.
The gates learn the tree's new shape
record_schema reports an undeclared directory under a store root, so the two
new folders would each be a blocker on the day they appear. Three changes, all
of them general rather than special cases:
- A directory that is itself a configured record-store root is not an
undeclared bucket. The set is derived from
cfg.RecordStores, so the config stays the single declaration of what a store is. recordStoregainsbucketRe. A store whose buckets are minted rather than enumerated declares them by grammar, which is what a run-id and an item-id directory both are.- Three store entries join
recordStores, each bucketed by grammar:rdiat.abcd/work/issues/readingsandrdgat.abcd/development/readings(buckets^rdg-[0-9]+$), anddspat.abcd/work/issues/dispositions(buckets^rdi-[0-9]+$).manifest.jsonis not markdown, so the existing scan skips it untouched.
scripts/check-issue-resolution.sh replaces its single $ISSUES_DIR pathspec
with the three status-directory pathspecs. RS001 already matched only the
terminal folders; RS002 and RS003 did not, and would have read a
resolved_by.commit-shaped line out of a reading or a disposition.
Acceptance criteria mapping
| itd-180 criterion | How spc-58 satisfies it | Test |
|---|---|---|
| Validated output ingested: one reading record per item, each run-scoped | The ingest writes one rdi-N.md per item under readings/<run-id>/, each id minted by recordid.Minter.Mint("rdi") and each envelope carrying run |
TestIngestWritesOneRecordPerItem |
| Two runs, same tension, different identifiers | Ids are timestamp-plus-entropy and never content-derived (adr-45), so identity is the write, not the text | TestTwoRunsSameTensionMintDistinctIDs |
Empty disposition_grounds, or a hold with an empty exit condition: refuse |
The refusing gate above, in capture's strict validation | TestDispositionRefusesEmptyGrounds, TestHeldDispositionRefusesEmptyExitCondition |
| A state unavailable at the item's position: refuse and name the rule | The disposition reads position off the keyed reading record and checks the availability table; the error quotes the rule |
TestDispositionRefusesStateUnavailableAtPosition |
| Every reading record either carries a disposition or is reported as outstanding | The reading_outstanding report, pinned at info |
TestOutstandingReportNamesUndispositionedItems, TestOutstandingReportSeverityIsInfoNotBlocker |
Tests
Every case below is watched to fail before its change lands.
internal/core/issueschema/statusdirs_test.go:TestStatusDirsAreTheOneCanonicalListasserts capture's and lint's lists are the shared value and that the new folders are absent from it.internal/core/lint/preflightgates_test.go:TestIssueResolutionGateScopesToStatusDirsreads the shell array out ofscripts/check-issue-resolution.shand asserts equality withissueschema.StatusDirs.internal/core/capture/reading_test.go:TestReadingRecordRefusesUnknownProperty,TestReadingRecordRequiresPositionBodyFields(one sub-case per position),TestDispositionRefusesEmptyGrounds,TestHeldDispositionRefusesEmptyExitCondition,TestDispositionRefusesStateUnavailableAtPosition,TestDispositionRefusesOrphanItem,TestPopulatedHoldAxisRefused.internal/core/capture/ingest_reading_test.go:TestIngestWritesOneRecordPerItem,TestTwoRunsSameTensionMintDistinctIDs,TestSecondDispositionForOneItemRequiresSupersedes.internal/core/capture/promote_test.go:TestPromoteRefusesUndispositionedReadingItem,TestPromoteStampsReadingItemPromotedTo.internal/core/lint/reading_outstanding_test.go:TestOutstandingReportNamesUndispositionedItems,TestOutstandingReportSeverityIsInfoNotBlocker,TestOpenHoldRendersItsExitCondition.internal/core/lint/schema_test.go:TestNestedStoreRootIsNotAnUndeclaredBucket,TestReadingRunBucketDeclaredByGrammar,TestManifestJSONIsSkippedByTheRecordScan.scripts/check-issue-resolution-cases.shgainsreading-record-ignoredanddisposition-ignored, each a fixture repository the gate must pass.
Out of scope
- The output contract that "validated" refers to. It is owned by the cold-reading output-contract intent; this spec consumes it and adds no second validation path.
- The assembler and its include table, and passing a disposition to a reading. Dispositions are warm by definition: the exclusion is the assembler's to make and the read-block eval's to assert.
- Populating the surprise entry, reserved here (
occasioned_by, keyed to whatever occasioned it) and populated in Iteration 2. The admission-side records are spc-67's. - Activating the two-axis hold field. Reserved, grammar stated, populated value refused.
- Whether
heldis available at the widening position. Deferred by the facilitator on 2026-08-30 with a revisit point at the first widening run's dispositions: the availability table ships with that row unfilled, so the refusal for that one combination is not armed. - Dispatching
abcd <id>on the new families.recordid.CitedIDRecovers the four id-bearing families only, and widening it ripples across every citation surface. - Reusing
open/resolved/wontfixfor reading items, which itd-180 rejects on the merits.
Frontmatter
| id | spc-58 |
|---|---|
| type | spec |
| lifecycle | closed |
.abcd/development/specs/closed/spc-58-a-cold-reading-s-findings-land-as.md
open on GitHub ↗ · commit history ↗
References1
- itd-180A cold reading's findings land as reading records, and the researcher's response is a separate disposition record — two acts, two writes, never collapsedimplements
mentions19
- adr-3Directory location is the source of truth for lifecycle state
- adr-45Record ids are timestamp-numeric, capture-stable, and never allocated by looking at the maximum
- dcn-26082800000000042026-08-28 — Cold-reading workstream rulings adopted (facilitator; committed in design discussion, disclosed as a pre-tooling lapse): (1) the construal-admissibility extension of adr-50 files first — the construal as it presently stands is committed record and readable by automated readers, its revision history is not (adr-55 carries the full rules); the construal is sited as a brief section under 01-product/. (2) The reading record (type name ruled at (17)) is a distinct record type in the issue tier with a separate disposition record and its own disposition vocabulary — the five-state slate with availability by position, per (19); the existing issue states are not reused for it; status is the presence of the keyed disposition, never folder membership. (3) One closing reading run is scheduled over the same object set after dispositions, with interpretations fixed in advance: silence is weak evidence of settlement; an accepted-and-acted detection returning is a finding; a rejected-with-purpose detection returning bears on whether the purpose resolved the tension. Amnesia is proven by a repository eval, never by a case run. A clean opening run is recorded as a run with an empty item set; the cycle proceeds and no further reading is commissioned. (4) The supply-regime check is built now: a regime field on the output contract, validated at ingest; enforcement is the default, and degradation to record-and-flag happens only on observed noise, recorded. (5) Reading invocation carries no free text: position and target state only. (6) The lapse log is a value in capture's validated category list, not the source enum. (7) Elicitation: the articulation round produces one intent, the exit noting candidates; the escalation rule is adopted (a defaults question that turns out conjectural escalates to the options regime); a held working principle permits articulation to proceed with the mechanism recorded as an explicit nullity, flagged in the readiness summary. (8) From the readings design, agreed as proposed: the reading record carries a position-typed item body (one record type, four bodies — registrative, generative, explicative, evaluative); the selection criteria are a recorded discipline with the committed six-criterion slate, never supplied at invocation; the comparative reading's candidate set is the widening reading's pre-admission output, with admission following the comparative characterisation; the no-input-is-authoritative condition enters the blindness core (no document passed to a reading is the fixed side of any comparison), and the widening/entailment asymmetry over draft and planned intents is stated explicitly in the assembler's include list — both confirmed at the facilitator's review, 2026-08-28; the disposition-side recurs citation is adopted as the recorded form of warm recurrence recognition. (9) Ruled 2026-08-28 (revised the same day): the manifest is committed to the durable tier at `.abcd/development/readings/<run-id>/`, alongside the run record — a new record family; lifecycle selects the tier, and commit reference plus per-item hashes make the assembly re-runnable and diffable. (10) Adopted 2026-08-28: brief invariant 15 — reading contexts and the ledger never meet, the scribe is not a transcript consumer, and the session-transcript store is reached through one door by an enumerated allow list (custodian writes and reads; lifeboat session-hunting consult-only; session-separation metadata; opt-in memory curation); a new consumer is an invariant change, never merely a code path, and only the history core package touches the store's path, held by a boundary test. (11) Adopted 2026-08-28: the escalation-logging addition — every escalation in the elicitation interview is logged as data, which question and on what grounds. (12) Ruled 2026-08-28: run metadata's instrument identity comprises the model identity, the definition's content hash, and the assembler version. (13) Ruled 2026-08-28: kinds bind as stamped — the instrument trio as one bundle with a shared spec, the rest standalone, the two disciplines as disciplines. (14) Ratified 2026-08-28: the 2026-08-27 grill-pass resolutions, as implemented in the drafts. (15) Ruled 2026-08-28: warm transcript consultation proceeds with the redaction residue disclosed; transcript content is not quoted into committed records until the secret-scan work closes the residue. (16) Ruled 2026-08-28: the construal extension refines adr-50 — adr-50 stays accepted, the extension states the refinement, and adr-50's related_adrs gains the minted id at filing (adr-55). (17) Ruled 2026-08-28: the record type is the reading record (folder `issues/readings/<run-id>/`); "detection" stays the name of the Step-6 instrument and its registrative body. (18) Ruled 2026-08-28: pattern-named is an envelope field; the two assembler rules (no include names a directory containing a record family; a reading's object excludes what it exists to change) are adopted; the read-block eval also asserts the instrument's own prior outputs never reach a reading; where the widening reading returns fewer than two configurations, the comparative reading is not exercised and the outcome is recorded as such. (19) Ruled 2026-08-28 (R7): five disposition states with availability by position — admission is `accepted` (position lives on the envelope); declining a widening proposal is `declined`, never `rejected`; the grounds field is `disposition_grounds`, required on every state except `held`; the disposition record validates its state against the envelope's position. (20) Authorised 2026-08-28 (facilitator): the build session runs the intent → spec → ship ceremony autonomously for this workstream's filings — adoption, spec creation and approval, readiness, kind binding, shipped moves, and audit ingest — with every act logged; PR merges, reversal flags, new dependencies, and records outside the workstream remain human-gated. The autonomy is deliberate and is itself part of what the cycle observes; ownership of every act rests with the committing identity, per the provenance rules.
- dcn-26083000000000242026-08-30 — The disposition vocabulary ships four states and the fifth stays unnamed, which is spc-58's decision restated rather than a new one: the shipped enum is `accepted`, `rejected`, `declined`, `held`, the schema is data so a fifth is one line the day it is named, and naming it is a vocabulary judgement belonging to the researcher rather than to a build. `admitted` is not the missing fifth — ruling (19) already folds admission into `accepted` because position lives on the envelope, so introducing it would reverse that ruling rather than complete it. The `recurs` citation is likewise ruled out by spc-58 as never a state, and nothing meaning "already covered" exists at any position because an undispositioned item is reported as outstanding instead. The open candidate the record leaves is the selection vocabulary's `deferred`, which makes this the same question as whether `held` is available at the widening position — already deferred to the first widening run's dispositions, and settled there or not at all.
- iss-2608300143315694itd-188 review consistency findings: spc-66 maps acceptance criterion 2 to TestReadingAndScribeSessionsAreDistinctRecords in internal/core/history, which the build neither adds nor declares dropped (deliver it or amend the mapping to say the criterion is procedural); 05-prompt-quality.md still counts ten prompts and six outside the roster after the eleventh landed; the guard test duplicates indexTokenRe and a readRepoFile helper, carries a control that compares a literal to itself, and repeats the canary presence checks the contract rule already runs; the canary's manifest path does not match spc-58's manifest home.
- iss-2608300143316805The scribe protocol, the definition's Delivery section, and the changelog entry claim record_schema judges a scribe-emitted reading or disposition record the moment it is committed and that the validation path exists today; on this base no reading or disposition schema or store exists (spc-58 is open), so record_schema refuses the directory as an undeclared bucket and a malformed record is indistinguishable from a well-formed one. The dependency on spc-58 must be stated in all three places.
- iss-2608300257421526itd-180 second-round residue: spc-58 still says promote refuses a reading item only when the disposition directory is absent while the build refuses any standing state but accepted; lint.StandingDispositions is exported with no production caller (a single reader in the issueschema leaf would retire it); the three reading stores declare no required fields so the committed-tree gate never judges a reading or disposition's content, contrary to the reading package's header claim that the reader refuses what the writer refuses.
- iss-2608301203525338record_schema declares a bucketField only for the admission store, so an admission whose run field contradicts its bucket is named while a disposition whose item field contradicts its item-keyed directory is not — the same double claim, checked in one store and not its sibling. The disposition store declares no frontmatter schema this cycle (spc-58's stated gap), so the field was left undeclared rather than half-stating a schema in a second place. Declare it when the disposition store gains its required set.
- iss-2608310912206749two specs state prose facts about the tree that are false including a staging count wrong on all three numbers
- itd-57Every abcd Voyage Ships With A Manual-Hold Sentinel That Blocks A Spec From Autonomous Pickup Until A Human Lifts It
- itd-184Four cold-reading definitions, one blindness core — each position licenses a different output, and none may hold another's licence
- itd-188Machine assistance in maintaining the ledger, without any context that holds both ledger content and a reading
- itd-189What the widening reading proposes is admitted or declined on the record — grounds on admission, dispositions on declines, and surprises as their own entries
- spc-61The cold-reading input assembler: positive inclusion at field granularity, a pathless bundle, and a hashed per-run manifest
- spc-62Four cold-reading definitions: one verbatim blindness core, four objects, four licences no definition may borrow
- spc-63The cold-reading output contract: a strict per-position schema, a supply-regime gate, and a staged ingest that leaves evidence or nothing
- spc-66The scribe: ledger-only context, authors nothing
- spc-67Step-2 admission records: schemas now, command enforcement next cycle
- spc-2609020626040342The admission and surprise verbs: one act, two records, and the ruled order held as a refusal