Factory docs, home
Page navigation

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.

FormUnit of workDerived fromStatus
slice:<productId>/<sliceId>/<revision>One frozen Slice of a ProductThe 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 runIdThe fields are stored; nothing forms the WorkRef
occurrence:<id>One lifecycle occurrenceThe 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 serviceThe Product and the service-access use IDPlanned; 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.native of 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 (withState in local-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 runId is 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, productId and sliceRef), so a view computes it without parsing an ID.
  • The fixture WorkRef run:<state>#<runId> is not Execution's aggregate ID run:<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 gate dispatchId (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 as gate.slice-frozen or delivery.verdict-recorded. It fits the journal's ID grammar: 1–128 characters of A-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.claimed and gate.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>:

EventEmitted byData
run.submittedsubmitrunId, pins, budget
attempt.claimedclaimrunId, attemptId, epoch, worker
attempt.completedcompleterunId, epoch, evidence
run.completedcomplete, after attempt.completedrunId
attempt.failedfailrunId, epoch, reason, evidence
attempt.interruptedinterrupt; cancel of a running RunrunId, epoch, cleanup
run.cancel-requestedrequestCancel of a running RunrunId, epoch, reason
run.cancelledrequestCancel of a queued Run; cancelled, after cancel or an interrupt with cancellation pendingrunId, reason
run.exhaustedsettle, after a failed or interrupted Attempt that used the Budget's last AttemptrunId, 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.

EventAggregateEmitted byData
verification.requestedverification:<runId>recordRequest, once per RunThe pinned request, which is also the command's payload
verification.reportedverification:<runId>Not since 5acdf7c: the Recipe /1 path wrote it (local-factory.ts:397 at 5acdf7c^), and retained /1 journals hold itattemptId, epoch, result
attempt.preparedattempt:<runId>/<token>recordPreparationaggregateId
attempt.reportedreport:<runId>/<epoch>/<token>recordReportaggregateId
attempt.fencedrecovery:<runId>/<epoch>recordFenceProofaggregateId

Portfolio (src/portfolio.ts) and Project guidance (src/project-guidance.ts):

EventAggregateEmitted byData
portfolio.product-registeredportfolio:localregisterProduct, through decideRegistryproduct
portfolio.product-renamedportfolio:localrenameProduct, through decideRegistryproductId, displayName
portfolio.observedportfolio:product:<productId> or portfolio:usageplan, inside every import (recordObservations, recordUsage, recordUsageObservations) that records, corrects or retracts an observationobservation, digest, supersedes
guidance.values-setguidance:product:<productId>setValues, through decideSet; the name is the constant EVENTproductId, 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.

EventCommand, after gate:<productId>:Emitted byData
gate.boundbindbindStorebinding, boundBy
gate.grantedgrant:<g>grantgrant
gate.revokedrevoke:<g>revokegrantId, revoked
gate.stop-requestedstop:<n>requestStopstop, stops
gate.resumedresume:<n>clearStopstops
gate.owner-takenown:<token>takeOwnershipowner, proof
gate.owner-releasedrelease:<token>releaseOwnershipid, generation, released
gate.slice-frozenfreeze:<s>:<r>freezeSliceslice
gate.production-admittedproduce:<hex48>admitProductionadmission, used, at (since refactor step R4)
gate.production-recordedproduced:<hex48>recordProductionkey, outcome
gate.check-admittedcheck:<hex48>admitCheckadmission, used, at (since refactor step R4)
gate.dispatch-admitteddispatch:<id32>admitDispatch, through #checkDispatch (check) or #integrationDispatch (integrate-local)dispatch, used
gate.outcome-recordedoutcome:<id32>:<status or kind>recordOutcomeid, outcome
gate.slice-closedclose:<s>:<r>closeSlicesliceRef, 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.

EventAggregateEmitted byData
delivery.binding-recordeddelivery:binding:<productId>recordBindingaggregateId
delivery.slice-recordeddelivery:slice:<productId>/<sliceId>/<revision>recordSliceaggregateId
delivery.snapshot-recordeddelivery:snapshot:<64 hex>recordSnapshotManifestaggregateId
delivery.verdict-recordeddelivery:verdict:<64 hex>recordVerdictaggregateId
delivery.integration-prepareddelivery:integration:<64 hex>recordPreparedaggregateId
delivery.receipt-recordeddelivery:receipt:<64 hex>recordReceiptaggregateId

