Development record
The reading-occasioned origin: promote stamps the run and the item it graduated, and the stamp primitive refuses the kind without them
Summary
spc-2609020626042168 delivers
itd-2609020625400169.
Today capture promote <rdi-N> mints a draft whose origin reads
extracted-from-record: promoteReadingItem in internal/core/capture/promote.go
passes provenance.KindExtractedFromRecord into intent.CreateDraft exactly as
the issue route does, and provenance.NewStamp refuses KindContributedByReading
outright. Its doc comment says the kind "is minted only by the reading-ingest
verb", and its error reads, exactly, "origin contributed-by-reading is minted
only by the reading-ingest verb, which carries the run and item identifiers; no
write path in this repository can supply them". That verb has shipped
(itd-185)
and it mints items, not intents. The one command that moves a reading item
toward an intent is promote, so the third arrival path of
itd-178
has a lint that resolves it (checkRecordProvenance in
internal/core/lint/provenance.go maps an item to its run directory) and no
writer.
This spec gives it the writer. The pair is taken from where the item was found:
capture.findReadingItem returns the run and the path
readings/<rdg-N>/<rdi-N>.md (through readingitem.Locate once the
condition spec lands), so the run is the directory the item sits in, which is
the same join the lint checks. The stamp primitive gains a constructor that takes the pair and refuses
it unshaped, and NewStamp keeps refusing the kind without one. Link mode
writes both edges and leaves the draft's origin untouched. The issue route is
not changed. The promoted draft's Press Release seed carries no item id, so the
join lives only in fields no reading projects.
Scope
In: A reading-pointer constructor in internal/core/provenance; the parsed
origin as the draft-mint option in internal/core/intent; the reading route of
capture.Promote deriving the run from the item's store path and refusing a
record whose own run disagrees; the seed note of a reading-route draft; a
promoted_from (historical) writer for link mode; the lint checking the join from both
ends; a promoted-seed plant in the read-block eval; the two plugin pages saying
which path writes which value.
Out: The issue route's stamp; any backfill; a landing other than an intent draft; a keyed digest that would make the stamp unforgeable, which itd-178 scopes out and this spec does not reopen.
Approach
The stamp primitive: NewReadingStamp, with NewStamp unchanged for the other two kinds
provenance.NewStamp(kind Kind, mode string) keeps its signature and keeps
refusing KindContributedByReading. A call that supplies no pointer is exactly
the call the intent's fourth criterion says must refuse, so that door stays
shut; only its message changes, to say the kind is minted through the
constructor below, because the sentence about the ingest verb being the minter
has been false since itd-185. Beside it:
// NewReadingStamp is the ONE constructor of a contributed-by-reading stamp.
// The run and the item are the pointer the value carries; a stamp without
// them is unrepresentable, which is why NewStamp refuses the kind.
func NewReadingStamp(run, item, mode string) (Stamp, error)
It refuses an empty run or item, a run that fails readingRunRe, an item that
fails readingItemRe, and a mode ModeOrDefault refuses, and returns
Stamp{Origin: Origin{Kind: KindContributedByReading, Run: run, Item: item}, Mode: m}.
The rendered value round-trips through ParseOrigin byte for byte, which a
test pins.
The fourth criterion has two halves, and the package holds one of them. The
primitive is a leaf with no filesystem, so it can judge shape and nothing
else: No value can be built without a well-formed pair, and its doc comment
states that division rather than implying more. Resolution to the readings
store is the promote path's, which reads the store before it mints, as the
next section states; the intent's fourth criterion is worded to that split.
The comment on KindContributedByReading is rewritten to name promote as the
minter.
The draft-mint option carries the parsed origin
intent.DraftOptions.Origin changes type from provenance.Kind to
provenance.Origin, so a caller naming the reading kind must hand over the run
and the item in the same field. CreateDraft in internal/core/intent/create.go
branches on it:
- Kind empty:
KindResearcherAuthored, as now. - Kind reading:
provenance.NewReadingStamp(o.Run, o.Item, opts.ProductionMode), and additionallyopts.PromotedFrommust equalo.Item, or the mint refuses ("a reading origin names item X but the back-edge names Y; the two are one join"). The two fields are redundant by design, and a draft carrying them in disagreement is a state no command produced. - Otherwise:
provenance.NewStamp(o.Kind, opts.ProductionMode), as now.
seedDraft is untouched in what it writes to the frontmatter: It writes
stamp.OriginValue(), and the reading value renders as
contributed-by-reading rdg-N/rdi-N, a plain scalar with no : inside it,
which is what keeps it readable to frontmatter.Fields. The two call sites
move with the type: CreateFromText sets nothing, as now, and the issue route
of capture.Promote passes
provenance.Origin{Kind: provenance.KindExtractedFromRecord}.
The seed carries no item id, and why the title may
seedNote today renders the Press Release placeholder as
_Seeded by promotion from <promoted_from>. Expand into the full press-release narrative before planning._ (historical),
and the Press Release is the first field intentProjection projects; a draft
is admitted at the entailment position. A promoted reading draft would
therefore hand the entailment reading an rdi-N, a prior reading's output
named inside the object of the next one, which the readings companion forbids
("no reading sees another's output"). The seed of a reading-route draft
becomes _Seeded by promotion from a reading item. Expand into the full press-release narrative before planning._:
same opening, same tail, so IsSeedNote goes on matching both forms by prefix
and suffix, and the issue-route seed keeps its iss-N, since an issue is not
a reading's output. The join to the item is carried where no projection
reaches: origin and promoted_from (historical) are frontmatter keys the projection does
not name (the read-block eval's DRAFT-ORIGIN class holds origin warm at
every position), and the "Graduated from" line that names the item sits in
Why This Matters, which is not projected either.
The draft's title stays the item's pattern. That is admissible where the id
is not: A pattern is the name of an established practice the reading pointed
at, a name that stands on its own in the literature or the record and would be
the title a person typed; it carries no finding, no tension and no identifier,
and a reader handed it learns what the draft is about, not what an instrument
returned. The reading record stays the single source of the instrument's own
text.
The read-block eval gains a promoted-seed case in its DRAFT-BODY class: A
second draft fixture under testdata/cold-reading/baseline/.abcd/development/intents/drafts/,
minted in the reading-route shape (the reading-form seed in its Press Release
carrying the class token, origin: contributed-by-reading naming a fixture
run and item, promoted_from (historical) naming the item, the "Graduated from" line in
Why This Matters). The class's Count moves to 2 and sentinelClasses does
not, since no class is added; the baseline run proves the token cold at
entailment alone, and a companion assertion in the same test requires the
fixture item's id in no bundle at any position, which is the sentence above
made falsifiable.
The promote path for a reading item derives the pair from the store
In promoteReadingItem, the run comes from
capture.findReadingItem(issuesRoot, req.ID), which returns the run and the
path together. This spec lands before the condition spec (see the landing
order), so it calls the capture function as it stands, and when that spec
makes the function a thin wrapper over readingitem.Locate this path
resolves through the leaf without an edit. readingItemPaths admits only a
run directory whose name passes recordid.ValidReadingRunID, so the derived
run is well formed by the rule the store enforces, and the pair resolves by construction: It was read off
readings/<run>/<item>.md, which is the store read that delivers the
resolution half of the fourth criterion. Belt and braces: The record's own
run field, which issueschema.ReadingRequired makes mandatory, must equal
the directory name, and a disagreement refuses before anything is minted
("rdi-N sits in run rdg-A but its record names rdg-B; nothing minted"). The
mint then passes
Origin: provenance.Origin{Kind: provenance.KindContributedByReading, Run: run, Item: req.ID}
with PromotedFrom: req.ID. The mint-first, stamp-second ordering, the
under-lock re-read of the standing disposition, and the orphan remedy are all
unchanged.
The standing disposition the re-read requires is accepted, and at the
widening position an accepted disposition can be written only once a
committed comparative run names the item's widening run: The shared
disposition writer refuses every disposition write at that position until
then. Promoting a widening item is therefore transitively gated on the
comparative run. This spec does not implement or test that gate, which is the
comparative channel's and the admission spec's; it inherits it through the
disposition it reads, and the first criterion is read under that condition.
Link mode writes both edges and leaves the origin alone
Today --intent <itd-N> on either route stamps only the source record's
promoted_to (historical); the existing draft gains nothing. The intent requires the
reading route to write promoted_from (historical) on the draft as well. A new
intent.SetPromotedFrom(repoRoot, intentID, source string) (Intent, error) is
added in internal/core/intent/lifecycle.go beside Link, on the same
readRepoFile, setFrontmatterFields, writeIntentFile idiom. It refuses an
intent not found in any bucket and a source failing promotedFromRe; a draft
already naming this source is a no-op that reports the record unchanged. It
writes promoted_from (historical) and nothing else. It never reads or rewrites origin or
production_mode, which is what "origin unchanged" rests on: An origin is
stamped at mint and never rewritten, so a draft filed from quoted text and linked
to a reading item stays researcher-authored and says so.
A draft whose promoted_from (historical) already names a different record is the case the
intent's first scope condition describes: An intent occasioned by several
items is promoted from one, and the others are joined by their own
promoted_to (historical). SetPromotedFrom returns a typed ErrBackEdgeTaken naming the
record already there; the reading route does not treat it as a refusal. It
skips the back-edge, goes on to stamp the item's promoted_to (historical), and reports
back_edge: kept <existing> in the result and in both renderings, so the
operator sees that the draft's one back-edge stayed where it was and the item
still points forward.
Ordering on the reading route: The draft write runs after the pre-flight and before the ledger-locked item stamp. A failure between the two leaves a draft naming the item and an item not yet naming the draft; re-running the same command completes it, because the draft write is idempotent and the stamp is the step that was missing. The issue route's link mode is not changed, per the intent's scope.
The lint checks the join from both ends
provenanceFindings in internal/core/lint/provenance.go gains one same-record
check: A contributed-by-reading origin on a record whose promoted_from (historical) is
absent, or names an item other than the origin's, is a finding, on the same
footing as extracted-from-record with no back-edge. checkRecordProvenance
gains a second map from the scan, item to promoted_to (historical), and reports an intent
whose origin names item X while X's promoted_to (historical) names a different record. The
reverse direction is deliberately not a finding: An item whose promoted_to (historical)
names a researcher-authored draft is link mode working as designed, and so
is an item whose promoted_to (historical) names a draft whose promoted_from names
another item, which is the several-items case above. Every message keeps
handEditResidual.
The plugin pages say which path writes which value
commands/capture.md's disclosure section currently says the reading value is
one "which only the reading-ingest verb mints"; that sentence becomes "which
capture promote <rdi-N> mints when it derives a draft from an accepted reading
item, naming the item's run and id", and the promote section's paragraph on
reading items gains the origin the draft carries and the kept-back-edge report.
commands/intent.md's disclosure section, which today names only two values,
gains the third with the same attribution. Neither page gains a flag: The
value is derived from which command ran, as before.
Landing order
The Iteration 2 set lands in two phases around the maintainer's readings, on the corrections ruling of 2026-09-02. Phase A, before any reading runs: The two-operand invocation (spc-2609021004075744), itd-194 (spc-2609021003136831), the preset windows (spc-2609020626048722), the comparative channel (spc-2609020626039834) and the reading-occasioned origin (spc-2609020626042168), in that order. The maintainer then runs the four readings in the cycle's order, widening, entailment, comparative and detection, and dispositions their outputs. Phase B, built at Step 5 after those dispositions: The condition disposition (spc-2609020626046252), the admission and surprise verbs (spc-2609020626040342), the reframe record (spc-2609020626048705), the scribe verbs (spc-2609020626045177) and the principles read object (spc-2609020626042471), in that order. No spec names a target version number: Each names the class of bump it makes, and the merging change sets each constant from the merged base and updates every pinned count in the same diff.
This spec lands last in Phase A, on the corrections ruling: The framework's
section 6.3 has the origin key written only by commands, and no promotion of
an accepted reading item may precede its writer, so the writer is in place
before the first reading runs. Because it lands before the condition
disposition, it calls capture.findReadingItem as that function stands and
inherits the leaf without an edit when the condition spec makes the function
a thin wrapper over readingitem.Locate. The gate it inherits on a widening
item's promotion is the comparative channel's, which lands before it, and the
admission verb's, which lands in Phase B; between the phases an accepted
disposition on a widening item is hand-authored in the target format after
the comparative run, and the promote path reads it as it reads a verb-written
one. This spec moves neither SchemaVersion nor AssemblerVersionCore: It
changes no artefact field, no kind and no include row, and the eval plant is
a fixture, not a table row.
How the Acceptance Criteria are satisfied
- ac-1 (mint stamps the pair).
promoteReadingItemderives the run from the item's store path and mints throughNewReadingStamp, so the draft's frontmatter readsorigin: contributed-by-reading <rdg-N>/<rdi-N>. Proved byTestPromoteReadingItemStampsContributedByReading, which reads the line back and then runs the record-provenance rule over the fixture and requires zero findings, so "the provenance lint resolves it" is exercised rather than inferred. The fixture item is at a position other than widening, since at widening theaccepteddisposition the route requires waits on the comparative run. - ac-2 (link mode writes both edges, origin unchanged).
SetPromotedFromwrites the back-edge, the ledger-locked stamp writespromoted_to(historical), and neither touches the disclosure pair. Proved byTestPromoteReadingItemLinkWritesBothEdgesAndLeavesOriginAlone, which asserts the two disclosure lines are byte-identical before and after. - ac-3 (issue route unchanged). The issue route passes
Origin{Kind: KindExtractedFromRecord}and nothing else moves. Proved by the existingTestPromoteStampsExtractedFromRecord, re-run unchanged. - ac-4 (the primitive refuses the kind without a well-formed pair; resolution is the promote path's).
NewStampkeeps refusing the kind, andNewReadingStamprefuses an empty or unshaped run or item; that is the shape half, proved byTestNewStampRefusesTheReadingKindWithoutAPointerandTestNewReadingStampRefusesAnUnshapedPair. The resolution half is the store read inpromoteReadingItem, proved byTestPromoteReadingItemStampsContributedByReading(the pair it stamps is the one it read) andTestPromoteReadingItemRefusesARunMismatch.
Tests
Every test is watched fail before the change and pass after.
internal/core/provenance:
TestNewReadingStampCarriesTheJoin: A well-formed pair yields a stamp whoseOriginValue()iscontributed-by-reading rdg-N/rdi-N.TestReadingStampRoundTripsThroughParseOrigin: Render then parse gives the sameOrigin.TestNewReadingStampRefusesAnUnshapedPair: Empty run, empty item, aniss-Nwhere an item is wanted, an item where a run is wanted, and a pair with surrounding whitespace each refuse; nothing is defaulted.TestNewStampRefusesTheReadingKindWithoutAPointer: The existing refusal, kept, and its message now namesNewReadingStamp.
internal/core/intent:
TestCreateDraftStampsAReadingOrigin: The rendered frontmatter carries the pair and the production mode together.TestCreateDraftRefusesAReadingOriginDisagreeingWithTheBackEdge: An origin namingrdi-1withPromotedFrom: rdi-2mints nothing.TestReadingRouteSeedNamesNoItem: The Press Release of a draft minted with a reading origin contains nordi-,IsSeedNotestill matches it, and the issue-route seed is unchanged.TestSetPromotedFromWritesOnlyTheBackEdge: Every other frontmatter line is byte-identical after the write.TestSetPromotedFromReportsATakenBackEdgeAndIsIdempotentOnTheSame: A different source returnsErrBackEdgeTakennaming the record already there and writes nothing; the same source is a no-op.
internal/core/capture:
TestPromoteReadingItemStampsContributedByReading(ac-1, ac-4).TestPromoteReadingItemRefusesARunMismatch(ac-4): A record whoserunfield names a run other than its directory mints nothing.TestPromoteReadingItemLinkWritesBothEdgesAndLeavesOriginAlone(ac-2).TestPromoteReadingItemLinkKeepsAnExistingBackEdge: A draft already promoted from another item keeps that back-edge, the second item'spromoted_to(historical) is stamped, and the result reports the kept record.TestPromoteReadingItemLinkCompletesOnRerunAfterAStampFailure: UnderstampWriteHookthe first run fails after the draft write; the second run finds the back-edge already present and completes the stamp.TestPromoteStampsExtractedFromRecord(ac-3, existing).
internal/core/lint:
TestRecordProvenanceRequiresTheBackEdgeBesideAReadingOrigin.TestRecordProvenanceChecksTheForwardEdge: An item whosepromoted_to(historical) names a draft other than the one whose origin names it is reported once, on the draft.
internal/surface/cli:
TestCapturePromoteReadingItemStampsTheOriginPair: The JSONintent_pathnames a file whose origin line carries the pair, so the front door is shown executing the core change rather than assumed to.TestCapturePromoteReadingItemLinkReportsAKeptBackEdge: Both renderings carry the kept-back-edge report.
evals: The read-block lane over the promoted-seed plant, baseline and holed,
run explicitly.
Out of scope
- Backfilling any record: Population is forward-only, and no draft exists today whose origin this would have changed.
- A reading item promoted to anything other than an intent draft. A discipline, an ADR or a brief passage carries its join by its own means.
- The issue route's link mode writing
promoted_from(historical) on the draft. The intent holds the issue path unchanged, and extending it is its own change. - The gate on dispositions at the widening position. It lives in the shared disposition writer under the comparative channel and admission specs; this spec inherits it.
- A mechanism that could tell a hand-typed legal value from a command's write. The lint's residual is disclosed in every message it emits, as itd-178 ruled.
Frontmatter
| id | spc-2609020626042168 |
|---|---|
| type | spec |
| lifecycle | closed |
.abcd/development/specs/closed/spc-2609020626042168-an-intent-that-a-reading-occasioned-says.md
open on GitHub ↗ · commit history ↗
References1
- itd-2609020625400169An intent that a reading occasioned says so in its origin, with the run and the item that occasioned itimplements
mentions12
- itd-178Every record written through a command carries its origin and its production mode — stamped by the command, never typed by hand
- itd-185One ingest verb validates every cold-reading output — including what the reading was licensed to produce, not only what it saw
- itd-194The reading include table admits only what the exclusion floor can read
- spc-2609020626039834The comparative channel: one widening run's candidates, two fields each, characterised before anyone admits one
- spc-2609020626040342The admission and surprise verbs: one act, two records, and the ruled order held as a refusal
- spc-2609020626042471The knowledge record becomes a read object: A principle carries typed claims, projects its statement, and inherits only what held
- spc-2609020626045177The scribe verb: an allow-list assembler, an authoring-refusing ingest, and a per-run context stamp the transcript store can check
- spc-2609020626046252A second writer into the disposition surface: `abcd intent condition` dispositions one scope condition from a reading item or a delivered intent
- spc-2609020626048705The reframe record: `capture reframe` writes one `rfm-N` keyed on the committed fingerprints of the three frame surfaces before and after, joined to the reading item, disposition or surprise that occasioned it
- spc-2609020626048722One committed entry per position, in three parts: the object set, the kinds within it and the window it measures at, held by an eval that re-measures every entry on every committed change
- spc-2609021003136831The include table declares what the floor parses, the floor refuses the markdown it cannot resolve, and the manifest marks every item it did not examine
- spc-2609021004075744The invocation is a position and a target state: the scope operand and the override stamp leave, and the committed preset for the position is applied by the assembler