One journal aggregate per Product (gate:product:<productId>) that binds a Factory-owned store; holds grants, revocations, a stop intent and the one dispatch owner; freezes Slices with their allowances; and admits productions, checks and dispatches as the linearisation point. Beside it, the delivery records module keeps create-once records in the journal and derives an Assessment purely.
Status: Accepted as step 1, increment 2, of the step-1 core (handover), reviewed together with increment 1 (Repository Assurance). Independent implementation review by Astra (gpt-6-astra, high effort): rounds 1–5 FAIL (6, 4, 3, 2 and 1 findings), round 6 PASS on 29 September 2026 (<local evidence archive>). At that review the increment tests passed 73/73 and the full npm run check 428/428, none skipped, on the pre-merge base f5a8991. No receipt exists yet, and this reference has no separate reference-review report. Not composed: the only src/ importers of either module are each other and the Repository collector, which takes the SnapshotManifest type from the delivery records (test "nothing imports a producer, and no other src module consumes the delivery core yet"); the collector (step 1, increment 3) is accepted but not composed, the repository guard seam (increment 3b) is accepted but not composed, and delivery.ts, the delivery CLI and the live Slice (increments 4–8) are not built. Local development only: no delivery, release or customer claim, and all 27 epics remain open. Human page: guide.
- Source:
src/product-gate.ts,src/delivery-records.ts - Tests:
test/product-gate.test.ts(17),test/product-gate-ordering.test.ts(5, real processes),test/delivery-records.test.ts(4),test/delivery-adversarial.test.ts(27),test/delivery-boundaries.test.ts(7, import boundaries of all three core modules and the producer boundary, read from the compiler's syntax tree) - Helpers:
test/helpers/gate.ts,test/helpers/gate-child.ts,test/helpers/adversarial.ts,test/helpers/repository-proof.ts
Intelligence: none — Authority, admission, integration, create-once delivery records and Assessments are pure decisions over durable facts; a model would be a second authority.
What it hides
- One aggregate, one decision. Every gate command is one
CommandJournal.executewhose pure decision re-parses the stored state (parseGateState; a defect isCORRUPT, never repaired), applies its rules in order (the first failure refuses and nothing is recorded), checks the state and transition invariants and the byte budget, and emits one event.atVersioniscontext.version + 1; clocks (now,frozenAt) travel in the payload. No I/O happens in a decision. - Command IDs.
gate:<p>:followed bybind,grant:<g>,revoke:<g>,stop:<n>,resume:<n>,own:<token>,release:<token>,freeze:<s>:<r>,produce:<hex48>,produced:<hex48>,check:<hex48>,dispatch:<id32>,outcome:<id32>:<status or kind>orclose:<s>:<r>.stop:<n>must be the next stop (STALEotherwise) andresume:<n>the current one. A changed payload under an ID isCONFLICT; an exact repeat replays. - Proof re-derivation.
admitDispatch(integrate-local)runsjudgeRepositoryinside the decision over the caller's trusted manifest, base listing and store-read Evidence. The result must canonically equal the supplied Verdict, which must beVerified.trustedandverdictare decision inputs and are never journaled; the payload keeps the prepared intent and its proof digest. - Ledger keys.
delivery:binding:<p>,delivery:slice:<p>/<s>/<r>,delivery:snapshot:<hex>,delivery:verdict:<hex>,delivery:integration:<prepared hex>anddelivery:receipt:<prepared hex>, each written once by commandrecord:<key>at expected version 0. Reads re-validate each record against its key.
Public interface
src/product-gate.ts. Runtime imports: parseFenceIdentity from ./attempt-fence.ts, canonicalJson and JSON_LIMITS from ./canonical-json.ts, parsePreparedIntegration and parseProductBinding from ./delivery-records.ts, the journal class and its errors from ./journal.ts, and acceptanceDigest, environmentDigest, judgeRepository, parseRepositoryVerdict, parseStoreIdentity, recipeDigest and verdictDigest from ./repository-assurance.ts. Types only from ./assurance.ts and ./repository.ts.
- Constants:
GATE_PROTOCOL = "sf-product-gate/1".GATE_LIMITS(frozen):maxSliceIdLength24,maxGrantIdLength32,maxRevision999 999,maxOccurrence99,maxOccurrenceGap16,maxGrantScopeEntries64,maxSliceScopeEntries16,maxTextLength280,maxGrants32,maxSlices64,maxRunsPerSlice20,maxAttemptsPerCheck20,maxStops1 000,maxElapsedMs86 400 000,maxStateBytes786 432. PatternsPRODUCT_ID(1–63 characters),SLICE_ID(1–24),GRANT_ID(1–32),DISPATCH_ID(32 hex). Free texts are 1–280 printable ASCII. class ProductGate(journal: CommandJournal):read(productId): Gate | undefinedvalidates the stored state (CORRUPTon any defect) and reads withreadAggregateonly.bindStore,grant,revoke,requestStop,clearStop,takeOwnership,releaseOwnership,freezeSlice,admitProduction,recordProduction,admitCheck,recordOutcomeandcloseSlicereturnGateResult {gate, replayed}or throwGateError.admitDispatch(input: AdmitDispatchInput): AdmitDispatchResultreturns{admitted: true, dispatch}, a refusal{admitted: false, reason, detail, dispatch: null}, or, for an exact replay,{admitted: false, reason: "REPLAYED", dispatch}: history, never a permit.
- Helpers:
gateAggregateId,sliceRefOf(<s>/<r>),ownerIdOf(delivery/<token>),actionIdOf(integrate-local:<p>:<sliceRef>),productionKeyOfandcheckKeyOf(produce:orcheck:plus the first 48 hex of the SHA-256 of canonical{productId, sliceRef, proposal | candidate}),verificationRunId(ver-<hex48>, the EvidencerunId),storeDigestOf,parseGateState,openDispatch(a dispatch whose outcome is null) andintegrationBlocked(an integrate-local dispatch of any Slice whose outcome is null orunknown). GateErrorCode:INVALID CORRUPT STALE CONFLICT LIMIT NOT_BOUND ALREADY_BOUND NOT_REGISTERED GRANT_EXISTS NO_GRANT REVOKED STOPPED NOT_STOPPED OWNERSHIP_PROOF NOT_OWNER OUT_OF_SCOPE SLICE_EXISTS SLICE_UNKNOWN SLICE_CLOSED DEADLINE ALLOWANCE PRODUCTION_OPEN PRODUCTION_UNKNOWN OUTCOME_FINAL DISPATCH_UNKNOWN DISPATCH_OPEN OCCURRENCE PROOF_MISMATCH CANDIDATE_MISMATCH NOT_VERIFIED.GateError {code, productId, currentVersion}.- Types:
Operation(read | produce | check | integrate-local),ProductBinding,GrantLimits {produceRuns, checkRuns, attemptsPerCheck, integrations, elapsedMs},Grant,StopIntent,DispatchOwner,OwnershipProof(fenced {generation, fenceNonce, alreadyFenced}orreleased {generation}),OwnerRef,SliceEntry,SliceDisposition,Admission,ProductionOutcome,CheckOutcome,IntegrationOutcome,Dispatch(check,integrate-local, or a reservedreviewthat no command creates and stored state refuses),GateState,Gate,GateCommand,GateResult, and one input type per command.
src/delivery-records.ts. Runtime imports: canonicalJson from ./canonical-json.ts, the journal class and its conflict errors from ./journal.ts, and parsers, digests and rejudgeRepository from ./repository-assurance.ts. Types only from ./product-gate.ts, ./repository.ts and ./assurance.ts.
RECORD_LIMITS(frozen):maxNeedLength2 000,maxBriefLength16 384,maxMeasurements16,maxPopulationLength500,maxSnapshotsPerSlice16,maxMetricLength56.class DeliveryLedger(journal):recordBinding/readBinding,recordSlice(returns{sliceDigest, replayed}) /readSlice,recordSnapshotManifest/readSnapshotManifest,recordVerdict(returns the proof digest) /readVerdict,recordPrepared/readPrepared,recordReceipt/readReceipt. An absent record reads asundefined.LedgerErrorcodes:invalid-input,conflict,corruptandmissing(declared; no path throws it today).deriveAssessment(slice, verdict, gathered): Assessment(pure),briefDigest,sliceDigestOf(the gate'ssliceDigest), and the parsers shared with the gate:parseProductBinding,parsePreparedIntegration(digest recomputed from the canonical payload, at most 4096 bytes),parseIntegrationObservation,parseSliceRecordandparseSnapshotManifest.- Types:
SliceNeed,SliceBrief(an optional guidance pin of the same Product),MeasurementSource(evidence {check, quantity: stdoutBytes | passed}ordeclared {value}),SliceRecord,SnapshotManifest,Assessment {measurements, conclusion: supported | inconclusive | rejected},IntegrationRecord,ReceiptRecord,LedgerErrorCode.
Invariants and guarantees
- Common admission rules, in order (
admissible): bound (NOT_BOUND); the Slice exists and is open (SLICE_UNKNOWN,SLICE_CLOSED); its grant is unrevoked (REVOKED); grant and Slice both include the operation (OUT_OF_SCOPE); no stop (STOPPED); the current, unreleased owner (NOT_OWNER);nowis at or before the Slice deadline (DEADLINE). Then per command: a production checks its key is new (CONFLICT), its allowance (ALLOWANCE) and that no production is pending (PRODUCTION_OPEN); a check checks its key, its allowance and that this Slice sealed that Candidate (PRODUCTION_UNKNOWN). Tests: "admissions check authority…", and the A1 rows. - Linearisation with revocation. Revoke and admit are commands on one aggregate. A revoke committed first makes a stale admission
STALEand a current oneREVOKED; an admission committed first stands, and the later revoke succeeds. Four revokers and four admitters racing in real processes give one history, one command per version (product-gate-ordering, A1). - One in flight. At most one production with outcome null and one dispatch with outcome null per Product, whatever the Slice (
PRODUCTION_OPEN,DISPATCH_OPEN). An integrate-local outcome that is null orunknownblocks every later integration of the Product, across closure and new Slices, until it is confirmed. - Check dispatch, in order: bound (
NOT_BOUND); an existing check admission (DISPATCH_UNKNOWN); then the common rules (admissible) on that admission's Slice, soDISPATCH_UNKNOWNprecedes the Slice, revocation, scope, stop, owner and deadline checks; then a new dispatch id (CONFLICT), no open dispatch (DISPATCH_OPEN), no earlier epoch alreadyobserved(OUTCOME_FINAL), and an epoch that is the next one and withinattemptsPerCheck(ALLOWANCE). - Integration admission, after the common rules, in order: a new dispatch id; no open dispatch; the integration allowance; the prepared intent names this Product, store and target ref (
OUT_OF_SCOPE) and this Slice, base and action occurrence (INVALID) and this owner (NOT_OWNER); the occurrence is above the Slice's highest and withinmaxOccurrenceGap(OCCURRENCE); the Verdict parses and digests to the intent's proof (PROOF_MISMATCH); a check admission of this Slice matches the intent's Candidate and commit and the proof's run (CANDIDATE_MISMATCH); that check's dispatch atpins.epochrecordedobservedwith this proof, and the pins are this Product, store, Candidate, base, commit, Slice and its three frozen digests (PROOF_MISMATCH); re-derivation succeeds, equals the Verdict and isVerified(NOT_VERIFIED); the authenticated Candidate was sealed under exactly the Slice's scope, and every edit and every file it adds, changes or removes against the authenticated base listing lies at or below a scope entry, or it is refused when its edits cannot be read (OUT_OF_SCOPE); finally the Product-wide unknown block (DISPATCH_OPEN). The prepared intent's digest is recomputed when the input is parsed. The scope rule came from review round 5; the step-1 spec's §4.1 list does not state it yet. - Ownership by proof only. Generation 1 takes no proof. A later generation needs
fencedwith the current generation and the current owner's fence nonce, orreleasedof the current generation after that owner released (OWNERSHIP_PROOF). Time never passes ownership. The owner's fence must be{runId: productId, epoch: 1, worker: delivery/<token>}, and the owner record is at most 2 KiB. Tests H: eight racers, a dead dispatcher, 6,000 racing takeovers and 70 dead successors, and 6,000 successive idle takeovers, with realcreateFenceandtryFence. - Outcomes. A check goes from null to
observedornot-startedby its admitted sender, or tounresolvedby the current owner of a later generation. An integration goes from null to any outcome, fromunknowntoconfirmed, and from a reconciler'snot-dispatchedtounknownorconfirmed; a sender'snot-dispatchedandconfirmedare final.confirmed.targetmust be the Candidate commit. An outcome from an inspection needs an unrevoked grant that includesread; one from a receipt does not. Outcome recording never checks deadline, stop or closure.recordProductiontakes the admitting or the current owner (only the current owner may abandon), and a sealed Candidate must be on the Slice's base.closeSlicetakes no owner;integratedneeds a confirmed integration of the Slice, and exactlypostcondition-observednames an observation id. - Counters and state.
used.*equals the admissions and dispatches it counts, never decreases and never exceeds the limits. Frozen fields, grants, closures and revocations are set once. Stored state must also be a history the rules could have made: each admission and dispatch under the grant, Slice, stop and owner tenure in force at its version, no integration after anunknown, check epochs1..n, and each integration on an observed check with its proof. State equals the fold of its events (test "the gate state equals the fold of its events"). - Byte budget. Growing commands (bind, grant, freeze and the admissions) refuse with
LIMITabove 768 KiB; recording commands may use the journal's 1 MiB, so a successor settles every obligation from the soft limit (test "byte budget…"). - Product isolation. Keys, IDs and pins carry the Product; Product A cannot integrate with B's Candidate, proof or store (test "two Products in one journal…").
- Ledger. Create-once: identical content replays, and different content is
conflictand changes nothing. A replay or a lost race re-reads the stored record with the same checks as a read, so a corrupt stored copy iscorrupt, never "replayed". A record planted under another key iscorrupton read. Limits and text rules match the gate's on write and on read. - Assessment is Verdict-first.
deriveAssessmentrefuses (TypeError) a Verdict for another Product, Slice, store, base or frozen input. It judges the supplied records again in full (rejudgeRepository) and refuses a Verdict whose judgement of a check covers all of that check's records but disagrees with what they give.FailedgivesrejectedandInconclusivegivesinconclusive. Evidence measurements come only from a Verdict its supplied records reproduce exactly (the first judged record's facts, orpassedas 1 or 0), declared values come as frozen, and anything else isnullwith a reason. AVerifiedVerdict the records do not reproduce isinconclusive; otherwise the conclusion follows the frozen predicates, with any unknown value givinginconclusive. No conclusion is stored.
Failure semantics
- Input defects are
INVALIDbefore any journal work. Rule failures throw their code and record nothing.VersionConflictErrorbecomesSTALE(withcurrentVersion),CommandConflictErrorbecomesCONFLICT("already recorded; re-read"; never re-issue under a new clock), and stored defects areCORRUPT. admitDispatchreturns everyGateErrorexceptCORRUPTas a refusal result (a malformedproductIdstill throwsINVALID). Journal storage errors pass through.- Ledger:
invalid-inputbefore writing, including a record larger than one canonical journal document;conflictfor different content under a key;corruptfor a stored record that fails its parser or key, or a recorded command with no record.
Trust scope
- Established locally (macOS arm64, Node 26.8.1, real SQLite journals and Evidence stores in temporary directories): the rule order, linearisation against revocation in real processes, fence-proved ownership chains, refusal of forged and mutated proofs inside the decision, the scope rule, outcome transitions, separate allowance counters, the byte budget and refusal of corrupt state.
- Import boundaries:
test/delivery-boundaries.test.tsreads imports from the pinned TypeScript 7.0.2's unstable JS API (typescript/unstable/syncto open the project in the compiler's server,typescript/unstable/astto walk each file's syntax tree), not from source text or diagnostics. It visits every import and export declaration with a specifier,import x = require(…),import(…)call andimport('…')type, every triple-slash file reference and every identifier namedrequireormodule; specifiers are the literals' decoded text and the checker resolves each one, so comments, strings, escapes, line terminators and@ts-ignore/@ts-nocheckchange nothing, and the walk must match the imports the compiler itself collected. The core modules' import tables name the export each import takes, and those modules rename no import; only animport typedeclaration counts as type-only, because Node's type stripping keeps every other import declaration (includingimport {},import { type X }and a side-effect import) and so loads its module. Outsidesrc/producer.ts,src/claude-producer.tsandsrc/openai-producer.ts, no file may reach a producer, importnode:module(ormodule, the static source ofcreateRequire), callimport()with anything but one string literal, namerequireormodule, or carry a/// <reference path>ortypesdirective (the compiler adds the named file outside the import list;src/has none); every source file must be in the program as an ES module and every import must resolve. Loaders obtained at run time (for exampleprocess.getBuiltinModule('module')) and paths handed to workers or child processes are not visible to it: it guards against accidental coupling, not deliberately hostile source. The API is unstable, so the test asserts the pinned version. - Not established: any composition (no bind from the Portfolio, no collector, no Git, no delivery). SQLite and Git never commit together, so a revoke can land between the final read and Git's move. The gate cannot see a fence: a
fencedproof is the caller's report oftryFence, checked only against the current generation and fence nonce. Authentication is re-derivation over trusted local files, not cryptography. No compaction, rebind or operator override.
Composition
- Depends on: Command journal, Attempt fence (
parseFenceIdentityonly), Repository Assurance, and Repository types; all unchanged. - Used by: nothing outside the two modules themselves, except that the Repository collector takes the
SnapshotManifesttype from the delivery records. - Intended caller contract (step-1 spec §5; increments 4–5, not built): check the Portfolio registry, create the store, record the complete binding draft (
recordBinding), thenbindStore; take ownership with a fence proof the caller obtained fromtryFence; freeze the Slice, then record the Slice whose digest the freeze names (recordSlice); admit each production and check before its effect; retain the Verdict (recordVerdict) and record the prepared intent (recordPrepared) beforeadmitDispatch; callinvokeOnceonly for an admitted dispatch, passing aguardthat repeats the final read, and record awithheldresult asnot-dispatchedwith its claim; record the sender's receipt (recordReceipt) before its outcome; a later generation reconciles from the receipt first, then from an inspection.
Changing it safely
- Run
node --test test/product-gate.test.ts test/product-gate-ordering.test.ts test/delivery-records.test.ts test/delivery-adversarial.test.ts test/delivery-boundaries.test.ts, thennpm run check. - Any change to a rule's order, a command ID, a key formula or a stored shape changes the recorded history: stored gates must still parse, or add a versioned reader. Keep
trustedandverdictout of the journaled payload and keep re-derivation inside the decision. - Reviewers check: no I/O in a decision; refusals record nothing;
REPLAYEDis never a permit; every new obligation is bounded so that recording fits under the hard limit.