branch-health (src/branch-health.ts):

EventEmitted byData
branch-health.polleddecidePollpollId, at, state, changed, snapshotDigest
branch-health.episodes-openeddecidePollproductId, pollId, episodes
branch-health.episodes-failingdecidePollproductId, pollId, episodes
branch-health.episodes-closeddecidePollproductId, pollId, episodes
branch-health.state-changeddecidePollproductId, pollId, from, to

discovery-sources (src/discovery-sources.ts):

EventEmitted byData
discovery-sources.collection-claimed#claimoccurrenceId, startedAt, deadline
discovery-sources.collection-started#registeroccurrenceId, day
discovery-sources.collection-recorded#recordoccurrenceId, digest

lifecycle-schedule (src/lifecycle-schedule.ts):

EventEmitted byData
occurrence.admittedapplyAdmitoccurrenceId, lane, rhythm, date, week, slotAt, segment, runner, startedAt, deadlineAt, coalesced
occurrence.resumedapplyResumeoccurrenceId, segment, runner, startedAt, deadlineAt
occurrence.settledapplySettleoccurrenceId, status, reason, occurrence
schedule.commissionedrecordCommissionrevision, digest
schedule.account-recordedrecordAccountaccount, sequence, overage, recordedAt
occurrence.call-admittedadmitCalloccurrenceId, call, task, account, work, attempt, bound, week, segment
occurrence.call-endedendCalloccurrenceId, call, outcome, debit, overBound
schedule.overage-observedendCallaccount, occurrenceId, call, at
schedule.sign-in-refusedendCallaccount, occurrenceId, call, at
schedule.window-closedendCallaccount, occurrenceId, call, at, until
occurrence.deferreddeferoccurrenceId, segment, runner, reason, bound, at, resumeAt
occurrence.reconciledreconcileoccurrenceId, segment, unknownCalls, confirmation

owner-digest (src/owner-digest.ts):

EventEmitted byData
owner-digest.batch-formedformBatchslot, slotAt, items, gaps
owner-digest.digest-formedformDigestweek, words, readingSeconds, nothingNeedsYou
owner-digest.note-recordedrecordNotecommandId, week, feeling, outsideAskMinutes

owner-policy (src/owner-policy.ts):

EventEmitted byData
owner-policy.channels-recordedrecordChannelsrevision, zone, live, channels, recordedBy, recordedAt, version
owner-policy.ask-recordedrecordAskaskId, kind, channel, askedAt, subject, productId, effects, minutes, recordedBy, recordedAt, version, answer
owner-policy.kr1-frozenrecordAskaskId, askedAt, frozenAt, baseline, evaluation
owner-policy.ask-answeredansweraskId, answer
owner-policy.update-recordedrecordUpdateupdateId, kind, channel, sentAt, words, recordedBy, recordedAt, version
owner-policy.note-recordedrecordNoteweek, outsideAskMinutes, feeling, answeredAt, recordedBy, recordedAt, version

service-access (src/service-access.ts):

EventEmitted byData
service-access.boundbindLocationproductId, binding, commissionedBy, atVersion
service-access.discoveredrecordDiscoverycommit, capturedAt, detection, references, findings, tree
service-access.declareddeclareReferencereferenceId, location, kind, instance, slot, declaredBy, atVersion
service-access.confirmedconfirmReferencereferenceId, kind, slot, confirmedBy, atVersion
service-access.route-pinneddeclareRoutekind, 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 fileHolds
<state>/journal.sqlite, one per fixture state directoryExecution 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 modulesThe caller supplies their journal; these libraries do not select a live path
ModuleAggregate IDsCommand IDsChosen byEvent prefix
Execution (reference)run:<runId>Any journal ID the caller givesThe caller: the fixture CLI's verify: IDsrun., 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 requestverification.
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>Derivedattempt., shared with Execution
Portfolio (reference)portfolio:local, portfolio:product:<productId>, portfolio:usageRegistration and rename: any journal ID; imports: portfolio-import:<64 hex>The caller; imports derive theirs from aggregate, version and payloadportfolio.
Project guidance (reference)guidance:product:<productId>New IDs require guidance-; recorded legacy IDs still replayThe callerguidance.
Product gate (reference)gate:product:<productId>gate:<productId>: then one of 14 suffixesDerived from the command's inputs, with the caller's owner token or dispatch IDgate.
Delivery records (reference)delivery: then binding:, slice:, snapshot:, verdict:, integration: or receipt:record:<aggregate ID>Deriveddelivery.
  • 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

