Factory docs, home
Page navigation

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-ref and for-each-ref run. 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, config and HEAD. 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, dispatch and receipt refs make one invocation per intent, with present state reported apart from retained history.

Public interface

Constants (src/repository.ts:77-103):

NameValue
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_LIMITSfiles: 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_MS30_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>: path must be absolute, normalised and canonical (realpath equal to itself: on macOS /private/var/…, never /var/…). Its parent must be a canonical directory of this user, not group- or world-writable. targetRef matches ^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 uses DEFAULT_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 a Repository; new Repository(...) throws TypeError.

class Repository (frozen; readonly identity: RepositoryIdentity)

  • Read: target(): Promise<string> and snapshot(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 (factory IDENT, canonical encoding, sf-candidate/1 message naming this Product). It does not check that a Candidate ref publishes that commit; exportCandidate does.
  • 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>; a guard that is given but not a function is invalid-input before anything is read or written
    • inspectIntegration(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} and Proposal {edits}, with 1–64 edits (an empty proposal is rejected). prior: null creates a file that must not exist; otherwise prior is the current file's sha256. text: null deletes. Both null is rejected, as is an unchanged file. Each path appears once (compared ignoring case), and every edit must equal a scope entry or sit below it. proposalBytes counts paths plus replacement UTF-8 text. The canonical JSON of the proposal must also fit canonicalJson's 1 048 576-byte bound (JSON_LIMITS.maxBytes in src/canonical-json.ts); JSON escaping of control characters can exceed it within proposalBytes, which fails as invalid-input after validation and before any write.
  • Candidates:
    • FrozenInputs {base, slice: {id, revision}, scope, acceptance, recipe, environment}, where scope is 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} and next is the new bytes' sha256: digest or null for a deletion (never the text).
    • SealedCandidate {digest, manifest, files: readonly SnapshotFile[], snapshotDigest}, where SnapshotFile {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}}, with base ≠ commit and 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:
      • intent is the payload digest (prepared.digest), not the intent commit prepared.intent;
      • target, dispatch and problem are string | null, and receipt is IntegrationReceipt | null;
      • postcondition is "desired-state-observed" | "not-observed" | "unknown";
      • invocation is "not-dispatched" | "confirmed" | "unknown" | "withheld"; withheld comes only from the guarded invokeOnce call that made the claim (invariant 15);
      • dispatched is true only for the call that started the update; an inspection can report invocation: "unknown" with problem: null.
    • Guard (step 1, increment 3b):
      • IntegrationClaim {intent, dispatch}: the payload digest and the claim D this call holds, given to the guard frozen;
      • IntegrationGuardAnswer is {proceed: true} | {proceed: false, reason};
      • IntegrationGuard is (claim: IntegrationClaim) => IntegrationGuardAnswer | PromiseLike<IntegrationGuardAnswer>.
  • Errors: RepositoryErrorCode and class 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

  1. Never adopts. createRepository creates path with an exclusive mkdir (exists otherwise) and writes the ownership marker sf-repository.json last. An unmarked directory never opens (createRepository, checkRootLayout at :986). Tests: boundaries "forged, moved or replaced … never recreated".

  2. 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 is HEAD.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 is unavailable. objects, objects/info and objects/pack must 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 its tmp_obj_XXXXXX file 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 as unavailable, 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.

  3. A rejected proposal writes nothing. parseProposal (:1722) and applyEdits (: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).

  4. 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".

  5. 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".

  6. 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".

  7. seal does not require B to be the current target. B must only be a factory commit of this repository, as snapshot accepts it, published or not. Staleness is checked only by invokeOnce.

  8. M is never taken from the caller. prepareIntegration re-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 is conflict. Preparation moves no target and grants nothing.

  9. 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-ref exited 0 runs update-ref --no-deref <target> M B, once, and with a guard only 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, throws git-failed and starts nothing. Tests: "two processes invoking the same intent", the B → M1/M2 race, and the HEAD.lock case.

  10. 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 returns invocation: "confirmed" with receipt: null and the reason in problem. That write may still have landed (for example a timeout after Git created the ref), so later inspection reports what it reads: confirmed if a valid receipt exists, unknown if only the dispatch exists, or it throws corrupt if a record fails verification. No test exercises a failed receipt write.

  11. 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 B update, with no retry, rebase, merge, dereference or checkout.

  12. inspectIntegration is 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).

  13. Fixed Git surface.

    • Hooks, fsmonitor, reflogs, gc and maintenance are disabled in the owned config and with -c on every command (COMMAND_CONFIG, :374).
    • The environment is GIT_ENVIRONMENT (:375) plus GIT_DIR: no system or global config, HOME=/var/empty, PATH=/usr/bin:/bin; cwd is the repository.
    • Repository content is never executed.

    Test: "hostile ambient Git configuration, hooks, templates and environment never execute", with a live negative control.

  14. 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_LIMITS is enforced before any persistent write: a proposal is checked before objects are built, while createRepository constructs the initial blobs in memory before its final size and layout checks, still before it creates the directory. Test: "source limits are exact".
  15. 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 awaits guard(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 returns invocation: "withheld", dispatched: false, the claim D in dispatch, receipt: null, the target as read before the claim, and problem "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 described when 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 gitTimeoutMs gives the 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, shows gitTimeoutMs or 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 unknown and 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_CHANNEL publishes integration-withheld after integration-claimed in place of integration-submitted. Tests: the six guard rows at the end of repository-integration.test.ts.

Failure semantics

RepositoryError.codeMeaning
invalid-inputThe 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.
rejectedUntrusted 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.
existscreateRepository found something at path.
unavailableThe 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.
missingUnknown commit, unpublished Candidate, or no prepared intent for the occurrence.
conflictAnother payload is already retained for the same Action occurrence, at preparation, invoke or inspect.
corruptStored 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-failedThe 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.
  • invokeOnce throws only when it started no target update. Once update-ref is spawned, it returns an observation with dispatched: true, and the unresolved facts are unknown with the reason in problem. 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 keeps invocation: "confirmed" in that call's observation, with receipt: null and a problem.
  • Withheld is a certain non-start, not a failure. A guarded call that returns withheld started no update, and says so only in that call's result; the retained claim reads as unknown everywhere 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-observed means the target named M when read. It does not show who wrote M, or that M will stay.
  • Retries. None inside the module.
  • Idempotency:
    • seal and prepareIntegration are idempotent on identical input;
    • invokeOnce replays report what they read and start nothing;
    • createRepository is 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.lock held 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, stays unknown.
  • 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=committed does not fsync refs.
  • 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-ref spawn. The guard is trusted caller code, is not authenticated, and one that never answers is abandoned after gitTimeoutMs but keeps running.
  • Value. Any Verdict, release or customer-value claim.

Composition

  • Depends on:
    • src/canonical-json.ts (Command journal): canonicalJson for manifest, marker, payload and key bytes, and jsonObjectEntries / jsonArrayItems for strict shape reads;
    • src/confined-executor.ts (Confined executor): snapshotDigest and type SnapshotFile for SealedCandidate.files and .snapshotDigest;
    • Node builtins only otherwise (child_process, crypto, diagnostics_channel, fs, path, zlib).
  • Used by: in src/, the Producers contract imports its types and REPOSITORY_LIMITS, Repository Assurance imports parseRepositoryIdentity and 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's snapshot as executor programs when handed them, but never calls Repository. Nothing composes it. ConfinedExecutor.run({ input: { files, digest: snapshotDigest } }) consumes an export in test/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 invokeOnce only for a dispatch the Product gate has admitted, after its final current-state read, and pass a guard that repeats that read immediately before the move; record a withheld result as not dispatched, with its claim. Persist RepositoryIdentity and PreparedIntegration, and reconcile with inspectIntegration. Nothing in src/ 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 check before acceptance.

    Tests spawn the real Git at GIT_EXECUTABLE, and they use REPOSITORY_CHANNEL events (initial-objects-written, objects-written, integration-claimed, integration-submitted, integration-acknowledged, integration-withheld) for fault injection and ordering; published is also emitted, but no current test consumes it. Renaming an event breaks the fault tests.

  • 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 stated objects/ limit and a never-released finalisation shape.
  • Receipts: each receipt pins SHA-256 hashes of src/repository.ts and 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) or IDENT;
    • CONFIG, HEAD or TOP_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;
    • unknown is never downgraded to failed or not applied;
    • invokeOnce still 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.

Source: docs/agents/repository.md