Seals bounded text-file proposals as immutable Candidates in a factory-owned bare Git repository, exports their exact bytes, and performs the one guarded local integration effect (target ref B → M, at most once per prepared Action occurrence).
Status: Repository sealing / export is "Accepted for fresh owned local repositories"; Repository integration is "Accepted as a local effect primitive" (build review). The additive pre-move guard on invokeOnce (step 1, increment 3b) is accepted on its independent implementation review (Astra round 2 PASS) on 29 September 2026; round 1 FAILed on one finding, a late allowance from a guard that held the thread past the time bound, fixed with a clock check and a test row (<local evidence archive>). Nothing in src/ passes a guard yet. Receipts: sealing, integration, native HEAD.lock repair, native loose-object finalisation repair, pre-move guard seam. Hash coverage today (checked 2026-09-29 on factory/delivery with the seam): the seam receipt's evidenceSha256 matches the current src/repository.ts and test/repository-integration.test.ts, so it is the receipt that now describes src/repository.ts; the nlink receipt still matches the current test/helpers/repository.ts, test/repository-boundaries.test.ts and test/repository-nlink-adversarial.test.ts, and its hashes of src/repository.ts and test/repository-integration.test.ts are of superseded versions; the lock receipt's hashes of those files are of superseded versions, and its docs/local-development.md hash is stale since 7b499ea added the dashboard package check; test/repository.test.ts still matches the sealing receipt, and test/helpers/repository-integration-child.ts the integration receipt; the other hashes in those two receipts are of superseded versions. This is accepted local infrastructure, not a Verdict, release or customer benefit, and all 27 epics remain open. Long-form contract: local development.
- Source:
src/repository.ts - Tests:
test/repository.test.ts,test/repository-boundaries.test.ts,test/repository-integration.test.ts,test/repository-nlink-adversarial.test.ts - Helpers:
test/helpers/repository.ts,test/helpers/repository-integration-child.ts
Intelligence: none — Seals and integrates exact bytes: every identity is recomputed and the target ref moves once per intent; nothing is inferred.
What it hides
- Git as an execution surface. A pinned root-owned binary, fixed argument lists, an explicit environment, hooks off, and time and output bounds. Only
unpack-objects,cat-file,update-refandfor-each-refrun. Callers never see Git. - Ownership of the on-disk root. Canonical path, device, inode, owner, mode 0700, exact top-level layout (the one tolerated extra entry is Git's own
HEAD.lock; see invariant 2), and byte-identical marker,configandHEAD. The check runs before every Git process starts and again before any non-timeout result is accepted, and a missing or replaced root is refused, never recreated. - Object construction and independent verification. Every blob, tree and commit is built here from validated bytes and written through strict unpack. Source, Candidate and intent objects are independently read back when written. Retained dispatch and receipt commits are independently verified during inspection and replay; the invoking call itself reports their constructed IDs after Git's acknowledgement without reading their bodies back.
- Integration bookkeeping. Create-only
intent,dispatchandreceiptrefs make one invocation per intent, with present state reported apart from retained history.
Public interface
Constants (src/repository.ts:77-103):
| Name | Value |
|---|---|
REPOSITORY_PROTOCOL / CANDIDATE_PROTOCOL / INTEGRATION_PROTOCOL | "sf-repository/1" / "sf-candidate/1" / "sf-integration/1" |
REPOSITORY_CHANNEL | "sf:repository" (diagnostics_channel; diagnostic only, never Evidence) |
GIT_EXECUTABLE | "/Applications/Xcode.app/Contents/Developer/usr/bin/git" (must be a root-owned regular file, writable only by root) |
REPOSITORY_LIMITS | files: 256, fileBytes: 131072, sourceBytes: 1048576, edits: 64, proposalBytes: 262144, pathBytes: 240, depth: 16, scopeEntries: 64 (frozen) |
DEFAULT_GIT_TIMEOUT_MS / MIN_GIT_TIMEOUT_MS / MAX_GIT_TIMEOUT_MS | 30_000 / 100 / 120_000 (per Git process) |
Internal bounds that are not exported: a guard's refusal reason is 1–256 printable ASCII characters, and a guard is awaited for at most gitTimeoutMs; intent payload ≤ 4096 bytes, manifest ≤ 1 MiB, repository path ≤ 1024 characters, target ref ≤ 128 characters, for-each-ref output ≤ 64 KiB, and stderr captured to about 16 KiB (counted in UTF-16 code units, not bytes) and reported trimmed to 1024 code units. Object reads are bounded per call, not per object: cat-file --batch output may not exceed the call's budget plus 64 framing bytes per unique object. A single commit or intent read is budgeted at MAX_COMMIT_BYTES (4096), dispatch plus receipt share 8192, one tree level gets 4 MiB, a snapshot's blobs get sourceBytes, and an export's base, M, trees and blobs get sourceBytes + 4 MiB together.
Formats: object IDs are full 40-character lowercase SHA-1 hex; digests are sha256: plus 64 lowercase hex; productId, slice.id, action.id and owner.id match ^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$; occurrence, revision and generation are positive safe integers. Every input record must be a plain object holding its declared fields and no others, as enumerable own data properties, and every list a plain dense array; extra fields (including a claimed passed or verified), accessors, symbol or reserved keys and holes are refused: rejected for a proposal, invalid-input for everything else.
Create and open
createRepository(options: { readonly path: string; readonly productId: string; readonly targetRef: string; readonly files: readonly SourceText[] }): Promise<RepositoryIdentity>:pathmust be absolute, normalised and canonical (realpathequal to itself: on macOS/private/var/…, never/var/…). Its parent must be a canonical directory of this user, not group- or world-writable.targetRefmatches^refs/heads/[A-Za-z0-9_-][A-Za-z0-9._-]*(?:/[A-Za-z0-9_-][A-Za-z0-9._-]*)*$, is at most 128 characters, contains no.., and has no segment ending in.or.lock. It usesDEFAULT_GIT_TIMEOUT_MS.parseRepositoryIdentity(value: unknown): RepositoryIdentity: exact parse; returns a frozen copy.openRepository(identity: RepositoryIdentity, options?: { readonly gitTimeoutMs?: number }): Repository: synchronous. It is the only way to obtain aRepository;new Repository(...)throwsTypeError.
class Repository (frozen; readonly identity: RepositoryIdentity)
- Read:
target(): Promise<string>andsnapshot(commit: string): Promise<SourceSnapshot>. A snapshot accepts only a factory commit of this repository: the pinned initial commit, or a stored commit in this Product's Candidate format (factoryIDENT, canonical encoding,sf-candidate/1message naming this Product). It does not check that a Candidate ref publishes that commit;exportCandidatedoes. - Seal:
seal(frozen: FrozenInputs, proposal: Proposal): Promise<SealedCandidate>. - Export:
exportCandidate(digest: string): Promise<SealedCandidate>. - Integrate:
prepareIntegration(payload: IntegrationPayload): Promise<PreparedIntegration>invokeOnce(prepared: PreparedIntegration, guard?: IntegrationGuard): Promise<IntegrationObservation>; aguardthat is given but not a function isinvalid-inputbefore anything is read or writteninspectIntegration(prepared: PreparedIntegration): Promise<IntegrationObservation>
Types
- Identity and source:
RepositoryIdentity {protocol, productId, path, dev, ino, nonce, objectFormat: "sha1", targetRef, initial}is JSON-safe; persist it.SourceText {path, text}SourceFile {path, mode: "100644", blob, sha256, bytes, text}SourceSnapshot {commit, tree, files}
- Proposals:
FileEdit {path, prior: string | null, text: string | null}andProposal {edits}, with 1–64 edits (an empty proposal isrejected).prior: nullcreates a file that must not exist; otherwiseprioris the current file'ssha256.text: nulldeletes. Both null is rejected, as is an unchanged file. Each path appears once (compared ignoring case), and every edit must equal ascopeentry or sit below it.proposalBytescounts paths plus replacement UTF-8 text. The canonical JSON of the proposal must also fitcanonicalJson's 1 048 576-byte bound (JSON_LIMITS.maxBytesinsrc/canonical-json.ts); JSON escaping of control characters can exceed it withinproposalBytes, which fails asinvalid-inputafter validation and before any write. - Candidates:
FrozenInputs {base, slice: {id, revision}, scope, acceptance, recipe, environment}, wherescopeis 1–64 distinct valid paths and the last three are"sha256:<hex>".ManifestFile {path, mode, blob, sha256, bytes}CandidateManifest {protocol, repository, slice, scope, acceptance, recipe, environment, base: {commit, tree}, commit, tree, files, proposal: {digest, edits}}, where each manifest edit is{path, prior, next}andnextis the new bytes'sha256:digest or null for a deletion (never the text).SealedCandidate {digest, manifest, files: readonly SnapshotFile[], snapshotDigest}, whereSnapshotFile {path, content: Uint8Array}is the confined executor's type.
- Integration:
IntegrationPayload {action: {id, occurrence}, product, slice, repository, ref, base, commit, candidate, proof, owner: {id, generation}}, withbase ≠ commitand a canonical encoding of at most 4096 bytes.PreparedIntegration {protocol, digest, intent, payload}is JSON-safe; persist it.IntegrationReceipt {id, target}IntegrationObservation {intent, observation: {id, at}, ref, target, postcondition, dispatch, receipt, invocation, dispatched, problem}, where:intentis the payload digest (prepared.digest), not the intent commitprepared.intent;target,dispatchandproblemarestring | null, andreceiptisIntegrationReceipt | null;postconditionis"desired-state-observed" | "not-observed" | "unknown";invocationis"not-dispatched" | "confirmed" | "unknown" | "withheld";withheldcomes only from the guardedinvokeOncecall that made the claim (invariant 15);dispatchedis true only for the call that started the update; an inspection can reportinvocation: "unknown"withproblem: null.
- Guard (step 1, increment 3b):
IntegrationClaim {intent, dispatch}: the payload digest and the claim D this call holds, given to the guard frozen;IntegrationGuardAnsweris{proceed: true} | {proceed: false, reason};IntegrationGuardis(claim: IntegrationClaim) => IntegrationGuardAnswer | PromiseLike<IntegrationGuardAnswer>.
- Errors:
RepositoryErrorCodeandclass RepositoryError extends Error { readonly code }.
Stored layout: Candidates live under refs/sf/candidates/<manifest sha256 hex> → seal commit S (tree {manifest.json}, parent M; M's sole parent is B). Integrations use refs/sf/integrations/<key>/{intent,dispatch,receipt}, where <key> is the SHA-256 of the canonical {protocol, action}, so Action IDs never shape ref names.
Invariants and guarantees
Never adopts.
createRepositorycreatespathwith an exclusivemkdir(existsotherwise) and writes the ownership markersf-repository.jsonlast. An unmarked directory never opens (createRepository,checkRootLayoutat:986). Tests: boundaries "forged, moved or replaced … never recreated".Root pinned across every Git process.
GitRunner.#run(:1356) re-checks ownership synchronously before the spawn and again when the process closes, before its result is accepted; a timeout rejects without that second check. The pre-spawn guard also checks the ref path an operation touches and, for unpack, each loose-object file it will write. Reads rely on the layout check of loose-object directories, bounded output and content verification, so a FIFO in place of an object is caught by the time bound. No effect or result is accepted across a replaced root or a linked internal directory. The only tolerated extra top-level entry isHEAD.lock, and only as an empty regular file of this user with at most one link, or already gone when examined (isGitHeadLock,:1067); it is never opened, removed or awaited, its Git origin is not authenticated, and any other lock name or shape isunavailable.objects,objects/infoandobjects/packmust be directories of this user. Each loose object an unpack will write must be absent or a regular, singly linked file of this user, with one tolerated exception: Git's own finalisation, in which a concurrent writer's Git has linked itstmp_obj_XXXXXXfile to the object's name and not yet removed it (looseObjectProblem,:1085). That object passes only if it inflates to exactly the object's bytes and, counted after that read, its links are its own name alone, or its own name and that temporary name beside it in the same fan-out directory, every observation the same inode and this user's. The search for the temporary name examines at most 65,536 entries of the fan-out directory; nothing is waited for, and the temporary name is never opened, removed or taken. A path the guard cannot examine refuses asunavailable, never with the file system's own error. Tests: REP-1 and REP-2 in boundaries; "root replaced around the target update" in integration; "only Git's own empty HEAD.lock is tolerated"; "only Git's own finalisation link is tolerated" in boundaries; "an object a sibling's Git is still finalising" in integration;repository-nlink-adversarial.test.ts.A rejected proposal writes nothing.
parseProposal(:1722) andapplyEdits(:1758) validate the whole proposal against the base in memory before any object is built. Test: "rejected whole before anything is written" (46 cases, byte-identical storage).Candidate identity. The digest is
sha256:of the canonical manifest bytes. M's message derives from the frozen inputs and the proposal digest, never the manifest, so there is no cycle. The manifest includes the repository identity, so the same edits in another repository give another digest. Identical inputs return the same Candidate and write nothing, and concurrent identical seals converge on one ref. Test: "identical inputs replay one Candidate".Only a published ref names a Candidate. Objects left by an interrupted seal are unreachable and never qualify; a later seal completes them. Test: "a seal killed after writing objects publishes nothing".
Export is re-verified, never repaired. It checks:
- S and the manifest, parsed strictly and canonically;
- that the manifest's repository equals this handle's identity;
- M and every tree, rebuilt from the manifest and compared byte for byte;
- every blob's size, SHA-256 and text subset;
- that B is a factory commit with the recorded tree.
Returned files are fresh copies. Tests: "corrupt, missing or swapped protected objects never qualify", "scoped to its own repository", "forged manifest".
sealdoes not require B to be the current target. B must only be a factory commit of this repository, assnapshotaccepts it, published or not. Staleness is checked only byinvokeOnce.M is never taken from the caller.
prepareIntegrationre-exports the Candidate and requires the payload's Product, repository, ref, B, M and Slice to match. It retains one immutable intent per (action.id,occurrence): an exact replay returns it, and any other payload isconflict. Preparation moves no target and grants nothing.At most one target update per intent, ever.
invokeOnce:- replays (starts nothing) if a dispatch claim exists;
- makes no claim if the target ≠ B;
- otherwise claims with a create-only dispatch ref naming a fresh commit D (random nonce).
Only the call whose own
update-refexited 0 runsupdate-ref --no-deref <target> M B, once, and with aguardonly after the guard allowed it (invariant 15). A claim that lost to another call's D returns that observation (dispatched: false). A claim Git did not acknowledge, with the ref absent or naming this call's own D, throwsgit-failedand starts nothing. Tests: "two processes invoking the same intent", the B → M1/M2 race, and the HEAD.lock case.Confirmation needs both signals. Git exit 0 plus a readback of exactly M confirms this call and produces receipt R (parent D). Anything else after submission is
unknown; there is no negative receipt. Test: INT-1. Receipt retention is a separate step: if writing R throws, this call still returnsinvocation: "confirmed"withreceipt: nulland the reason inproblem. That write may still have landed (for example a timeout after Git created the ref), so later inspection reports what it reads:confirmedif a valid receipt exists,unknownif only the dispatch exists, or it throwscorruptif a record fails verification. No test exercises a failed receipt write.Create-only records. Intent, dispatch, receipt and Candidate refs are never moved or deleted by this module. The target ref moves only through the guarded
M Bupdate, with no retry, rebase, merge, dereference or checkout.inspectIntegrationis read-only. It reads the target, then the receipt, then the dispatch, and writes no object or ref. A confirmed history survives later target drift, and an unknown history is never resolved by M appearing or disappearing (#observe,:816).Fixed Git surface.
- Hooks, fsmonitor, reflogs, gc and maintenance are disabled in the owned config and with
-con every command (COMMAND_CONFIG,:374). - The environment is
GIT_ENVIRONMENT(:375) plusGIT_DIR: no system or global config,HOME=/var/empty,PATH=/usr/bin:/bin;cwdis the repository. - Repository content is never executed.
Test: "hostile ambient Git configuration, hooks, templates and environment never execute", with a live negative control.
- Hooks, fsmonitor, reflogs, gc and maintenance are disabled in the owned config and with
Supported source subset.
- Files are mode
100644, valid UTF-8 without NUL and not LFS pointers. - Paths use segments of
A-Z a-z 0-9 . _ -, with no.,..or.git*(case-insensitive). - Names must not differ only in case, and no path may be both a file and a directory.
REPOSITORY_LIMITSis enforced before any persistent write: a proposal is checked before objects are built, whilecreateRepositoryconstructs the initial blobs in memory before its final size and layout checks, still before it creates the directory. Test: "source limits are exact".
- Files are mode
The pre-move guard (step 1, increment 3b).
invokeOnce(prepared, guard)behaves exactly as without a guard until the call holds the claim; without a guard nothing changes. The claim holder then awaitsguard(claim)once, after the claim and every other asynchronous step of the call, immediately before the target update (#consult,:757). Only exactly{proceed: true}(a plain object with that one data property) lets the update start, and the ownership check still runs before Git is spawned. Anything else starts no update and returnsinvocation: "withheld",dispatched: false, the claim D indispatch,receipt: null, the target as read before the claim, andproblem"withheld before submission: <reason>":- a refusal
{proceed: false, reason}gives its own reason (1–256 printable ASCII characters); - a throw or rejection gives
the guard failed: <message>, the message cut to 256 UTF-16 code units (a value that cannot be describedwhen it has no string form); - any other answer (nothing,
true,{proceed: "yes"}, an allowance with extra fields or a reason, an accessor, a class instance, a refusal without a valid reason) gives one fixed reason; - no answer within
gitTimeoutMsgivesthe guard did not answer within <n> ms; a later answer is ignored, and the guard is abandoned, not stopped. Synchronous caller code cannot be interrupted, and while it holds the thread the timer cannot fire, so an allowance also counts as late when the monotonic clock (performance.now()), read after the answer is validated, showsgitTimeoutMsor more since the guard was called: a synchronous, async or thenable guard, or an answer whose reading blocks, that allows only after the bound is withheld.
The claim is kept and nothing else is written, so the occurrence is consumed: inspection, replays, other handles and other processes read a claim without a receipt as
unknownand start nothing. The guard is never consulted by a replay, a call that lost the claim or whose claim Git did not acknowledge, a call refused before the claim (stale base, invalid, missing or corrupt records), or after the update has started.REPOSITORY_CHANNELpublishesintegration-withheldafterintegration-claimedin place ofintegration-submitted. Tests: the six guard rows at the end ofrepository-integration.test.ts.- a refusal
Failure semantics
RepositoryError.code | Meaning |
|---|---|
invalid-input | The caller's own arguments: identity, options, FrozenInputs, digest, payload shape, a guard that is not a function, a PreparedIntegration that does not match its payload, a payload over 4096 bytes, or a proposal whose canonical JSON exceeds 1 MiB (canonical, :1984). Nothing is touched. |
rejected | Untrusted proposal data is malformed or does not apply (bad prior, out of scope, unchanged, collision or over a limit), or a payload does not match this repository or its Candidate at preparation. |
exists | createRepository found something at path. |
unavailable | The root is missing, replaced, not exactly as owned, or a traversed internal path is linked or foreign. The parent is unusable, or the platform is not POSIX. |
missing | Unknown commit, unpublished Candidate, or no prepared intent for the occurrence. |
conflict | Another payload is already retained for the same Action occurrence, at preparation, invoke or inspect. |
corrupt | Stored data failed verification, including a Candidate or payload mismatch found at invoke or inspect time, a missing target ref or initial commit, a failing cat-file or for-each-ref, and object or ref read output over its bound. (Unpack output over its bound is git-failed; a target-update overflow is a lost acknowledgement, so unknown.) |
git-failed | The pinned Git is absent or not root-owned, a spawn failed, the process was killed at gitTimeoutMs (SIGKILL), unpack or create-ref failed, or the dispatch claim was not acknowledged. |
invokeOncethrows only when it started no target update. Onceupdate-refis spawned, it returns an observation withdispatched: true, and the unresolved facts areunknownwith the reason inproblem. That covers a failed exit, a signal, a timeout, an output overflow, a failed readback and a root replaced mid-update. An unwritable receipt after a confirmed update keepsinvocation: "confirmed"in that call's observation, withreceipt: nulland aproblem.- Withheld is a certain non-start, not a failure. A guarded call that returns
withheldstarted no update, and says so only in that call's result; the retained claim reads asunknowneverywhere else, since a claim without a receipt cannot show that nothing was started. It never licenses dispatching the same occurrence again: a new effect needs a new occurrence, which the caller must decide it may prepare. - Unknown is not failed. A non-zero exit or a readback other than M never means "not applied" (INT-1).
invocation: "unknown"never licenses a retry. The caller must not prepare the same effect under a new occurrence while the earlier one may be in flight. - Postcondition is not attribution.
desired-state-observedmeans the target named M when read. It does not show who wrote M, or that M will stay. - Retries. None inside the module.
- Idempotency:
sealandprepareIntegrationare idempotent on identical input;invokeOncereplays report what they read and start nothing;createRepositoryis not idempotent (exists).
Trust scope
Established locally. These hold in tests on macOS (Darwin 27, arm64), Node 26.8.1 and Apple Git 2.50.1:
- Exact sealing and export, with fail-closed handling of corruption, relabelling, cross-Product copies, forged manifests, root replacement and redirected internal paths.
- One invocation per intent across repeats, concurrent processes, restarts and kills, with unknown outcomes reported truthfully and read-only reconciliation.
- A native
HEAD.lockheld by a concurrent trusted target update is tolerated (empty, this user's, at most one link) and never taken; a guarded update that meets it fails inside Git's bounds and, after the claim, staysunknown. - Git's own loose-object finalisation link, seen when concurrent first integrations write the shared empty tree, is tolerated with every link accounted for and the content exact, and never touched; other second links, kinds, owners and contents refuse before any claim, in the guard before Git or the one after it.
- Composition with the real confined executor, proven by one permanent test.
Not established:
- Storage and writers.
- Protection from a concurrent hostile writer: checks bracket each process, but Node has no
openat. - Protection from forging by any writer with full directory access.
- Detection of
objects/replaced by another real directory of this user: the identity pins only the root, so a swapped-in directory is used as found. Its objects are still verified against their IDs when read, and the links of each object written are accounted inside the root. - Tolerance of Git's finalisation link in a fan-out directory of more than 65,536 entries: it refuses unless the temporary name is among the entries examined.
- Crash atomicity across the claim, target move and receipt, which are separate single-ref updates.
- Power-loss durability:
core.fsync=committeddoes not fsync refs.
- Protection from a concurrent hostile writer: checks bracket each process, but Node has no
- Scope.
- Any aggregate repository disk quota or retention.
- Importing existing repositories or checkouts, remotes, other ref backends and SHA-256 object format.
- Any other host: the Git path is pinned to Xcode's binary on macOS.
- Refusal of a foreign UID: checked in code, and for loose objects and the objects directories with owners reported by the tests, but not exercised with another account.
- Authority.
- Authority, grants, revocation, dispatch admission and owner replacement, which belong to the Product gate; it is accepted but not yet composed with this module.
- Authentication of
proof, which is bound and retained, not checked as a Verdict. - External fencing of an already-admitted sender or another writer.
- Closing the window after the guard: a change of authority after the guard answers and before Git's rename is not seen; the residual race spans the
update-refspawn. The guard is trusted caller code, is not authenticated, and one that never answers is abandoned aftergitTimeoutMsbut keeps running.
- Value. Any Verdict, release or customer-value claim.
Composition
- Depends on:
src/canonical-json.ts(Command journal):canonicalJsonfor manifest, marker, payload and key bytes, andjsonObjectEntries/jsonArrayItemsfor strict shape reads;src/confined-executor.ts(Confined executor):snapshotDigestandtype SnapshotFileforSealedCandidate.filesand.snapshotDigest;- Node builtins only otherwise (
child_process,crypto,diagnostics_channel,fs,path,zlib).
- Used by: in
src/, the Producers contract imports its types andREPOSITORY_LIMITS, Repository Assurance importsparseRepositoryIdentityand its manifest types, and the Product gate and delivery records and the Repository collector (accepted, step 1, increment 3) import its types only; the collector runs exported Candidate files and the base'ssnapshotas executor programs when handed them, but never calls Repository. Nothing composes it.ConfinedExecutor.run({ input: { files, digest: snapshotDigest } })consumes an export intest/repository.test.ts("an exported Candidate runs as the accepted confined executor input"). - Not the same Git surface: Portfolio inventory (
src/portfolio-inventory.ts, default/usr/bin/git) and the Dashboard repository check (src/dashboard-server.ts) inspect existing checkouts with their own code, not this module. - Intended caller contract: call
invokeOnceonly for a dispatch the Product gate has admitted, after its final current-state read, and pass aguardthat repeats that read immediately before the move; record awithheldresult as not dispatched, with its claim. PersistRepositoryIdentityandPreparedIntegration, and reconcile withinspectIntegration. Nothing insrc/passes a guard yet.
Changing it safely
Run (only when told; the tests spawn real Git and child processes, and the HEAD.lock test allows 60 s):
- focused:
node --test test/repository.test.ts test/repository-boundaries.test.ts test/repository-integration.test.ts test/repository-nlink-adversarial.test.ts; - typecheck:
npm run typecheck; npm run checkbefore acceptance.
Tests spawn the real Git at
GIT_EXECUTABLE, and they useREPOSITORY_CHANNELevents (initial-objects-written,objects-written,integration-claimed,integration-submitted,integration-acknowledged,integration-withheld) for fault injection and ordering;publishedis also emitted, but no current test consumes it. Renaming an event breaks the fault tests.- focused:
Which tests prove what:
repository.test.ts: round trip, replay, restart, rejection-writes-nothing, invalid input, exact limits and executor composition.repository-boundaries.test.ts: identity, REP-1 and REP-2, layout, HEAD.lock and loose-object link tampering, cross-Product copies, corruption, forged manifests, unsupported commits, hostile config, a kill before publication, and time, output and storage bounds.repository-integration.test.ts: intent and conflict, races, one dispatch, a sibling's Git still finalising an object, stale base, INT-1, restart, kills, drift, forged records, linked or replaced roots, and the guard seam (allow, refuse, throw or malformed or late answer, consulted only by the claim holder, a root replaced during the guard).repository-nlink-adversarial.test.ts: the loose-object guard's refusals of other links, kinds and owners, legitimate and hostile races before and after Git, its bounded search, the statedobjects/limit and a never-released finalisation shape.
Receipts: each receipt pins SHA-256 hashes of
src/repository.tsand the test files it lists, as they were at that acceptance (coverage today is under Status); an edit to any covered file makes its receipt stale. A behaviour change needs a fresh independent review and a new receipt. Update the local development section and the build review rows. Never rewrite hashes in an existing receipt.Encoding changes are protocol changes. Changing any of these alters or breaks the digests and verification of existing stored data, so it needs a new protocol string, never a silent edit:
- the manifest shape;
- any commit message (
initialMessage,candidateMessage, seal, intent, dispatch, receipt) orIDENT; CONFIG,HEADorTOP_LEVEL;- the marker;
- the integration key.
Reviewers check:
- no new Git subcommand, argument or environment entry;
- the ownership guard still runs before every spawn and before any result is accepted;
- validation still happens before any write;
- the marker is still written last;
- refs stay create-only;
- there is still no retry after submission;
unknownis never downgraded to failed or not applied;invokeOncestill throws only before submission;- the guard is still awaited only by the claim holder, after the claim and immediately before the update, and anything but exactly
{proceed: true}withholds it.