ModuleAggregate IDsCommand IDsEvent prefixes
Lifecycle schedulediscovery/schedulediscovery/commission/<revision>, discovery/account/<account>/<sequence>; occurrence IDs with /admit, /resume/<segment>, /call/<n>, /call/<n>/end, /defer/<segment>, /reconcile/<segment>, /settleschedule., occurrence.
Discovery sourcesdiscovery-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 accessservice-access:bindings, service-access:product:<product>service-access:<product>:<verb>:<id>; discovery and route use their next sequenceservice-access.
Branch healthbranch-health:product:<product>branch-health/poll/<product>/<poll instant>branch-health.
Owner policyowner-policy:ledgerowner-policy/channels/<revision>, owner-policy/ask/<id>, its /answer, owner-policy/update/<id>, owner-policy/note/<week>owner-policy.
Owner digestowner-digest:mainowner-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: and service-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.

MemberWhen it is present
eventAlways
productIdThe event concerns one Product
workRefThe event concerns one unit of work
callIdThe call has one
commandIdA journal command admitted the work
Span identitiesThe 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.

ChannelConstantKeyEvents, then the function that publishes them
sf:repositoryREPOSITORY_CHANNEL (repository.ts)eventinitial-objects-written (createRepository); objects-written and published (Repository.seal); integration-claimed, integration-withheld, integration-submitted and integration-acknowledged (Repository.invokeOnce)
sf:confined-executorEXECUTOR_CHANNEL (confined-executor.ts)eventcounted, materialised, launched and collected (ConfinedExecutor.run); admitted (collect, once the control pipe holds the limit records and the preload's admission record)
sf:producerPRODUCER_CHANNEL (producer.ts), published through the contract's publishphaseprepared 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.

EventPoint (core spec §5)Span fieldsName in the core spec
store-createdC2: after createRepository returns, before recordBindingNoneNone; the crash point after C2
fence-heldO1: after holdFence returns for a slot, before any tryFence or takeOwnershipownerNone; row H kills between O1 and O2
predecessor-fencedO2: after tryFence of the recorded owner returns fenced, before takeOwnershipowner, generationNone
integration-inspectedR2(b): after inspectIntegration returns, before the one permitted outcome command, if anydispatchId, intent, observationId, invocationNone; row R13's repeated inspections
snapshot-storedF8: after one SnapshotStore.put returns, before recordSnapshotManifestsnapshotNone; the crash point mid-F8
candidate-sealedP2: after seal returns a Candidate, before recordProduction (P3)commandId, candidateNone; the crash point between P1 and P3
collection-startedV5: after the re-export before collection, immediately before collectcommandId, dispatchId, epochNone
evidence-collectedV5: after collect returns and the Candidate is re-exported, before judgement (V6)commandId, dispatchId, epochNone; the crash point between V3 and V8
verdict-issuedV6: after judgeRepository returns a Verdict, before recordVerdict (V7)commandId, dispatchId, proof, resultverdict-issued (row R12)
intent-preparedI4: after prepareIntegration returns, before recordPrepared (I5)intent, occurrenceprepared (row R12)
integration-admittedI6: after the integrate-local dispatch is admitted, before the final read (I7); never on REPLAYED or a refusalcommandId, dispatchId, intent, occurrenceadmitted (rows R12, A2 and A5)
final-read-passedI7: after the final read passes, synchronously, before invocation-startedcommandId, dispatchIdfinal-read (I7, row A3)
invocation-startedI8: immediately before invokeOncecommandId, dispatchId, intentinvoking (I8; the A2, A3 and A6 row)
guard-enteredI8: on entry to the guard, after Repository's claim and before the guard's own readcommandId, dispatchId, intent, claimguard (I8; the A2, A3 and A6 row)
invocation-returnedI8: after invokeOnce returns an observation, before recordReceipt or recordOutcome (I9)commandId, dispatchId, intent, observationId, invocationNone
  • Every message carries event and productId, and callId when the call has one. It carries workRef for 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 for candidate-sealed, and the dispatch for the others.
  • When invokeOnce claims the intent, the two channels interleave in this order: integration-admitted, final-read-passed, invocation-started, integration-claimed, guard-entered, then integration-withheld, or integration-submitted and integration-acknowledged, and last invocation-returned.

Time

  • A new command's payload carries at: one ISO 8601 UTC instant with milliseconds, such as 2026-10-02T09:00:00.000Z (the gate's TIMESTAMP form), 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.
