Development record
The scribe verb: an allow-list assembler, an authoring-refusing ingest, and a per-run context stamp the transcript store can check
Summary
spc-2609020626045177 delivers
itd-2609020625402599.
abcd scribe assemble --run <rdg-N> builds the scribe's context for one
ingested run from the ledger's allow list, which is derived from the ledger's
own directory constants, and from the researcher's supplied dispositions text:
The run's reading records as the store holds them, the standing dispositions,
admissions, surprises and reframes, and the supplied text. It parks the
context and a manifest of what it passed in the local tier. abcd scribe ingest --scribe-json <path> validates the scribe's four outputs, writes
dispositions, admissions and surprises through the capture verbs and the
gate they share, refuses a payload that authors anything, and promotes the
manifest beside the run inside the read block. Every reading bundle and every
scribe context carries a per-run context stamp, the transcript store records
at capture which stamps a transcript carried, and abcd history gains a check
that names a retained transcript carrying both stamps of one run, reports
that no retained transcript carries two stamps of one run when none does, and
says when the property is unobserved.
spc-66
met both of
itd-188's
criteria by declaration; this spec meets them by mechanism.
The decision this spec builds on is adopted:
adr-2609021016275803
carries status: accepted, adopted by the maintainer on 2026-09-02 at the
planning interview after being checked against the design framework and the
readings companion, and
iss-2609020626114155
is resolved by it. Brief invariant 15 already carried the sentence "No session
holds both a reading and the ledger"; the ADR states the mechanism behind it,
the per-run context stamp and the session-separation check that reports
held-for-what-was-seen or unobserved and never clean, and the invariant is
amended to name both. The ADR also states, so nobody reads it as a departure
from the framework, that the scribe transcribes dispositions and reads the
run's reading records from the store: The ingest verb writes the reading
records first, and the scribe is handed the committed records rather than a
raw output a second time, which is what this spec's assembler does below.
scribe is a top-level verb, because the two contexts must never share a
front door.
Scope
In: The internal/core/scribe package with its assembler, context, manifest
and ingest; the scribe verb and its two sub-verbs; the sessionkind package
and the per-run stamp on the reading bundle and the scribe context; the
transcript store's context_stamps metadata and the separation check; the
history separation sub-verb and the line on history list; the definition's
Inputs and Delivery sections; the plugin page, the brief chapter and its index
row, the history chapter's sub-verb row, the internals README entry and the
surface snapshot; the eval plant.
Out: Any judgement by either verb about the content transcribed; a change to the scribe's access rule; redaction policy, which the capture validators already apply; the ordering gate the disposition writer holds, which spc-2609020626040342 builds and this verb inherits; the durable-tier writer, which spc-2609020626039834 exports and this verb calls; the ADR and the invariant amendment.
Approach
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's classes: SchemaVersion on the bundle and the manifest moves by
one, because the bundle gains a field; AssemblerVersionCore moves MINOR,
because the bundle shape is part of the contract. It lands fourth in Phase B,
after the admission and surprise verbs, whose Admit and Surprise it
writes through, and after the reframe record, whose reframes directory its
derived allow list already names.
The context stamp, and where it lives
internal/core/sessionkind is a leaf package: The two kinds Reading and
Scribe, Stamp(kind, run, digest string) string rendering
abcd.context-stamp/<kind>/<rdg-N>/<sha256-12>, Parse, and StampRe, the
one expression that recognises a stamp. Both reading and history import
it; neither imports the other. A stamp is per run, so a session that reads
the docs or the source, where the kind tokens are committed, carries no
stamp, and a stamp is matched exactly, so a transcript carrying the reading
stamp of one run and the scribe stamp of another is not a violation.
The digest is the first twelve hex digits of the sha256 over the context's
own item set: For the bundle, its items array as serialised; for the scribe
context, its ledger array. Bundle in internal/core/reading/manifest.go
gains ContextStamp string (context_stamp), set by Assemble for the
run it mints; it is a token, not a path, so brief invariant 15 holds, and
TestNoBundleFieldIsAScopeSelector in internal/core/reading/scope_test.go
gains the member. The closed bundle allow-set in
TestBundleGainsNoFieldFromTheReport in internal/core/reading/size_test.go
gains context_stamp; the item allow-set there does not move for this spec.
The scribe context carries context_stamp in the same position.
The stamp reaches a transcript because a session reads its bundle or its context through a tool whose result the host retains, which is the intent's mechanism and its disclosed limit: Where a host hands content the transcript does not retain, the check reports the property unobserved.
scribe assemble
internal/core/scribe/assemble.go adds Assemble(req AssembleRequest) with
AssembleRequest{RepoRoot, Run, DispositionsPath, OutDir, OutDirLabel string; DryRun bool}. Run is validated by recordid.ValidReadingRunID and must be
an ingested run, with its commit marker at
.abcd/development/readings/<rdg-N>/run.json, refused otherwise: The scribe
transcribes a reading the ledger already holds, so the run's items come from
the store, never from a raw reading output. The one supplied path is the
researcher's dispositions text, read behind fsutil.ReadGuarded at
reading.MaxFileBytes, in any form they wrote it.
The context is positive inclusion at directory grain. AllowList() is
derived from issueschema.LedgerDirs(), the function spc-2609020626039834
adds beside the ledger directory constants, under capture.LedgerRelPath:
readings, dispositions, admissions, surprises, reframes and the
three status directories open, resolved and wontfix. A family the
ledger declares later is on the list the day its constant is, and the
comparative position's exclusion rows derive from the same function, so the
two instruments describe one set. The collector walks those directories and
nothing else, refusing a symlinked directory or leaf as capture's readers
do, reading each .md behind fsutil.ReadGuarded at
issueschema.RecordReadLimit. The shipped tree, the brief, the intents, the
specs, the decisions and the transcript store are excluded because no walk
starts in them, and assertAllowList is the fail-closed half: It refuses to
emit any item whose path lacks one of the allow-list prefixes, so an item
that reached the context by any future route is refused rather than
disclosed (adr-56). The transcript store sits outside the repository tree and
is unreachable by construction; the context also names no home path, held by
the same scrub reading applies.
The context is abcd.scribe.context/1: _type, schema_version,
context_stamp, run, ledger (one {path, text} per record, sorted by
path, the named run's reading records among them), and supplied
(dispositions, verbatim). The scribe's material may name ledger paths,
because the scribe files by them; it is the reading bundle that may not. It
lands at .abcd/.work.local/scratch/scribe-runs/<rdg-N>/context.json or
under --out, on reading assemble's empty-directory and dry-run rules.
The manifest is abcd.scribe.manifest/1: _type, schema_version,
context_stamp, run, definition_sha256 (the hash of agents/scribe.md,
read as LoadDefinition reads a reading definition), context_sha256,
supplied (dispositions_sha256), items (one {path, bytes, sha256} per
ledger record), allow_list, and exclusions (the shipped tree, the durable
record outside the ledger, the local tier, and the transcript store, each
with its signal). At assemble it is parked beside the context as
scribe-manifest.json, in the local tier, and nothing in the durable tier is
touched: A scribe session that is assembled and never ingested leaves no
trace beside the run.
scribe ingest
internal/core/scribe/ingest.go adds Ingest(req IngestRequest) with
IngestRequest{RepoRoot, ScribeJSONPath, ContextPath string}. The payload is
abcd.scribe.output/1, decoded strictly:
{
"_type": "abcd.scribe.output/1",
"run": "rdg-N",
"context_sha256": "…",
"dispositions": [{"item": "rdi-N", "state": "accepted", "grounds": "…",
"exit_condition": "", "supersedes": "", "recurs": []}],
"admissions": [{"item": "rdi-N", "grounds": "…"}],
"surprises": [{"occasioned_by": "…", "text": "…"}],
"fidelity_flags": [{"first": "…", "second": "…"}],
"outstanding": ["rdi-N"],
"refusals": [{"subject": "…", "reason": "…"}]
}
The run's identity is proven as reading.Ingest proves one: The parked
manifest is read from the local tier, and the context at ContextPath
(default, the local-tier path above) must hash to the manifest's
context_sha256. Until that holds nothing is written.
The authoring refusal has three parts, each naming the field and the item. A
key outside the closed shapes above, resolution, pattern, position or
any other, is refused by strict decoding with the key named. A disposition
whose item does not occur in supplied.dispositions is a disposition the
researcher did not supply. A disposition grounds, an admission grounds,
an exit_condition or a surprise text that does not occur verbatim, after
whitespace folding, in supplied.dispositions is a ground the researcher did
not write. The scribe reformats; it never adds a word, and the check is that
every word it carries was already there. Every item in outstanding must be
an item of the run with no entry in dispositions, and the result reports
the list; an item of the run that appears nowhere in the payload is refused,
because silence is not one of the scribe's options.
Writes go through the verbs' own functions, in payload order:
capture.Disposition, then capture.Admit, then capture.Surprise (the
last two delivered by
spc-2609020626040342),
each under the ledger lock it takes for itself, each inheriting the redaction
and the refusals it already applies. The verb adds no validation path of its
own. Two inherited refusals are named here because a scribe payload meets
them whole. First, the ordering gate in the shared disposition writer: At the
widening position no disposition in any state is written until a committed
comparative run names the item's run, so a payload over a widening run nobody
has characterised refuses at its first disposition, before anything lands,
and the result names the comparative run the ledger is waiting for. Second,
Admit's admission-alone branch requires the admission's ground to equal the
standing disposition's, so a payload carrying a disposition and an admission
for one item on two grounds refuses at the admission and names both texts;
spc-2609020626040342's one-ground rule therefore holds on this path without a
second check. A refusal from any write stops the ingest and the result names
what landed before it, so a rerun can see the partial state rather than mint
it twice. Fidelity flags and refusals are carried into the result unresolved
and never into a record, which is what the definition promises.
Once every write has landed, the parked manifest is promoted to
.abcd/development/readings/<rdg-N>/scribe-manifest.json through
reading.WriteRunArtefact, the one exported durable-tier writer, which
spc-2609020626039834 adds: It opens the containment root reading.Ingest
uses, requires the run's run.json, and refuses an existing file, because the
durable tier is write-once at emission. Promotion comes last so that a refused
ingest leaves the manifest parked and a rerun re-proves the same context. That
directory is denied to every assembly by the exclusion floor's
.abcd/development/readings row, so the next reading cannot see it.
The transcript store's half
history.Capture in internal/core/history/history.go scans the raw
transcript with sessionkind.StampRe before redaction (a stamp carries no
secret) and records every distinct stamp it found in the frontmatter as
context_stamps, a comma-joined list; Record gains ContextStamps []string, and marshalRecord and parseRecord in
internal/core/history/store.go carry it, the field being optional on read so
that a record captured before this change still parses and
recordSchemaVersion does not move. The stamp is therefore metadata, and the
check reads metadata only, which is the consumer brief invariant 15 already
enumerates as "session-separation evidence (metadata only, never bodies)".
internal/core/history/separation.go adds
SessionSeparation(rootSHA string) (SeparationReport, error) over List:
{Transcripts, Stamped int; Runs []string; Violations []Violation{SessionID, File, Run string}; Unobserved bool; Reason string}. A violation is a
transcript carrying both the reading stamp and the scribe stamp of one run,
named by session id, filename and run. Runs lists every run any stamp
named, so a clean report says what it examined. Unobserved is true, with
the reason stated, when the store holds no transcript or none carrying any
stamp: A check that ran and saw nothing and a check that could see nothing
must not produce the same artefact (adr-56).
abcd history separation is a read-only sub-verb rendering the report: The
violations by name, or "no retained transcript carries two stamps of one run"
with the runs it saw, or the unobserved reason. history list's text render
ends with the same one line; the list's JSON stays an array, so a consumer of
it is untouched. The smoke lane's TestReadOnlyVerbsRun in
evals/smoke_test.go gains history separation.
The surfaces and the definition
internal/surface/cli/scribe.go registers scribe with assemble and
ingest, on reading.go's shape: Closed operands, exit 2 on every refusal,
the operator's own spelling of --out echoed back, and a result rendered on
the refusal path whenever it has something to disclose. commands/scribe.md
is the plugin page and states the host obligation: The scribe session is
granted the context and nothing else, and it is not the reading session.
.abcd/development/brief/04-surfaces/24-scribe.md is the chapter, with its
sub-verb table; the surfaces README.md gains row 24 and scribe in its
<!-- index: commands --> region; 11-history.md gains the separation
row; .abcd/development/release/surface.json is regenerated.
internal/README.md gains entries for core/sessionkind/ and
core/scribe/, on the core/grounds/ entry's shape: What the leaf holds and
why it is not inside either package that imports it.
agents/scribe.md moves to prompt_version: 0.2.0, with its entry under the
shared dated heading in agents/CHANGELOG.md that every prompt bump landing
in this iteration uses. Its Inputs list is regenerated from AllowList(), so
it will name the admissions, surprises and reframes stores beside the five
directories it names today: A completion of the ledger enumeration rather
than a change to the rule, which stays ledger content only, and
TestScribeInputsAreLedgerOnly in internal/core/lint/scribecontract_test.go
keeps holding it while a new test there holds the list to the function. Its
Delivery section names the two verbs and the payload above in place of "There
is no ingest verb", and the scribe protocol in
.abcd/development/brief/05-internals/01-agents.md is edited in two places:
Rule 3, which names reading ingest and capture disposition as the paths a
transcribed record reaches the tree through, names scribe ingest beside
them, and the sentence "Mechanical assembly belongs to the ingest verb" names
scribe assemble.
The eval plant
The read-block fixture gains
.abcd/development/readings/rdg-2608300900000001/scribe-manifest.json
carrying the EXHAUST sentinel, and the class's count and homes move with it
in evals/coldreading_fixture_test.go. TestPriorRunExhaustNeverReaches
then covers ac-8 at every assembling position with no new assertion. This is
a regression guard rather than a falsifier: A .json under
.abcd/development/readings matches no include row and sits under a denied
segment, so the plant cannot leak today; it is planted so that a row or a
segment change that would let it leak is caught by class and position.
How the Acceptance Criteria are satisfied
- ac-1: The collector walks the derived allow list, the named run's
reading records among it, and carries the supplied dispositions text; the
manifest lists every path passed;
assertAllowListrefuses any other path. Tests:TestScribeContextIsLedgerAndSuppliedTextOnly,TestScribeContextCarriesTheRunsRecordsFromTheStore,TestScribeManifestNamesEveryPathPassed,TestAssertAllowListFailsClosed. - ac-2: A supplied disposition is written through
capture.Disposition, and the ledger differs afterwards by that one file. Test:TestScribeIngestWritesASuppliedDisposition. - ac-3: An unknown key is refused by name at decode; a ground absent from
the supplied text is refused naming
groundsand the item, on the disposition and on the admission alike. Tests:TestScribeIngestRefusesAnAuthoredField,TestScribeIngestRefusesAnUnsuppliedGround,TestScribeIngestRefusesAnUnsuppliedAdmissionGround. - ac-4: An item listed in
outstandingis reported and no record is written for it. Test:TestScribeIngestReportsOutstandingAndWritesNothing. - ac-5: A transcript captured with both stamps of one run carries them in
context_stamps, andSessionSeparationnames it with the run. Test:TestSeparationNamesATranscriptCarryingBothStampsOfOneRun. - ac-6: Two transcripts, one stamp each, produce no violation, and the
report says that no retained transcript carries two stamps of one run and
lists the run it saw. Test:
TestSeparationReportsNoTranscriptCarryingTwoStamps. - ac-7: An empty store reports
Unobservedtrue with the reason. Test:TestSeparationReportsAnEmptyStoreAsUnobserved. - ac-8: The planted
scribe-manifest.jsonreaches no bundle. Test:TestPriorRunExhaustNeverReacheswith the extended plant, kept as a regression guard.
Tests
Watched fail before, pass after; each refusal proved by a mutation that removes it.
internal/core/scribe/assemble_test.go:TestScribeContextIsLedgerAndSuppliedTextOnly,TestScribeContextCarriesTheRunsRecordsFromTheStore,TestScribeManifestNamesEveryPathPassed,TestAssertAllowListFailsClosed(an item under.abcd/developmentinjected through the seam is refused),TestAllowListIsDerivedFromLedgerDirs,TestScribeAssembleRefusesAnUncommittedRun,TestScribeContextCarriesThePerRunStamp,TestScribeAssembleWritesNothingBesideTheRun,TestScribeContextCarriesNoHomePath.internal/core/scribe/ingest_test.go:TestScribeIngestWritesASuppliedDisposition,TestScribeIngestRefusesAnAuthoredField,TestScribeIngestRefusesAnUnsuppliedGround,TestScribeIngestRefusesAnUnsuppliedAdmissionGround,TestScribeIngestRefusesAnUnsuppliedDisposition,TestScribeIngestReportsOutstandingAndWritesNothing,TestScribeIngestRefusesASilentItem,TestScribeIngestProvesTheContextHash,TestScribeIngestCarriesFidelityFlagsIntoNoRecord,TestScribeIngestNamesWhatLandedBeforeARefusal,TestScribeIngestRefusesBeforeTheComparativeRun(the inherited gate, with nothing landed),TestScribeIngestPromotesTheManifestLast(a refused ingest leaves it parked; a completed one lands it write-once).internal/core/sessionkind/sessionkind_test.go:TestStampsArePerRunAndMatchedExactly,TestStampReRecognisesOnlyAStamp.internal/core/reading/manifest_test.go:TestBundleCarriesTheReadingStampOfItsRun, andTestNoBundleFieldIsAScopeSelectorcoveringContextStamp;internal/core/reading/size_test.go:TestBundleGainsNoFieldFromTheReportwith the bundle allow-set extended.internal/core/history/separation_test.go:TestCaptureRecordsContextStamps,TestARecordWithoutContextStampsStillParses,TestSeparationNamesATranscriptCarryingBothStampsOfOneRun,TestSeparationIgnoresTwoStampsOfTwoRuns,TestSeparationReportsNoTranscriptCarryingTwoStamps,TestSeparationReportsAnEmptyStoreAsUnobserved,TestSeparationReadsMetadataOnly(the bodies are unreadable under the test and the report is unchanged).internal/surface/cli/scribe_surface_test.go:TestScribeOperandsArePinned(on thereadingOperandsidiom),TestScribeAssembleEchoesTheOperatorsOut,TestScribeIngestRendersOnRefusal.internal/core/lint/scribecontract_test.go: The existing cases over the moved definition,TestScribeInputsMatchTheLedgerDirsandTestScribeDeliveryNamesTheVerb.evals/coldreading_fixture_test.goandevals/smoke_test.go: theEXHAUSTplant and thehistory separationread-only case.
Out of scope
- Any judgement by the verbs about the content transcribed: A state, a ground, an exit condition or a resolution the material does not carry is refused, never supplied.
- The scribe's access rule, which stays the definition's; the verb enforces it and derives its enumeration of the ledger from the schema's constants.
- Reading transcript bodies from the check. The stamp is metadata at capture, and the check stays inside the consumer invariant 15 enumerates.
- The ordering gate and the one-ground rule, which spc-2609020626040342 builds in the functions this verb writes through.
- The durable-tier writer, which spc-2609020626039834 exports.
- The ADR and the invariant amendment, both adopted on 2026-09-02 (adr-2609021016275803,
status: accepted); this spec builds on them and does not restate them.
Frontmatter
| id | spc-2609020626045177 |
|---|---|
| type | spec |
| lifecycle | closed |
.abcd/development/specs/closed/spc-2609020626045177-the-scribe-s-context-is-assembled-and.md
open on GitHub ↗ · commit history ↗
References1
- itd-2609020625402599The scribe's context is assembled and its output is ingested by a verb, and the record can show that no session held both a reading and the ledgerimplements
mentions16
- adr-56An exclusion control asserts only what it can prove
- adr-2609021016275803No session holds both a reading and the ledger, and a per-run context stamp is how the record shows it
- dcn-26092600000000032026-09-26 — Three departures the scribe lane (itd-2609020625402599, spc-2609020626045177) made from its closed spec, which its review found recorded only in code, the chapter or the lane report, recorded here (implementer of lane fix2-scribe, autonomous run A). First, the surface chapter is `04-surfaces/31-scribe.md`, not the `24-scribe.md` the spec names: row 24 is `decide`'s, taken before the lane landed, so the chapter took the next free row. The spec is closed and keeps its text; four open lanes claim row 31 (build, lab, source ledger and this one), so the integration step renumbers three of them. Second, the transcript store's check is `SessionSeparation(repoRoot, rootSHA)`, not the `SessionSeparation(rootSHA)` the spec names, because it reads through `history.List`, which takes the repository root to find a checkout's opt-in per-repo transcript store; the report is unchanged. Third, the scribe's context is assembled from the ledger as it stands in the working tree, uncommitted records included, while the intent's scope condition (cond-2609020626046719) says committed ledger content. The working-tree read is what the code does today and the chapter says so. Whether the condition or the code should move is not decided here: it is captured as iss-2609261056373310, a ruling owed.
- iss-2609020626114155ADR and brief invariant owed: no session holds both a reading and the ledger, and the session-kind stamp changes the reading bundle's shape
- itd-188Machine assistance in maintaining the ledger, without any context that holds both ledger content and a reading
- itd-194The reading include table admits only what the exclusion floor can read
- spc-66The scribe: ledger-only context, authors nothing
- 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-2609020626042168The reading-occasioned origin: promote stamps the run and the item it graduated, and the stamp primitive refuses the kind without them
- spc-2609020626042471The knowledge record becomes a read object: A principle carries typed claims, projects its statement, and inherits only what held
- 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