facets.json, facets.lock, the , and the delta. It is never partial: a failure anywhere MUST roll back all materialization via the journal — a log in which every materialization write records its inverse operation, replayed in reverse on failure — and leave the project exactly as it was.
Verify
Cache self-audit, lockfile comparison, or registry confirmation. Tampered content never materializes.
Materialize
Assets written to every selected adapter, journaled for rollback.
Commit
facets.json + facets.lock + receipt written together, atomically.Sequence
1
Load state and gate
The manifest is read, the install lock — a per-project advisory lock ensuring only one install operates on a project at a time — is acquired, and the lockfile and receipt are loaded (an invalid receipt is re-bootstrapped from the lockfile; receipt asset entries with crafted names are reported and skipped individually). A delta naming the same facet in both additions and removals MUST be rejected, and the frozen gates run (see Frozen lockfile). Failures here touch nothing — no rollback is needed.
2
Merge delta in memory
Additions are upserted, removals are deleted — all in memory. The on-disk
facets.json MUST NOT be written ahead of the install.3
Per-facet loop
Each facet in the desired set — the manifest’s facets after the delta is merged in memory — passes four sub-stages: parse → resolve → verify → materialize. The loop stops at the first failure.
4
Drift removal
The receipt is compared against the desired set: any facet still on disk but no longer wanted has its assets deleted — entirely offline.
5
Transactional tri-write
The manifest-write policy is applied just before the write, then
facets.json, facets.lock, and the receipt are written together. A failure anywhere rolls back all materialization and leaves all three files unchanged.Registry interactions
Commit makes three distinct kinds of registry interaction, gated independently. The wire protocol behind them — endpoints, request and response shapes — is owned by the registry’s own API specification and is out of scope for this document. Version resolution determines the exact version a specifier refers to (turning0.*, *, latest, or a bare name into a concrete X.Y.Z). Needed only when no exact version is already known — an exact specifier needs none; a satisfying lockfile entry needs none. When it does fire, the metadata response also carries the published fingerprint, so integrity confirmation rides version resolution for free.
Archive resolution downloads the archive bytes for an exact name@version. Needed only on a cache miss — a warm cache skips it entirely, regardless of how the exact version was obtained.
Integrity confirmation verifies the cached or downloaded content against the registry’s published (content_integrity). Needed only when a lockfile entry is being created or replaced — a satisfying lockfile entry serves as the trust anchor instead. An unreachable registry MUST fail the operation rather than write an unconfirmed entry.
These combine independently: a facet may need any combination, or none. The cache short-circuits archive resolution; a satisfying lockfile entry short-circuits integrity confirmation.
Registry versions are immutable — the bytes for a published
name@version never change. Integrity confirmation and lockfile comparison enforce this invariant client-side rather than assuming it.The per-facet loop
Every facet in the desired set passes the same four sub-stages, in order. The loop is interleaved — facet A is resolved and materialized before facet B starts — and stops at the first failure.Parse
The manifest is aname → value map, and each value is re-parsed per facet. For a registry entry the value is a bare version specifier (1.2.3, 1.*, 1.2.*, *, latest) and the facet name lives in the key, so the full source is reconstructed as name@specifier. For git and local entries the value is a self-contained source string (URL, file: path) and the key is just a label.
Resolve
Whether the lockfile is trusted for version resolution depends on how an entry is requested, not on a flag: Implicit version request — the lockfile is NOT trusted for version resolution. A non-exact specifier during anadd (foo-facet, foo-facet@1.*, foo-facet@latest) always triggers resolution to the newest matching version, even when the lockfile already satisfies it. The user explicitly asked for this version or range — the request is honored.
Explicit version request — the lockfile IS trusted when satisfying a version (foo-facet@2.0.1). A satisfying recorded version needs no version resolution; only an absent or stale entry triggers it. The facet was already declared — the locked state is reproduced.
The exact-version gate then decides whether the network fires at all: a satisfying lockfile entry supplies the exact version (no resolution), an exact specifier supplies it directly, and only a non-exact, unlocked specifier resolves fresh against the registry.
Verify
Once a fully-qualified version is in hand, the cache and integrity path is identical regardless of how the version was obtained. The hash domains and guarantees behind these checks are defined in the Integrity Model.A cache hit is never taken at face value. The self-audit always runs on the hit path, followed by exactly one anchor check: lockfile comparison when the lockfile pins this version, integrity confirmation when it does not. The two anchor checks are mutually exclusive.
1
Cache self-audit
Recompute the cached content’s hashes (per-asset and canonical archive) against the (
cache-integrity.json) written at populate time. A failure MUST evict the slot; the chain is retried exactly once as a download. Tampered content MUST NOT be materialized.2
Lockfile comparison
When the lockfile pins this version, the audited integrity MUST equal the locked integrity (string comparison). A mismatch MUST fail the commit — the highest-priority check, never a silent re-download.
3
Integrity confirmation
When no satisfying lockfile entry exists, the audited integrity MUST equal the registry’s published
content_integrity. An unreachable registry MUST fail the commit: a lockfile entry is never written on trust.content_integrity and, when the lockfile pins this version, against the locked integrity, before the verified content populates the cache.
Git and local sources
Git and local facets are built from source during commit rather than downloaded as archives, and each has its own verification contract: Git — the lockfile pins the resolved commit SHA (the ref stays infacets.json); a clone that cannot be resolved to a commit MUST fail the install. A locked entry is served cache-first: the slot is self-audited against its sidecar, and an audited hit that contradicts the locked integrity is the same hard failure as the registry path. On a miss, the clone is pinned to the locked commit, built, and held to a one-check reproduction guard: the built content MUST hash to the locked integrity, or the install fails — the tag-move defense. Registry-style integrity confirmation does not apply to git sources.
Local — trust-by-path: the source is containment-checked, rebuilt from disk on every install, and the lockfile follows what is on disk (no integrity confirmation, no caching). The exception is frozen mode, which promises bit-for-bit reproduction: the built content MUST hash to the locked integrity or the install fails, and the entry is never rewritten.
Materialize
Materialization writes assets from verified content into each selected adapter’s storage. Writes are skip-if-identical — a facet whose lockfile entry is unchanged but whose on-disk assets needed rewriting is reported as repaired (the full outcome taxonomy is documented atfacet install). Every write MUST journal its inverse operation for rollback.
Lockfile shape
Commit writesfacets.lock as part of the tri-write: per facet, the tagged source provenance, the exact resolved version, the verified integrity, and the contributed asset tuples. The full schema is specified in Lockfile.
Machine-local install receipt
Receipts are per-project, machine-local records stored outside the project’s version-controlled tree ($FACET_DIR/receipts/). They track what has been materialized so drift removal can clean up correctly — even when the lockfile no longer mentions the facet.
Per-project isolation
Each project has its own receipt, identified by a hash of the project’s canonical path. Two projects never share a receipt.
Self-identifying
The receipt embeds the canonical path. A mismatch on load MUST fail closed — treated as absent and re-bootstrapped from the lockfile.
Untrusted input
Asset entries with crafted names (path traversal, backslashes) are reported and skipped individually — the rest of the receipt still loads and is processed.
Contained deletion
Deletion goes through adapters using validated asset tuples —
(scope, type, name) triples, the adapter-agnostic identity of a materialized asset — never raw filesystem paths taken from the receipt.Drift removal
After the loop, the receipt is compared against the desired set. Any facet the receipt shows as installed — but that is no longer wanted — has its assets deleted. The comparison runs against the receipt, not the on-disk lockfile; this is what makes orphan-on-pull recoverable (a facet whose lockfile entry vanished because a teammate removed it and you pulled — the receipt still remembers the local materialization). Lockfile entries missing from a freshly-bootstrapped receipt are caught too. And because the asset tuples to delete come from the receipt itself, removal needs no cache and no network. Drift removal runs before the tri-write, but its deletions MUST be journaled like every other write — if the commit fails afterward, rollback restores them.Manifest-write policy
The manifest value written for an addition depends on the specifier shape. The policy is applied in memory just before the tri-write — never earlier:Transactional tri-write
On success, three files MUST be written together: the project manifest, the lockfile, and the receipt. A failure anywhere MUST leave all three exactly as they were.A failed operation leaves the project exactly as it was — no snapshot/restore needed.
Frozen lockfile
Frozen mode (the--frozen-lockfile flag) treats the lockfile as the complete, authoritative source of truth.
The system MUST check bidirectional consistency before touching anything: every manifest facet MUST have a satisfying lockfile entry, the lockfile MUST NOT pin anything the manifest dropped, and git/local sources MUST match their locked provenance.
Not in the install flow
- Text asset resolution — text is in the archive. No install-time fetching.
- Disk layout — determined by adapters, not the specification.
- Server installation —
serversdeclared by a facet SHOULD be surfaced as a warning; nothing is installed for them.