RecordTime it holdsWhose clock
Evidence metadata (sf-evidence/1)collectedAtThe collector's
Repository collector facts, inside the Evidence bytesstartedAt, endedAt and deadlineThe collector's, around ConfinedExecutor.run
Fixture worker reportdurationMs, a duration onlyThe monotonic clock
Repository IntegrationObservationobservation.atThe host's, when the observation completed
Gate bindingcheckout.capturedAtThe caller's
Gate Slice entryfrozenAt, and deadline derived from itThe payload's frozenAt
Gate dispatchadmitted.atThe payload's now
Gate integration outcome read from an observationatThe observation's
Gate production and check admissionsat in the admission's event, repeating the payload's now (since refactor step R4); none in state. Events recorded before R4 carry no atThe payload's now
Delivery recordsfrozenAt in the Slice record; recordedAt in integration and receipt recordsThe caller's
Guidance revisionrecordedAt, in state, result and event but not in the payloadThe service's, read once before the transaction, so an identical save still replays
Lifecycle scheduleCommission/account times, occurrence and segment times; event fields vary as catalogued aboveInjected clock or supplied commission
Discovery collection and captureCollection start/end, capture fetch times and claim deadlineCollector clock
Branch healthPoll at/pollId, observation time and episode timesPoll caller; provider timestamps remain separately attributed
Owner policyrecordedAt plus supplied askedAt, answeredAt or sentAtLedger clock and attributed caller time, kept distinct
Owner digestBatch slotAt, digest issue time and note recordedAtInjected clock; calendar slot is distinct from recording time
Service accessDiscovery capturedAt; other transitions have atVersion onlyDiscovery input; no commit time invented
Portfolio observationobservedAt (null when unknown) and capturedAtThe source's and the collector's
Execution Runs and Attempts, ledger records, the fixture request, Portfolio registrations and the other gate recordsNoneNot recorded

Codes

Rule for new results

  • A new result carries code, one word of a closed union of lower-case kebab words, and detail, 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 on status.
  • 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's IntegrationGuard) 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 are CurrentCheckCode; seamAnswer (product-gate.ts) gives its refusal to either seam as that reason.
  • admitDispatch answers an exact replay with reason: "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 satisfies tables.
FamilyMeaning
invalidThe request is malformed or breaks a rule it could have known; nothing was done
not-foundA named record, Run, Product or file does not exist
staleThe request names a version, generation or Attempt that a later commit replaced; re-read before deciding again
conflictThe ID or key is already recorded with other content, or the thing already exists
limitA bound, allowance or deadline refuses the request
corruptStored data failed validation; nothing repairs it silently
unavailableA store, process, lock or host feature could not be used, and nothing was started
unknown-effectAn effect may have happened; its outcome is unknown and permits no blind retry
internalA 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

