The names that identify work as it moves through the Factory: work identities, stored events, command and aggregate prefixes, diagnostics channels, recorded times, codes, protocols and deliberate duplicates. It records what src/ stores and publishes, and the rules new code follows so that every lane names things one way. Nothing here grants Authority or feeds a decision: diagnostics and views built on these names are never Evidence.
Status: Current source catalogue; independent source and documentation review passed. The acceptance record records verification; required CI is tracked in the canonical status. The stored-name tables describe current source, including lifecycle scheduling, discovery sources, service access and the bet modules. WorkRef, callId and the delivery diagnostics below remain planned: no delivery composition or work-trace projection exists. A recorded schedule, batch or ask does not prove a live scheduler, a sent message or applied authority.
Source contracts remain in the module references; this page owns only the shared name registry. The catalogue tests compare it with source. Acceptance evidence belongs in the build review.
Work identity
WorkRef
A WorkRef names one unit of work across modules, so records, diagnostics and views can be joined without parsing IDs. It is derived from identities the records already hold. It is never random, never a path, and never an input to an admission or any other decision.
| Form | Unit of work | Derived from | Status |
|---|---|---|---|
slice:<productId>/<sliceId>/<revision> | One frozen Slice of a Product | The gate's productId and sliceRef (<sliceId>/<revision>) | The fields are stored; nothing forms the WorkRef |
run:<state>#<runId> | One fixture Run | <state>, the state digest below, and the Run's runId | The fields are stored; nothing forms the WorkRef |
occurrence:<id> | One lifecycle occurrence | The occurrence ID, such as discovery/<product>/basic/<date> (lifecycle spec §5) | Occurrence IDs exist in lifecycle scheduling; the WorkRef wrapper is planned |
service-use:<productId>/<useId> | One use of an external service | The Product and the service-access use ID | Planned; not in src/ |
- The state digest is the first 12 hex characters of the SHA-256 of the state directory's real path, as UTF-8 bytes. The real path is
realpathSync.nativeof the resolved directory: absolute, with no symbolic link in it. The fixture CLI keeps the journal, the Evidence store, the fences and the workspaces under that path (withStateinlocal-factory.ts). - A relative or linked spelling of one state directory gives one WorkRef. A moved or renamed directory gives its Runs new WorkRefs, and no stored record changes.
- A fixture Run's
runIdis unique only within its state directory, so its WorkRef carries the state digest. - A WorkRef never enters a command ID, an aggregate ID or a payload. New event data carries the fields a WorkRef derives from (for a Slice,
productIdandsliceRef), so a view computes it without parsing an ID. - The fixture WorkRef
run:<state>#<runId>is not Execution's aggregate IDrun:<runId>:#is outside the journal's ID grammar, so the two cannot be confused. - Spans inside a unit of work are identities the records already hold. They are a journal
commandId, a gatedispatchId(32 hex), an Attempt ID (<runId>/<epoch>), an Evidence ref, a prepared intent's digest and a Repository observation ID.
callId
A planned callId identifies one CLI invocation or one HTTP request, for tracing only. It is random per call, for example from crypto.randomUUID(), and appears only in diagnostics messages, the operations log and error bodies (as requestId). It never enters a command ID, payload, event or stored record. The journal replays only an identical command, so a value that differs between two tries of one command would turn its replay into a conflict.
Journal events
A command records its events in its aggregate's history, each with a pending outbox row that nothing delivers (Command journal). An event's type and data are stored history that views read, so a type never changes and a new data shape is a protocol revision.
Naming rule for new events
- A new event type is
<prefix>.<subject>-<past participle>in lower-case kebab words, such asgate.slice-frozenordelivery.verdict-recorded. It fits the journal's ID grammar: 1–128 characters ofA-Z a-z 0-9 . _ : / -, starting with a letter or digit. <prefix>is the module's registered event prefix (Command and aggregate prefixes); no two modules share one.- The subject names what the command recorded, and the past participle what happened to it.
- New event data carries the fields its WorkRef derives from, and
at(Time). - Stored types that predate the rule, such as
run.submitted,attempt.claimedandgate.bound, keep their names, because a rename would rewrite history.
Stored event types
Each table names the emitting function and top-level data fields. verification.reported is retained history only. A record-valued payload follows the linked module contract; the catalogue scanner checks literal fields, not all indirect schemas.
Execution (src/execution.ts), in aggregate run:<runId>:
| Event | Emitted by | Data |
|---|---|---|
run.submitted | submit | runId, pins, budget |
attempt.claimed | claim | runId, attemptId, epoch, worker |
attempt.completed | complete | runId, epoch, evidence |
run.completed | complete, after attempt.completed | runId |
attempt.failed | fail | runId, epoch, reason, evidence |
attempt.interrupted | interrupt; cancel of a running Run | runId, epoch, cleanup |
run.cancel-requested | requestCancel of a running Run | runId, epoch, reason |
run.cancelled | requestCancel of a queued Run; cancelled, after cancel or an interrupt with cancellation pending | runId, reason |
run.exhausted | settle, after a failed or interrupted Attempt that used the Budget's last Attempt | runId, attempts |
Guarded verification (src/local-factory.ts, src/attempt-ledger.ts), in the same fixture journal. The ledger records each event through its private #create, and each record is its aggregate's state.
| Event | Aggregate | Emitted by | Data |
|---|---|---|---|
verification.requested | verification:<runId> | recordRequest, once per Run | The pinned request, which is also the command's payload |
verification.reported | verification:<runId> | Not since 5acdf7c: the Recipe /1 path wrote it (local-factory.ts:397 at 5acdf7c^), and retained /1 journals hold it | attemptId, epoch, result |
attempt.prepared | attempt:<runId>/<token> | recordPreparation | aggregateId |
attempt.reported | report:<runId>/<epoch>/<token> | recordReport | aggregateId |
attempt.fenced | recovery:<runId>/<epoch> | recordFenceProof | aggregateId |
Portfolio (src/portfolio.ts) and Project guidance (src/project-guidance.ts):
| Event | Aggregate | Emitted by | Data |
|---|---|---|---|
portfolio.product-registered | portfolio:local | registerProduct, through decideRegistry | product |
portfolio.product-renamed | portfolio:local | renameProduct, through decideRegistry | productId, displayName |
portfolio.observed | portfolio:product:<productId> or portfolio:usage | plan, inside every import (recordObservations, recordUsage, recordUsageObservations) that records, corrects or retracts an observation | observation, digest, supersedes |
guidance.values-set | guidance:product:<productId> | setValues, through decideSet; the name is the constant EVENT | productId, values, provenance, digest, recordedAt, revision |
Product gate (src/product-gate.ts), in aggregate gate:product:<productId>. The names are the EVENT table, and each command emits exactly one event.
| Event | Command, after gate:<productId>: | Emitted by | Data |
|---|---|---|---|
gate.bound | bind | bindStore | binding, boundBy |
gate.granted | grant:<g> | grant | grant |
gate.revoked | revoke:<g> | revoke | grantId, revoked |
gate.stop-requested | stop:<n> | requestStop | stop, stops |
gate.resumed | resume:<n> | clearStop | stops |
gate.owner-taken | own:<token> | takeOwnership | owner, proof |
gate.owner-released | release:<token> | releaseOwnership | id, generation, released |
gate.slice-frozen | freeze:<s>:<r> | freezeSlice | slice |
gate.production-admitted | produce:<hex48> | admitProduction | admission, used, at (since refactor step R4) |
gate.production-recorded | produced:<hex48> | recordProduction | key, outcome |
gate.check-admitted | check:<hex48> | admitCheck | admission, used, at (since refactor step R4) |
gate.dispatch-admitted | dispatch:<id32> | admitDispatch, through #checkDispatch (check) or #integrationDispatch (integrate-local) | dispatch, used |
gate.outcome-recorded | outcome:<id32>:<status or kind> | recordOutcome | id, outcome |
gate.slice-closed | close:<s>:<r> | closeSlice | sliceRef, closed, observationId |
Delivery records (src/delivery-records.ts): one create-once aggregate per record, written by command record:<aggregate ID> through the private #create. The record itself is the aggregate's state.
| Event | Aggregate | Emitted by | Data |
|---|---|---|---|
delivery.binding-recorded | delivery:binding:<productId> | recordBinding | aggregateId |
delivery.slice-recorded | delivery:slice:<productId>/<sliceId>/<revision> | recordSlice | aggregateId |
delivery.snapshot-recorded | delivery:snapshot:<64 hex> | recordSnapshotManifest | aggregateId |
delivery.verdict-recorded | delivery:verdict:<64 hex> | recordVerdict | aggregateId |
delivery.integration-prepared | delivery:integration:<64 hex> | recordPrepared | aggregateId |
delivery.receipt-recorded | delivery:receipt:<64 hex> | recordReceipt | aggregateId |
branch-health (src/branch-health.ts):
| Event | Emitted by | Data |
|---|---|---|
branch-health.polled | decidePoll | pollId, at, state, changed, snapshotDigest |
branch-health.episodes-opened | decidePoll | productId, pollId, episodes |
branch-health.episodes-failing | decidePoll | productId, pollId, episodes |
branch-health.episodes-closed | decidePoll | productId, pollId, episodes |
branch-health.state-changed | decidePoll | productId, pollId, from, to |
discovery-sources (src/discovery-sources.ts):
| Event | Emitted by | Data |
|---|---|---|
discovery-sources.collection-claimed | #claim | occurrenceId, startedAt, deadline |
discovery-sources.collection-started | #register | occurrenceId, day |
discovery-sources.collection-recorded | #record | occurrenceId, digest |
lifecycle-schedule (src/lifecycle-schedule.ts):
| Event | Emitted by | Data |
|---|---|---|
occurrence.admitted | applyAdmit | occurrenceId, lane, rhythm, date, week, slotAt, segment, runner, startedAt, deadlineAt, coalesced |
occurrence.resumed | applyResume | occurrenceId, segment, runner, startedAt, deadlineAt |
occurrence.settled | applySettle | occurrenceId, status, reason, occurrence |
schedule.commissioned | recordCommission | revision, digest |
schedule.account-recorded | recordAccount | account, sequence, overage, recordedAt |
occurrence.call-admitted | admitCall | occurrenceId, call, task, account, work, attempt, bound, week, segment |
occurrence.call-ended | endCall | occurrenceId, call, outcome, debit, overBound |
schedule.overage-observed | endCall | account, occurrenceId, call, at |
schedule.sign-in-refused | endCall | account, occurrenceId, call, at |
schedule.window-closed | endCall | account, occurrenceId, call, at, until |
occurrence.deferred | defer | occurrenceId, segment, runner, reason, bound, at, resumeAt |
occurrence.reconciled | reconcile | occurrenceId, segment, unknownCalls, confirmation |
owner-digest (src/owner-digest.ts):
| Event | Emitted by | Data |
|---|---|---|
owner-digest.batch-formed | formBatch | slot, slotAt, items, gaps |
owner-digest.digest-formed | formDigest | week, words, readingSeconds, nothingNeedsYou |
owner-digest.note-recorded | recordNote | commandId, week, feeling, outsideAskMinutes |
owner-policy (src/owner-policy.ts):
| Event | Emitted by | Data |
|---|---|---|
owner-policy.channels-recorded | recordChannels | revision, zone, live, channels, recordedBy, recordedAt, version |
owner-policy.ask-recorded | recordAsk | askId, kind, channel, askedAt, subject, productId, effects, minutes, recordedBy, recordedAt, version, answer |
owner-policy.kr1-frozen | recordAsk | askId, askedAt, frozenAt, baseline, evaluation |
owner-policy.ask-answered | answer | askId, answer |
owner-policy.update-recorded | recordUpdate | updateId, kind, channel, sentAt, words, recordedBy, recordedAt, version |
owner-policy.note-recorded | recordNote | week, outsideAskMinutes, feeling, answeredAt, recordedBy, recordedAt, version |
service-access (src/service-access.ts):
| Event | Emitted by | Data |
|---|---|---|
service-access.bound | bindLocation | productId, binding, commissionedBy, atVersion |
service-access.discovered | recordDiscovery | commit, capturedAt, detection, references, findings, tree |
service-access.declared | declareReference | referenceId, location, kind, instance, slot, declaredBy, atVersion |
service-access.confirmed | confirmReference | referenceId, kind, slot, confirmedBy, atVersion |
service-access.route-pinned | declareRoute | kind, instance, purpose, target, declaredBy, atVersion |
attempt.* names two families of events on different aggregates with different data: Execution's claims and settlements, and the ledger's records. Both are stored, so both stay.
Command and aggregate prefixes
Command IDs and aggregate IDs share the journal's ID grammar. Command IDs are journal-wide: once one is recorded, any other use of it in that journal conflicts (CommandConflictError). So each module's prefixes stay distinct, and no prefix, with its separator, begins another module's.
| Journal file | Holds |
|---|---|
<state>/journal.sqlite, one per fixture state directory | Execution Runs, the fixture request and the Attempt ledger |
<local Portfolio journal> | The Portfolio registry and streams, and Project guidance; the core spec puts the gate and the delivery records there too (§9) |
| Lifecycle, discovery sources and bet modules | The caller supplies their journal; these libraries do not select a live path |
| Module | Aggregate IDs | Command IDs | Chosen by | Event prefix |
|---|---|---|---|---|
| Execution (reference) | run:<runId> | Any journal ID the caller gives | The caller: the fixture CLI's verify: IDs | run., attempt. |
| Guarded verification (reference) | verification:<runId> | verify:<runId>: then request, submit, claim:<token>, settle:<epoch>, recover:<epoch> or cancel:<uuid>; retained /1 journals also hold report:<epoch> | Derived from the Run, the epoch and the Attempt's owner token; only cancel: takes a fresh random UUID per request | verification. |
| Attempt ledger (reference) | attempt:<runId>/<token>, report:<runId>/<epoch>/<token>, recovery:<runId>/<epoch> | verify:<runId>: then prepare:<token>, report:<epoch>:<token> or fence-proof:<epoch> | Derived | attempt., shared with Execution |
| Portfolio (reference) | portfolio:local, portfolio:product:<productId>, portfolio:usage | Registration and rename: any journal ID; imports: portfolio-import:<64 hex> | The caller; imports derive theirs from aggregate, version and payload | portfolio. |
| Project guidance (reference) | guidance:product:<productId> | New IDs require guidance-; recorded legacy IDs still replay | The caller | guidance. |
| Product gate (reference) | gate:product:<productId> | gate:<productId>: then one of 14 suffixes | Derived from the command's inputs, with the caller's owner token or dispatch ID | gate. |
| Delivery records (reference) | delivery: then binding:, slice:, snapshot:, verdict:, integration: or receipt: | record:<aggregate ID> | Derived | delivery. |
- Shared-journal collisions. Portfolio registration and Execution still accept caller-chosen journal IDs. A collision with a derived command ID can block that command permanently. Guidance now refuses new non-prefixed IDs inside the decision, preserving exact legacy replay.
- Planned service uses. The
service-use:WorkRef is not an implemented use ledger or an authority grant.
Current additional prefixes
| Module | Aggregate IDs | Command IDs | Event prefixes |
|---|---|---|---|
| Lifecycle schedule | discovery/schedule | discovery/commission/<revision>, discovery/account/<account>/<sequence>; occurrence IDs with /admit, /resume/<segment>, /call/<n>, /call/<n>/end, /defer/<segment>, /reconcile/<segment>, /settle | schedule., occurrence. |
| Discovery sources | discovery-sources:collection:<occurrence>, discovery-sources:claim:<occurrence>, discovery-sources:starts:<product>:<day> | discovery-sources/claim/<occurrence>, discovery-sources/record/<occurrence>, discovery-sources/start/<product>/<day>/<version> | discovery-sources. |
| Service access | service-access:bindings, service-access:product:<product> | service-access:<product>:<verb>:<id>; discovery and route use their next sequence | service-access. |
| Branch health | branch-health:product:<product> | branch-health/poll/<product>/<poll instant> | branch-health. |
| Owner policy | owner-policy:ledger | owner-policy/channels/<revision>, owner-policy/ask/<id>, its /answer, owner-policy/update/<id>, owner-policy/note/<week> | owner-policy. |
| Owner digest | owner-digest:main | owner-digest/batch/<slot>, owner-digest/digest/<week>, owner-digest/note/<caller id> | owner-digest. |
Rule for new prefixes
- A module registers its aggregate prefixes, command-ID prefixes and event prefix on this page in the change that adds its first command. This rule and the event rule apply to the lifecycle and service-access modules from their first increment.
- A command ID is derived from the command's own inputs where it can be, so a retry repeats it exactly. A random part (an owner token, a dispatch ID, a cancellation request) is chosen once and reused for every try of that command.
- A prefix ends with a separator (
:,/or-), and neither it nor any registered prefix begins the other. - The WorkRef forms
slice:,occurrence:andservice-use:are not journal prefixes.
Diagnostics channels
Modules publish diagnostics on node:diagnostics_channel. A diagnostic is never Evidence (core spec G4) and never read by a decision, and the publishing module stores none. publish runs each subscriber synchronously inside the call, which is how fault tests stop, kill or revoke at an exact point.
Rule for new channels and events
- One channel per module, named
sf:<module>. - Event names are
<subject>-<past participle>in lower-case kebab words; the channel already names the module. - A message holds names, identifiers, codes, counts and digests only: never payload text, file content, paths or credentials.
A message is one frozen object with these members.
| Member | When it is present |
|---|---|
event | Always |
productId | The event concerns one Product |
workRef | The event concerns one unit of work |
callId | The call has one |
commandId | A journal command admitted the work |
| Span identities | The event has span fields; each event's row lists them |
Existing channels
These names stay as they are: fault and ordering tests subscribe to them, and renaming an event breaks those tests (as Repository records for its own). Their messages predate the rule: several carry paths, launched carries the sandbox profile text, and sf:producer keys its messages by phase.
| Channel | Constant | Key | Events, then the function that publishes them |
|---|---|---|---|
sf:repository | REPOSITORY_CHANNEL (repository.ts) | event | initial-objects-written (createRepository); objects-written and published (Repository.seal); integration-claimed, integration-withheld, integration-submitted and integration-acknowledged (Repository.invokeOnce) |
sf:confined-executor | EXECUTOR_CHANNEL (confined-executor.ts) | event | counted, materialised, launched and collected (ConfinedExecutor.run); admitted (collect, once the control pipe holds the limit records and the preload's admission record) |
sf:producer | PRODUCER_CHANNEL (producer.ts), published through the contract's publish | phase | prepared and admission (#execute, in claude-producer.ts and openai-producer.ts); spawned, stderr and exited (#exchange, in claude-producer.ts); response (#execute, in openai-producer.ts); finished (settleResult) |
The sf:delivery events
The delivery composition's channel (DELIVERY_CHANNEL = "sf:delivery", core spec §3.5) is not built. Its events are this closed list, published at exactly these points, and fault tests name them in KILL_AT, STOP_AT, REVOKE_AT and STOP_INTENT_AT (core spec §8). A journal command needs no event, because its command-ID prefix already gives fault points before and after it (KILL_BEFORE, KILL_AFTER). An event marks only what a prefix cannot: the point just after an effect outside the journal and before the record that follows it, or a synchronous window before an effect.
These names supersede the core spec's. Its final-read, invoking, guard, prepared and admitted (§5, I7 and I8; §8, rows A2, A3, A5, A6 and R12) become final-read-passed, invocation-started, guard-entered, intent-prepared and integration-admitted. Delivery code and its fault tests use the names in this table.
| Event | Point (core spec §5) | Span fields | Name in the core spec |
|---|---|---|---|
store-created | C2: after createRepository returns, before recordBinding | None | None; the crash point after C2 |
fence-held | O1: after holdFence returns for a slot, before any tryFence or takeOwnership | owner | None; row H kills between O1 and O2 |
predecessor-fenced | O2: after tryFence of the recorded owner returns fenced, before takeOwnership | owner, generation | None |
integration-inspected | R2(b): after inspectIntegration returns, before the one permitted outcome command, if any | dispatchId, intent, observationId, invocation | None; row R13's repeated inspections |
snapshot-stored | F8: after one SnapshotStore.put returns, before recordSnapshotManifest | snapshot | None; the crash point mid-F8 |
candidate-sealed | P2: after seal returns a Candidate, before recordProduction (P3) | commandId, candidate | None; the crash point between P1 and P3 |
collection-started | V5: after the re-export before collection, immediately before collect | commandId, dispatchId, epoch | None |
evidence-collected | V5: after collect returns and the Candidate is re-exported, before judgement (V6) | commandId, dispatchId, epoch | None; the crash point between V3 and V8 |
verdict-issued | V6: after judgeRepository returns a Verdict, before recordVerdict (V7) | commandId, dispatchId, proof, result | verdict-issued (row R12) |
intent-prepared | I4: after prepareIntegration returns, before recordPrepared (I5) | intent, occurrence | prepared (row R12) |
integration-admitted | I6: after the integrate-local dispatch is admitted, before the final read (I7); never on REPLAYED or a refusal | commandId, dispatchId, intent, occurrence | admitted (rows R12, A2 and A5) |
final-read-passed | I7: after the final read passes, synchronously, before invocation-started | commandId, dispatchId | final-read (I7, row A3) |
invocation-started | I8: immediately before invokeOnce | commandId, dispatchId, intent | invoking (I8; the A2, A3 and A6 row) |
guard-entered | I8: on entry to the guard, after Repository's claim and before the guard's own read | commandId, dispatchId, intent, claim | guard (I8; the A2, A3 and A6 row) |
invocation-returned | I8: after invokeOnce returns an observation, before recordReceipt or recordOutcome (I9) | commandId, dispatchId, intent, observationId, invocation | None |
- Every message carries
eventandproductId, andcallIdwhen the call has one. It carriesworkReffor every event except the first three, which concern the Product and no Slice. commandId, where listed, is the gate command that admitted the work: the production admission forcandidate-sealed, and the dispatch for the others.- When
invokeOnceclaims the intent, the two channels interleave in this order:integration-admitted,final-read-passed,invocation-started,integration-claimed,guard-entered, thenintegration-withheld, orintegration-submittedandintegration-acknowledged, and lastinvocation-returned.
Time
- A new command's payload carries
at: one ISO 8601 UTC instant with milliseconds, such as2026-10-02T09:00:00.000Z(the gate'sTIMESTAMPform), which the caller reads once. Its event repeats it. A re-issued command with another clock value is another payload, so it conflicts and is re-read, never retried (core spec §1). - The journal records no commit time and is not migrated to add one. Where no record holds a time, a view says "time not recorded" and never interpolates one.
| Record | Time it holds | Whose clock |
|---|---|---|
Evidence metadata (sf-evidence/1) | collectedAt | The collector's |
| Repository collector facts, inside the Evidence bytes | startedAt, endedAt and deadline | The collector's, around ConfinedExecutor.run |
| Fixture worker report | durationMs, a duration only | The monotonic clock |
Repository IntegrationObservation | observation.at | The host's, when the observation completed |
| Gate binding | checkout.capturedAt | The caller's |
| Gate Slice entry | frozenAt, and deadline derived from it | The payload's frozenAt |
| Gate dispatch | admitted.at | The payload's now |
| Gate integration outcome read from an observation | at | The observation's |
| Gate production and check admissions | at in the admission's event, repeating the payload's now (since refactor step R4); none in state. Events recorded before R4 carry no at | The payload's now |
| Delivery records | frozenAt in the Slice record; recordedAt in integration and receipt records | The caller's |
| Guidance revision | recordedAt, in state, result and event but not in the payload | The service's, read once before the transaction, so an identical save still replays |
| Lifecycle schedule | Commission/account times, occurrence and segment times; event fields vary as catalogued above | Injected clock or supplied commission |
| Discovery collection and capture | Collection start/end, capture fetch times and claim deadline | Collector clock |
| Branch health | Poll at/pollId, observation time and episode times | Poll caller; provider timestamps remain separately attributed |
| Owner policy | recordedAt plus supplied askedAt, answeredAt or sentAt | Ledger clock and attributed caller time, kept distinct |
| Owner digest | Batch slotAt, digest issue time and note recordedAt | Injected clock; calendar slot is distinct from recording time |
| Service access | Discovery capturedAt; other transitions have atVersion only | Discovery input; no commit time invented |
| Portfolio observation | observedAt (null when unknown) and capturedAt | The source's and the collector's |
| Execution Runs and Attempts, ledger records, the fixture request, Portfolio registrations and the other gate records | None | Not recorded |
Codes
Rule for new results
- A new result carries
code, one word of a closed union of lower-case kebab words, anddetail, 1–280 printable ASCII characters. That is the core spec's text rule: longer text is cut with a trailing..., and any other character becomes?. - Stored records discriminate on
kind; operation results discriminate onstatus. - A refusal is a returned result or a thrown error, never a journal record: refusals record nothing.
- A pre-effect seam's refusal reason (the collector's
CollectGate, Repository'sIntegrationGuard) is<code>: <detail>, cut to the guard's 1–256 printable ASCII characters. - The first result in this form is the gate's
currentCheck(product-gate.ts, refactor step R4), whose codes areCurrentCheckCode;seamAnswer(product-gate.ts) gives its refusal to either seam as that reason. admitDispatchanswers an exact replay withreason: "REPLAYED": history, never a refusal and never a permit.- Existing codes keep their spelling, because callers and tests read them. Composition roots map them to nine families through exhaustive
satisfiestables.
| Family | Meaning |
|---|---|
invalid | The request is malformed or breaks a rule it could have known; nothing was done |
not-found | A named record, Run, Product or file does not exist |
stale | The request names a version, generation or Attempt that a later commit replaced; re-read before deciding again |
conflict | The ID or key is already recorded with other content, or the thing already exists |
limit | A bound, allowance or deadline refuses the request |
corrupt | Stored data failed validation; nothing repairs it silently |
unavailable | A store, process, lock or host feature could not be used, and nothing was started |
unknown-effect | An effect may have happened; its outcome is unknown and permits no blind retry |
internal | A defect in the Factory's own code, or a broken invariant |
A composition root's table for the Evidence store's codes shows the shape. It is a sketch, since no such table exists in src/, and each of these codes has one meaning in evidence.ts.
type CodeFamily = "invalid" | "not-found" | "stale" | "conflict" | "limit" | "corrupt" | "unavailable" | "unknown-effect" | "internal";
const EVIDENCE_FAMILY = {
"invalid-input": "invalid",
"invalid-id": "invalid",
missing: "not-found", // "evidence <id> does not exist"
corrupt: "corrupt",
conflict: "conflict", // "already exists and is not identical"
} as const satisfies Record<EvidenceErrorCode, CodeFamily>;
A missing code, an extra key or a code added to EvidenceErrorCode later fails the typecheck, so the table stays exhaustive. Some codes have more than one meaning: Portfolio's CONFLICT, for one, also reports a stream that kept changing during an import. The reviewed table that first needs such a code maps it, not this page.
No family names a refusal by Authority: the gate's REVOKED, STOPPED, NOT_OWNER, OUT_OF_SCOPE and OWNERSHIP_PROOF, the core spec's DeliveryError denied and the Dashboard's FORBIDDEN. The first composition root that maps them decides their family, in its reviewed table.
Existing vocabularies
| Module | Type, file | Codes | Form |
|---|---|---|---|
| Command journal | JournalErrorCode, errors.ts | INVALID_INPUT COMMAND_CONFLICT VERSION_CONFLICT DECISION_FAILED UNSUPPORTED_SCHEMA STORAGE CLOSED NOT_FOUND READ_ONLY | Upper case |
| Execution | ExecutionErrorCode, execution.ts | RUN_NOT_FOUND RUN_EXISTS RUN_TERMINAL ATTEMPT_ACTIVE STALE_ATTEMPT CANCEL_PENDING CORRUPT_RUN | Upper case |
| Evidence | EvidenceErrorCode, evidence.ts | invalid-input invalid-id missing corrupt conflict | Kebab |
| Attempt fence | FenceErrorCode, attempt-fence.ts | invalid-input exists in-use unavailable storage released | Kebab |
| Attempt workspace | WorkspaceErrorCode, attempt-workspace.ts | invalid-input exists unavailable not-held | Kebab |
| Attempt ledger | LedgerError, attempt-ledger.ts | None: a message only | None |
| Fixture CLI | FactoryErrorCode, factory-support.ts | invalid-input conflict not-found | Kebab |
| Confined executor | ExecutorErrorCode, confined-executor.ts | unsupported-host runtime-unavailable | Kebab |
| Repository | RepositoryErrorCode, repository-protocol.ts (re-exported by repository.ts) | invalid-input rejected exists unavailable missing conflict corrupt git-failed | Kebab |
| Repository collector | SnapshotError, repository-collector.ts | missing corrupt conflict unavailable | Kebab |
| Portfolio | PortfolioErrorCode, portfolio.ts | INVALID CONFLICT NOT_FOUND LIMIT UNAVAILABLE CORRUPT | Upper case |
| Project guidance | GuidanceErrorCode, project-guidance.ts | INVALID NOT_FOUND STALE CONFLICT LIMIT CORRUPT | Upper case |
| Dashboard | ApiErrorCode, dashboard-contract.ts | BAD_REQUEST FORBIDDEN NOT_FOUND METHOD_NOT_ALLOWED UNSUPPORTED_MEDIA_TYPE PAYLOAD_TOO_LARGE CONFLICT STALE BUSY JOURNAL_UNAVAILABLE CORRUPT UNAVAILABLE INTERNAL | Upper case |
| Product gate | GateErrorCode, product-gate.ts | 30 codes, listed in its public interface | Upper case |
| Product gate, current check (a result rather than an error) | CurrentCheckCode, product-gate.ts | not-bound dispatch-unknown dispatch-settled slice-closed revoked stopped not-owner deadline | Kebab |
| Delivery records | LedgerErrorCode, delivery-records.ts | invalid-input conflict corrupt missing | Kebab |
| Producers | ProducerErrorCode, producer.ts | invalid-input invariant | Kebab |
| Intelligence | RefusalCode, intelligence.ts, a result rather than an error | unknown-task blocked not-specified not-measured escalation-undeclared escalation-mismatch pin-mismatch policy-mismatch not-qualified commission-mismatch implicit-retries producer-unknown not-independent | Kebab |
| branch-health | BranchHealthErrorCode, branch-health.ts | INVALID CONFLICT CORRUPT LIMIT | Upper case |
| discovery-sources | DiscoveryErrorCode, discovery-sources.ts | INVALID CONFLICT CORRUPT CACHE_UNAVAILABLE CACHE_CORRUPT | Upper case |
| lifecycle-schedule | ScheduleErrorCode, lifecycle-schedule.ts | INVALID CORRUPT CONFLICT CONTENDED LIMIT NOT_COMMISSIONED REVISION NOT_A_LANE UNKNOWN_OCCURRENCE UNKNOWN_CALL NOT_RUNNING NOT_OWNER CALL_OPEN SETTLED STALE RULE | Upper case |
| owner-digest | DigestErrorCode, owner-digest.ts | INVALID CONFLICT CORRUPT LIMIT | Upper case |
| owner-policy | AskLedgerErrorCode, owner-policy.ts | INVALID CORRUPT CONFLICT CONTENDED LIMIT NOT_OPENED REVISION UNKNOWN_ASK RULE | Upper case |
| service-kinds | AccessErrorCode, service-kinds.ts | INVALID CORRUPT STALE CONFLICT LIMIT TAKEN EXISTS OUT_OF_BINDING NOT_FOUND CLASS ROUTE KIND | Upper case |
Assurance, Repository Assurance and the fixture worker throw a plain TypeError for malformed input. A producer's DispatchGate refuses with GateRejection, whose code is any 1–32 characters of A-Z 0-9 _, not a closed union. One meaning has several words: not found is NOT_FOUND, not-found, missing or RUN_NOT_FOUND, and stale is STALE, VERSION_CONFLICT or STALE_ATTEMPT.
Protocols and frozen names
A stored or pinned shape changes through an explicit revision and compatibility contract, never by silently rewriting history (Factory model, invariant 9). A module may refuse an unsupported old revision; the protocol table records those boundaries. Frozen: every event type, command ID, aggregate prefix and protocol identifier on this page, the sf:repository event names, and the wording of Verdicts.
| Module | Protocol identifiers |
|---|---|
| Evidence / Assurance | sf-evidence/1: the record envelope |
| Local fixture worker, guarded verification | sf-local-fixture-worker/1 and sf-guarded-fixture-worker/1 (collectors); sf-guarded-fixture/1 (execution manifest); sf-fixture-gate/1 (the fixture gate's control-pipe records and its start callback's symbol); sf-local-fixture-verify/1 and sf-local-fixture-verify/2 (Recipe revisions; /1 Runs are read and settled, never dispatched); sf-local-fixture/1 and sf-local-fixture/2 (Candidate kinds); local-fixture-scenario/<digest> (scenario revision) |
| Attempt fence and workspace | sf-attempt-fence/1, sf-attempt-workspace/1 |
| Confined executor | sf-confined-node-stateless/1 (profile), sf-confined-runtime/1, sf-confined-snapshot/1, and sf-confined/1 (the preload's control records) |
| Repository | sf-repository/1, sf-candidate/1, sf-integration/1 |
| Repository Assurance and collector | sf-repository-acceptance/1, sf-repository-verify/1, sf-confined-environment/1, sf-repository-scenario-collector/1, sf-repository-observation/1, sf-repository-scenario/1/<hex> |
| Product gate and delivery records | sf-product-gate/1, sf-product-binding/1, sf-slice/1, sf-delivery-snapshot/1 |
| Portfolio | portfolio-registry/1, portfolio-stream/1, usage-receipt/1 |
| Project guidance | product-guidance/1 (the digest), product-guidance-state/1 |
| Dashboard | factory-dashboard/3, served over HTTP and never stored |
| Intelligence | sf-intelligence/1; task protocols sf-producer-prompt/1, sf-review-prompt/1 and sf-review-observation/1 |
| Producers | sf-producer-request/1, sf-producer-receipt/1, sf-producer-dispatch/1, sf-producer-consumption/1, sf-producer-commission/1, sf-producer-prompt/1, sf-claude-transport-policy/1, sf-openai-transport-policy/1, sf-openai-request/1 |
| Reserved | sf-factory-inspect/1: the fixture inspect document, which carries no protocol member; nothing emits the identifier |
| Lifecycle schedule | sf-lifecycle-schedule/1, sf-schedule-commission/1 |
| Discovery sources | sf-research-brief/1, sf-capture/1, sf-collection/1, sf-collection-claim/1, sf-collection-starts/1, sf-citation/1, sf-drift/1 |
| Service access | sf-service-access/1, sf-service-access-bindings/1, sf-service-catalogue/1, sf-service-map/1, sf-tree-scan/1 |
| Service catalogue route protocols | github-actions/1, app-store-connect/1, apple-signing/1, google-play/1, android-signing/1 (declarations, not proof of an implemented broker) |
| Branch health | sf-branch-health/2; earlier states are refused, never migrated |
| Owner policy | sf-ask-ledger/1 |
| Owner digest | sf-owner-digest/2, sf-digest-commission/1; earlier digest states are refused |
- Verdict wording. A Verdict's reasons are free text inside it, and they are protocol. Three checks compare the canonical JSON of a whole Verdict: the fixture
recheckinguarded-verification.ts, the stored-report checkobservationProbleminfactory-support.ts, and the gate's re-derivation in#integrationDispatchinproduct-gate.ts. A reworded reason would therefore make every stored fixture Verdict invalid and every retained repository proofNOT_VERIFIED. A mapping from a judgement to{code, detail}for display is never stored; refactor step R6 plans it (reasonOf). - Pinned closures. The seven files of
GUARDED_FIXTURE_SOURCESinfixture-manifest.tsare byte-frozen: a change makes every completed/2Run present as invalidated. The eight files ofCOLLECTOR_SOURCESinrepository-collector.tsare digested into every repository recipe: a change invalidates every repository Verdict made under the old digest. Both lists holdevidence.ts. Since refactor step R3 the collector's list holds Repository's wire grammar,repository-protocol.ts, in place ofrepository.ts: a change to Repository's Git, sealing or integration code no longer changes the digest, and a change to the grammar does. sf-evidence/1takes no new record kind. A new kind, such as the lifecycle's Citation (sf-citation/1), is a format of its own, stored outside both closures.- The fixture
inspectdocument is/1by default.FactoryResultinfactory-support.ts, plus the CLI'sexitCode, has noprotocolmember. A reader, such as step 1's run-briefing tool (increment 8), reads a document without one assf-factory-inspect/1and refuses anyprotocolit does not know. A later/2names itself in that member.
Vocabulary
Three terms of the delivery core map to the Factory's one language as follows.
| Term in code | What it is | Factory term |
|---|---|---|
| Allowance | The gate's GrantLimits and SliceEntry.used: production runs, check runs, Attempts per check, integrations and elapsed time | The Slice's Budget. Execution's Budget is per Run |
generation | The gate owner's ownership version: 1 for the first owner, and one higher at each later takeOwnership | The Product-scope epoch (Workers). Execution's epoch is per Run and Attempt |
| Check dispatch | A gate dispatch of kind check, epoch 1 to attemptsPerCheck, with Evidence runId ver-<hex48> and attemptId <runId>/<epoch> | An Attempt of the gate's check Run, not an Execution aggregate. The design defines one Run, so whether delivery Runs become Execution Runs is an open owner decision |
Some exported names mean different things in different files.
| Name | Senses |
|---|---|
CheckOutcome | A check's judgement word (assurance.ts); a check dispatch's outcome (product-gate.ts) |
Admission | A producer's dispatch answer (producer.ts); a gate production or check admission (product-gate.ts) |
LedgerError | The Attempt ledger's error (attempt-ledger.ts); the delivery records' error (delivery-records.ts) |
TokenCounts | Portfolio usage tokens (portfolio-inventory.ts); provider tokens (producer.ts) |
Outcome | The fixture CLI's result word (factory-support.ts); a producer's outcome (producer.ts); neither is the domain Outcome |
Decision | A journal decision (journal.ts); a module's model involvement (intelligence.ts) |
Registry | The Portfolio registry (portfolio.ts); the Intelligence registry (intelligence.ts) |
RepositoryIdentity | A Factory-owned store (repository-protocol.ts, re-exported by repository.ts); a registered checkout (portfolio-inventory.ts) |
MAX_TIMEOUT_MS | 60,000 for a fixture process (local-worker.ts); 300,000 for the executor (confined-executor.ts) |
A file that needs both senses imports one under an alias. A rename waits for the file's next reviewed change, because every one of these files belongs to an accepted module.
Deliberate duplicates
Some definitions exist twice on purpose. A copy stays while its file is frozen, pinned by a receipt, or kept out of another module's import graph. Where a test holds a mirror equal, the table names it. The delivery records' own prepared-intent parser is gone (refactor step R3): the records and the gate parse a prepared intent with parsePreparedIntegration in repository-protocol.ts. The gate's and the records' validator kits, grammars and limit mirrors are gone too (refactor step R4): both take them from strict-input.ts, and the gate re-exports that file's GATE_LIMITS and exports RegExp copies of its PRODUCT_ID, SLICE_ID and GRANT_ID grammars, made from their sources, which neither module validates with. Two of the records' rules still differ from Repository's wire grammar: a Slice revision is bounded by GATE_LIMITS.maxRevision (999 999), where repository-protocol.ts takes any safe integer, and the records' HEAD_REF, still used for observations, is wider than its target-ref rule.
| Duplicate | Where | Why it stays | Held equal by |
|---|---|---|---|
The sf-evidence/1 envelope, which gives a record its identity | evidence.ts (private); EVIDENCE_FORMAT in repository-assurance.ts | evidence.ts is in both closures and exports no envelope function | "the envelope digest the judge recomputes is evidence.ts's own record identity" |
Process classification: classify and classifyObservation | classify in assurance.ts (private); classifyObservation in repository-assurance.ts | assurance.ts is receipt-pinned and exports no classifier | "classifyObservation is exactly the fixture classify over every exit, signal, timeout and collector error" |
The snapshot listing digest: snapshotDigest and listingDigest | snapshotDigest in confined-executor.ts; listingDigest in repository-assurance.ts | Repository Assurance imports only SNAPSHOT_PROTOCOL from the executor | "listingDigest equals the executor snapshotDigest on 200 random listings and refuses what the executor refuses" |
| The executor's limits and profile name | confined-executor.ts; repository-assurance.ts | The same import rule | "the module repeats the executor limits it may not import, and imports no I/O" |
| Canonical JSON | canonicalJson in canonical-json.ts; evidence.ts (private) | The fixture closure stays closed: canonical-json.ts is not in it | The fixture closure pins evidence.ts byte for byte |
PRODUCT_ID | portfolio.ts, project-guidance.ts, dashboard-server.ts, repository-assurance.ts, and strict-input.ts, whose grammar the gate and the records validate with (refactor step R4; the gate's exported PRODUCT_ID is a RegExp made from it) | The Portfolio receipt pins portfolio.ts, and the project-guidance receipt's evidence pins all three; the collector closure holds repository-assurance.ts | Identical patterns; no test |
| Repository's wider product ID | createRepository accepts any journal-grammar ID (DOMAIN_ID in repository-protocol.ts) | Repository's receipts pin the grammar, which is also in the collector's closure, and a binding needs the store's product ID to be the Product's, which PRODUCT_ID bounds, so the gate never binds a wider one | Documented, not narrowed |
The local-fixture-scenario/<digest> formula | local-factory.ts (/1); guarded-verification.ts (/2) | Retained journals hold /1 Runs, and only the owner retires that path | No test |
Changing it safely
What must stay true
- A change that adds an event type, command or aggregate prefix, diagnostics channel or event, protocol identifier or error code updates this page in the same change. A stored name never changes: its tables here grow, and no row is rewritten.
- A new event's data fields, and any field added to a stored event's data, are listed here in the same change, because a new
datashape is a protocol revision. - A change to the
sf:deliverylist comes before the delivery code that publishes it, with the fault tests that name it. - The tables cite current files and symbols. A renamed emitting or publishing function, or a moved constant, fails the catalogue test until this page names the new one.
Tests and helpers
Run the catalogue and documentation tests, then rebuild the site. The catalogue checks stored event names and literal data fields, diagnostics names and keys, protocol identifiers, error-code unions and cited symbols. The documentation map classifies this cross-module reference; it has no matching topic guide.
Limits: the source scanner recognises { type, data } drafts in that order, literal or resolved constant/parameter types, and literal diagnostics event or phase names. An encountered unresolved form fails. Indirect payload schemas, command-ID derivation, clock provenance and duplicate meanings require review. Source-text coverage is not a journal migration, a live-data audit or runtime proof. Delivery events above are a reserved contract, not observed effects.
