Development record
Origin and production-mode keys, stamped by the command
Summary
spc-56 delivers itd-178:
two frontmatter keys, origin and production_mode, written only by the
commands that mint records, plus the lint that reports a record carrying them in
a shape no command could have produced. Neither key touches authorship. They are
disclosure at field granularity, on the same footing as the Assisted-by:
trailer at commit granularity.
Population is forward-only. Existing records are untouched, sparseness is information, and an absent stamp is never backfilled.
Scope
- Vocabulary (
internal/core/provenance/, new leaf package): The two closed value sets and the one parser that reads and renders them, held as data on theinternal/core/issueschemaandinternal/core/changelogprecedent, so the writers (core/intent,core/capture,core/spec) and the gate (core/lint) read one copy. Standard library only; no new dependency. - Resolver support (
internal/core/frontmatter): None needed. Both keys are single-line scalars, so the shared line scanner reads them unchanged. This is a deliberate constraint on the encoding, not an accident. - Issue schema (
internal/core/issueschema/issueschema.go): Both keys added toKnown. Without that, capture's reader refuses every stamped record and skips it, which is exactly how a previous flag shipped unable to execute. - Write paths:
intent.seedDraft(drafts),spec.Create(specs),capture.commitCaptureviabuildIssueText(issues), and the mutating transitionscapture.transitionandcapture.PromoteviasetScalarField. - Attribution seam (
internal/core/identity): The repo's default production mode is read from the existing.abcd/config/identity.jsonpin (itd-91), as an optional member onidentity.Pin. No second config file and no second reader. - Lint (
internal/core/lint): A newrecord_provenancerule reporting a record whose keys are in a state no write path produces. - Surfaces: A closed-choice
--production-modeflag on the record-minting verbs, and the two keys documented inbrief/04-surfaces/05-intent.mdandcommands/capture.md.
Approach
Decision — both keys are single-line scalars, and the reading pointer rides in
the origin value. A nested mapping (the resolved_by shape) is readable by
capture's own YAML subset but invisible to frontmatter.Fields, the same-line
scanner every intent and spec reader uses; a nested origin would therefore
need a second record parser, which this repository forbids. So the grammar is:
origin: researcher-authored
origin: extracted-from-record
origin: contributed-by-reading <run-id>/<item-id>
production_mode: hand-written | dictated-and-formatted | scribe-transcribed
provenance.ParseOrigin is the one predicate that reads it, and the one the
lint calls. The value contains no : sequence, so it stays a plain YAML scalar
to every reader in the corpus.
Decision — an enum flag is not free text. itd-178 bars a flag that carries
either key as free text; a closed-choice flag validated against the vocabulary
before anything is written is the same shape as --impact and --severity,
both of which already stamp machine-read enums. So --production-mode exists,
defaults to the repo's declared default (and to hand-written when the pin
declares none), and is refused outright on any value outside the set. origin
has no flag at all: it is derived from which command ran.
researcher-authored: The default for text written directly rather than derived. It names the route, not whether a person or an agent ran the command.extracted-from-record: Stamped automatically bycapture.Promote, the one shipped path that derives a record from another record.contributed-by-reading: Stamped only by the reading-ingest verb, which carries the run and item identifiers already and never asks an operator for them.
Decision — origin is stamped at mint and never rewritten; production_mode
may be restamped by a mutation that adds text. A record's arrival path is a
fact about how it came to exist and does not change when someone resolves it. A
resolution note, by contrast, is new text with its own production mode, so
capture.transition restamps production_mode when the caller declares one and
leaves it alone otherwise. This keeps the pair meaningful without making either
key a running log.
Decision — a restamp of a record carrying no origin is REFUSED, before any
write. Forward-only population means every record committed before these keys
existed carries neither, and none is backfilled. Restamping such a record would
append a lone production_mode, which is precisely the shape record_provenance
reports as a state no command produced — the gate firing on a record the command
had just written. The pair is written together or not at all, so the transition
refuses (this record predates disclosure; nothing to restamp) and names the
remedy: re-run without --production-mode. The refusal is about the RESTAMP and
never about the transition — an unstamped record stays resolvable, because
forward-only population must not strand a record nobody can close.
The gate reads the origin through the vocabulary's parser, not as a
presence test (iss-2608300941548519): a record carrying an origin outside
the closed set refuses the restamp on the same terms, because writing a mode
beside it produces a pair no command writes. The converse is a deliberate
write inside the lint's declared residual: a record carrying a valid origin
and no production_mode, itself a record_provenance blocker, is repaired
into a clean pair by a restamp, since that pair is exactly what a command
writes.
The attribution seam is extended, not duplicated. identity.Pin gains an
optional ProductionMode member; LoadPin validates it against the vocabulary
and returns an error on an unknown value, exactly as it already errors on a
malformed pin. The self-contained pre-commit identity guard seds name and
email out of that file by name, so an added member is invisible to it, and
WritePin's existing refusal of quotes, backslashes and control characters is
untouched. An absent member means hand-written.
The lint reports what no write path could have produced. record_provenance
walks the record stores through lint.LoadRecordGraph (the one canonical record
scan core/site already consumes) and reports four states:
- a value outside its closed set;
- one key present without the other, because every write path stamps both together;
origin: extracted-from-recordon an intent carrying nopromoted_from(historical) back-edge, which no promote could have written;origin: contributed-by-readingwhose run and item identifiers do not resolve to a reading record.
The residual is stated, not hidden. A hand edit that types a legal value in
a legal combination is byte-identical to a command's write, and no lint over
committed bytes can tell them apart. Closing that gap needs a stamp the record
cannot forge, which is a keyed digest the repository has no secret to hold, and
itd-178 scopes no such mechanism. record_provenance therefore catches
implausible hand edits, not all of them, and the rule's own message says so.
The honest claim this spec makes is "the lint reports a hand-edited record", and
the honest bound is "in every shape a command could not have written".
Reading-record resolution ships armed, exercised by fixture. The reading
store is itd-180's to create. Until it lands, no command in this repository can
mint contributed-by-reading, so the fourth check is proved against a fixture
reading record and has no production input. That is a sequencing fact, disclosed
here rather than discovered later.
Free text is redacted on the way in, as everywhere else. Neither key carries free text, so neither adds a redaction surface; the run and item identifiers are shape-validated ids. This is stated because it is the question a reviewer will ask of any new committed field.
Acceptance criteria mapping
| itd-178 criterion | How spc-56 satisfies it | Test that pins it |
|---|---|---|
| A record written through a command carries both keys, neither supplied as free text by the operator | All four mint paths stamp both; origin has no flag, and --production-mode is enum-validated before any write |
TestSeedDraftStampsProvenance, TestSpecCreateStampsProvenance, TestCommitCaptureStampsProvenance, TestProductionModeFlagRefusesFreeText |
origin: contributed-by-reading resolves its item and run identifiers to a reading record |
provenance.ParseOrigin extracts both ids; record_provenance resolves them against the reading store and reports a dangling pair |
TestParseOriginReadingPointer, TestRecordProvenanceReportsUnresolvableReading |
| A hand-edited record carrying either key is reported by the lint | The four record_provenance states above, over LoadRecordGraph |
TestRecordProvenanceLoneKey, TestRecordProvenanceOutOfVocabulary, TestRecordProvenanceExtractedWithoutPromotedFrom |
Tests
Each fails first: internal/core/provenance does not exist, issueschema.Known
refuses both keys today, and no write path emits them.
internal/core/provenance/provenance_test.go:TestParseOriginThreeForms,TestParseOriginRejectsUnknownKind,TestParseOriginReadingPointer(a missing or malformed id pair is refused),TestProductionModeVocabularyIsClosed.internal/core/intent/create_test.go:TestSeedDraftStampsProvenance.internal/core/spec/store_test.go:TestSpecCreateStampsProvenance.internal/core/capture/serialize_test.go:TestCommitCaptureStampsProvenance, plusTestCaptureReaderAcceptsProvenanceKeys(theKnownallow-list, proved through the reader rather than asserted about the map).internal/core/capture/promote_test.go:TestPromoteStampsExtractedFromRecord.internal/core/capture/workflow_test.go:TestTransitionRestampsProductionMode,TestTransitionLeavesOriginAlone,TestTransitionRefusesRestampOnUnstampedRecord.internal/core/identity/identity_test.go:TestLoadPinReadsProductionModeDefault,TestLoadPinRefusesUnknownProductionMode,TestLoadPinAbsentMemberDefaultsHandWritten.internal/core/lint/schema_test.go: The threerecord_provenancecases in the table, plusTestRecordProvenanceSilentOnUnstampedRecord(forward-only population is not a finding).internal/surface/cli/capture_surface_test.goandintent_cli_test.go:--production-modeaccepted, defaulted, and refused out of vocabulary.
Out of scope
- Passing either key to a cold reading. Both are excluded by the input assembler's field projection, and the read-block eval asserts it.
- The production-mode vocabulary itself, which is the ruled authorship account's decision. spc-56 ships the mechanism that stamps it.
- Backfilling existing records. Forward-only is the ruled population property.
- Any claim about authorship or credit.
ACKNOWLEDGEMENTS.mdand theAssisted-by:trailer are unchanged and untouched. - Minting the reading store or the reading-ingest verb.
Frontmatter
| id | spc-56 |
|---|---|
| type | spec |
| lifecycle | closed |
.abcd/development/specs/closed/spc-56-every-record-written-through-a-command.md
open on GitHub ↗ · commit history ↗
References1
- itd-178Every record written through a command carries its origin and its production mode — stamped by the command, never typed by handimplements
mentions8
- iss-239Dangling pre-rebuild spec ids in the draft corpus: itd-44 (spc-56), itd-51 (spc-33/37), itd-57 (spc-48/60/62), itd-61 (spc-75) cite specs from the numbering adr-21/adr-26 reset (live store is spc-1..spc-22). Worse, intents/README.md and itd-34 assert the itd-44 lineage 'landed as itd-44 (spc-56 thin adoption)' — no such spec or decision-verdict code exists, so the record claims a delivery the tree cannot back.
- iss-2608300925423309capture resolve --production-mode and capture wontfix --production-mode on a record that predates disclosure (every existing issue) append a lone production_mode with no origin, and the branch's own record_provenance blocker then reports the record the command just wrote as a state no command produced; the restamp test covers only an already-stamped fixture. Refuse a restamp when the record carries no origin (this record predates disclosure; nothing to restamp), state it in spc-56, and test on an unstamped fixture. Low, noted: resolveProductionMode reads the pin on paths that mint nothing, so a malformed pin refuses unrelated operations — fail-closed by design.
- iss-2608300931349990itd-178 prose findings: the plugin pages say every record a command writes carries the two keys, but capture disposition writes a dsp record with neither (narrow to intent, spec and issue records, as spc-56 scoped); the same pages assert the keys are excluded from every reading by the input assembler's field projection, describing code not in this tree (the sentence lives in spc-56's out-of-scope list — delete it from the user-facing surface); the internal package map omits the new provenance leaf while listing its sibling leaves.
- iss-2608300941548519itd-178 second-round observations: the restamp gate tests origin presence, not validity, so a record with an out-of-vocabulary origin accepts a restamp (the origin stays invalid and the lint still reports it — harmless; calling the origin parser would make the intent literal); a record carrying a lone origin, itself a blocker, is silently repaired into a clean pair by a restamp — a legal write inside the lint's declared residual, worth a sentence in spc-56.
- itd-34Intents Promote In Three Flavours — Standalone, Bundle-Member, Discipline
- itd-44A Capture-Time `decision` Verdict That Routes Infrastructure Choices Into The Existing ADR Store
- itd-91A Project Declares Once How It Wants AI Acknowledged — And Every Commit, PR, And Record Follows
- 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 collapsed