ModuleType, fileCodesForm
Command journalJournalErrorCode, errors.tsINVALID_INPUT COMMAND_CONFLICT VERSION_CONFLICT DECISION_FAILED UNSUPPORTED_SCHEMA STORAGE CLOSED NOT_FOUND READ_ONLYUpper case
ExecutionExecutionErrorCode, execution.tsRUN_NOT_FOUND RUN_EXISTS RUN_TERMINAL ATTEMPT_ACTIVE STALE_ATTEMPT CANCEL_PENDING CORRUPT_RUNUpper case
EvidenceEvidenceErrorCode, evidence.tsinvalid-input invalid-id missing corrupt conflictKebab
Attempt fenceFenceErrorCode, attempt-fence.tsinvalid-input exists in-use unavailable storage releasedKebab
Attempt workspaceWorkspaceErrorCode, attempt-workspace.tsinvalid-input exists unavailable not-heldKebab
Attempt ledgerLedgerError, attempt-ledger.tsNone: a message onlyNone
Fixture CLIFactoryErrorCode, factory-support.tsinvalid-input conflict not-foundKebab
Confined executorExecutorErrorCode, confined-executor.tsunsupported-host runtime-unavailableKebab
RepositoryRepositoryErrorCode, repository-protocol.ts (re-exported by repository.ts)invalid-input rejected exists unavailable missing conflict corrupt git-failedKebab
Repository collectorSnapshotError, repository-collector.tsmissing corrupt conflict unavailableKebab
PortfolioPortfolioErrorCode, portfolio.tsINVALID CONFLICT NOT_FOUND LIMIT UNAVAILABLE CORRUPTUpper case
Project guidanceGuidanceErrorCode, project-guidance.tsINVALID NOT_FOUND STALE CONFLICT LIMIT CORRUPTUpper case
DashboardApiErrorCode, dashboard-contract.tsBAD_REQUEST FORBIDDEN NOT_FOUND METHOD_NOT_ALLOWED UNSUPPORTED_MEDIA_TYPE PAYLOAD_TOO_LARGE CONFLICT STALE BUSY JOURNAL_UNAVAILABLE CORRUPT UNAVAILABLE INTERNALUpper case
Product gateGateErrorCode, product-gate.ts30 codes, listed in its public interfaceUpper case
Product gate, current check (a result rather than an error)CurrentCheckCode, product-gate.tsnot-bound dispatch-unknown dispatch-settled slice-closed revoked stopped not-owner deadlineKebab
Delivery recordsLedgerErrorCode, delivery-records.tsinvalid-input conflict corrupt missingKebab
ProducersProducerErrorCode, producer.tsinvalid-input invariantKebab
IntelligenceRefusalCode, intelligence.ts, a result rather than an errorunknown-task blocked not-specified not-measured escalation-undeclared escalation-mismatch pin-mismatch policy-mismatch not-qualified commission-mismatch implicit-retries producer-unknown not-independentKebab
branch-healthBranchHealthErrorCode, branch-health.tsINVALID CONFLICT CORRUPT LIMITUpper case
discovery-sourcesDiscoveryErrorCode, discovery-sources.tsINVALID CONFLICT CORRUPT CACHE_UNAVAILABLE CACHE_CORRUPTUpper case
lifecycle-scheduleScheduleErrorCode, lifecycle-schedule.tsINVALID CORRUPT CONFLICT CONTENDED LIMIT NOT_COMMISSIONED REVISION NOT_A_LANE UNKNOWN_OCCURRENCE UNKNOWN_CALL NOT_RUNNING NOT_OWNER CALL_OPEN SETTLED STALE RULEUpper case
owner-digestDigestErrorCode, owner-digest.tsINVALID CONFLICT CORRUPT LIMITUpper case
owner-policyAskLedgerErrorCode, owner-policy.tsINVALID CORRUPT CONFLICT CONTENDED LIMIT NOT_OPENED REVISION UNKNOWN_ASK RULEUpper case
service-kindsAccessErrorCode, service-kinds.tsINVALID CORRUPT STALE CONFLICT LIMIT TAKEN EXISTS OUT_OF_BINDING NOT_FOUND CLASS ROUTE KINDUpper 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.

ModuleProtocol identifiers
Evidence / Assurancesf-evidence/1: the record envelope
Local fixture worker, guarded verificationsf-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 workspacesf-attempt-fence/1, sf-attempt-workspace/1
Confined executorsf-confined-node-stateless/1 (profile), sf-confined-runtime/1, sf-confined-snapshot/1, and sf-confined/1 (the preload's control records)
Repositorysf-repository/1, sf-candidate/1, sf-integration/1
Repository Assurance and collectorsf-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 recordssf-product-gate/1, sf-product-binding/1, sf-slice/1, sf-delivery-snapshot/1
Portfolioportfolio-registry/1, portfolio-stream/1, usage-receipt/1
Project guidanceproduct-guidance/1 (the digest), product-guidance-state/1
Dashboardfactory-dashboard/3, served over HTTP and never stored
Intelligencesf-intelligence/1; task protocols sf-producer-prompt/1, sf-review-prompt/1 and sf-review-observation/1
Producerssf-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
Reservedsf-factory-inspect/1: the fixture inspect document, which carries no protocol member; nothing emits the identifier
Lifecycle schedulesf-lifecycle-schedule/1, sf-schedule-commission/1
Discovery sourcessf-research-brief/1, sf-capture/1, sf-collection/1, sf-collection-claim/1, sf-collection-starts/1, sf-citation/1, sf-drift/1
Service accesssf-service-access/1, sf-service-access-bindings/1, sf-service-catalogue/1, sf-service-map/1, sf-tree-scan/1
Service catalogue route protocolsgithub-actions/1, app-store-connect/1, apple-signing/1, google-play/1, android-signing/1 (declarations, not proof of an implemented broker)
Branch healthsf-branch-health/2; earlier states are refused, never migrated
Owner policysf-ask-ledger/1
Owner digestsf-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 recheck in guarded-verification.ts, the stored-report check observationProblem in factory-support.ts, and the gate's re-derivation in #integrationDispatch in product-gate.ts. A reworded reason would therefore make every stored fixture Verdict invalid and every retained repository proof NOT_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_SOURCES in fixture-manifest.ts are byte-frozen: a change makes every completed /2 Run present as invalidated. The eight files of COLLECTOR_SOURCES in repository-collector.ts are digested into every repository recipe: a change invalidates every repository Verdict made under the old digest. Both lists hold evidence.ts. Since refactor step R3 the collector's list holds Repository's wire grammar, repository-protocol.ts, in place of repository.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/1 takes 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 inspect document is /1 by default. FactoryResult in factory-support.ts, plus the CLI's exitCode, has no protocol member. A reader, such as step 1's run-briefing tool (increment 8), reads a document without one as sf-factory-inspect/1 and refuses any protocol it does not know. A later /2 names itself in that member.

Vocabulary

Three terms of the delivery core map to the Factory's one language as follows.

Term in codeWhat it isFactory term
AllowanceThe gate's GrantLimits and SliceEntry.used: production runs, check runs, Attempts per check, integrations and elapsed timeThe Slice's Budget. Execution's Budget is per Run
generationThe gate owner's ownership version: 1 for the first owner, and one higher at each later takeOwnershipThe Product-scope epoch (Workers). Execution's epoch is per Run and Attempt
Check dispatchA 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.

NameSenses
CheckOutcomeA check's judgement word (assurance.ts); a check dispatch's outcome (product-gate.ts)
AdmissionA producer's dispatch answer (producer.ts); a gate production or check admission (product-gate.ts)
LedgerErrorThe Attempt ledger's error (attempt-ledger.ts); the delivery records' error (delivery-records.ts)
TokenCountsPortfolio usage tokens (portfolio-inventory.ts); provider tokens (producer.ts)
OutcomeThe fixture CLI's result word (factory-support.ts); a producer's outcome (producer.ts); neither is the domain Outcome
DecisionA journal decision (journal.ts); a module's model involvement (intelligence.ts)
RegistryThe Portfolio registry (portfolio.ts); the Intelligence registry (intelligence.ts)
RepositoryIdentityA Factory-owned store (repository-protocol.ts, re-exported by repository.ts); a registered checkout (portfolio-inventory.ts)
MAX_TIMEOUT_MS60,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.

DuplicateWhereWhy it staysHeld equal by
The sf-evidence/1 envelope, which gives a record its identityevidence.ts (private); EVIDENCE_FORMAT in repository-assurance.tsevidence.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 classifyObservationclassify in assurance.ts (private); classifyObservation in repository-assurance.tsassurance.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 listingDigestsnapshotDigest in confined-executor.ts; listingDigest in repository-assurance.tsRepository 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 nameconfined-executor.ts; repository-assurance.tsThe same import rule"the module repeats the executor limits it may not import, and imports no I/O"
Canonical JSONcanonicalJson in canonical-json.ts; evidence.ts (private)The fixture closure stays closed: canonical-json.ts is not in itThe fixture closure pins evidence.ts byte for byte
PRODUCT_IDportfolio.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.tsIdentical patterns; no test
Repository's wider product IDcreateRepository 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 oneDocumented, not narrowed
The local-fixture-scenario/<digest> formulalocal-factory.ts (/1); guarded-verification.ts (/2)Retained journals hold /1 Runs, and only the owner retires that pathNo 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 data shape is a protocol revision.
  • A change to the sf:delivery list 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.

Source: docs/agents/observability.md