Invariants¶
These are the rules every PR has to respect. They're not "best practices" — each is enforced by at least one conformance test, and breaking one means the PR is rejected (or the test stops being green).
The eval/realise seam¶
The pure layer performs no effects. No subprocess, no open(…, "w"), no network read, no env read, no time read. The frontend, the IR, the evaluator: all pure.
- Enforced by: structural — every effect goes through
Transport,Store, oros.environ, and those imports only appear below the seam. - The conformance pin: hermetic tests (
LocalTransport(tmp_path)) run real code paths against sandbox bytes. If a pure-layer module ever calledsubprocess, those tests would have to mock it; the "no subprocess mocks" rule catches it.
The pure layer is decidable. A new feature cannot break the finite-tree-→-structural-recursion property of the type checker.
- Enforced by: review (no subtyping, no first-class type members, no untyped escape hatches).
- The conformance pin:
punix checkis called over a 200-package corpus in the test matrix; if termination breaks, CI hangs.
The atomic-flip primitive (single mutable state-switch)¶
Every deploy mutates exactly one observable piece of state: the current symlink, via exactly one Transport.atomic_symlink_switch call.
- Enforced by: review + a conformance test that monkeypatches
atomic_symlink_switchto raise and verifiescurrentis unchanged. - PR-time rule: if you're adding a new place where
current(or any similar live-state pointer) mutates, you're breaking this invariant. The right move is to fold the new operation INTO the existing primitive, or push it pre-flip (into the write-staging phase, before the symlink switch).
Content-addressing must include recipe identity¶
The canonical-derivation hash includes the recipe identity — that means the recipe's name PLUS a content-hash of its source files (autotools.py, _postinstall.sh, _dep_env.py, etc.).
- Enforced by: a conformance test that edits a recipe hook body and asserts the dependent packages' store paths change.
- PR-time rule: if you're adding a new recipe, its source files MUST be hashed into the canonical derivation. The
recipes_lib/std/*.pyfiles already are; user-defined recipes must be too.
Secret values never reach the pure layer¶
SecretRef lowers to IrSecret; IrSecret serialises as a reference string (from_env:NAME), never the value. The pure layer literally has no access to the resolved value.
- Enforced by: structural — the lowerer maps
SecretRef→IrSecret, andIrSecret.canonical()emits only the reference string. - The conformance pin: two deploys with different env values for the same
from_envreference yield byte-identical store paths. - PR-time rule: if you're adding a new way for the pure layer to interact with secrets, you're violating this. The right place for the interaction is at the deploy seam, in
secrets.resolve_secrets.
Conformance suite is a release gate¶
A regression in any conformance test blocks the release — not the PR, the release. There is no "we'll fix this in a follow-up"; the fix lands in the SAME PR.
- Enforced by: CI on every push and PR.
- PR-time rule: if a conformance test fails, fix the code before asking for review. If the property is genuinely wrong (the requirement is incorrect, not the code), update the test in the same PR and call out the change in the PR description.
"No subprocess mocks" in deploy-layer tests¶
Tests that exercise the deploy layer use real transports, not mocks of subprocess.run.
- Enforced by: review — there is no
subprocess.runpatch in any Transport-layer test.LocalTransport(tmp_path)is the hermetic fixture;localhost_sshdis the SSH fixture (auto- skips when sshd / ssh-keygen aren't onPATH). - PR-time rule: if you find yourself reaching for
monkeypatch.setattr("subprocess.run", ...)in a deploy test, stop and re-route throughTransport. The point of the Protocol is that no test ever needs to.
"Strict host-key checking" stays on, even in tests¶
SshTransport always passes -o StrictHostKeyChecking=yes. Tests opt into the strict-mode flow by pinning a known-hosts file the fixture controls — not by disabling the check.
- Enforced by: review + the SSH conformance fixture (
tests/c_e2e/conftest.py::localhost_sshd) pre-populates aknown_hostsrather than skipping verification. - PR-time rule: never default
-o StrictHostKeyChecking=noor=accept-new. The fixture'sknown_hostsis the right pattern for any new SSH-based test.
Closure-push contract is content-addressed¶
if not transport.exists(p): transport.push(p, p) — that's the entire push primitive. For a content-addressed path, existence IS correctness; no per-file checksum needed.
- Enforced by: structural —
_push_store_closurehas no per-file checksum code. - PR-time rule: don't add a "verify the remote bytes match" step. That would break content-addressing's "same hash ⇒ same bytes by construction" guarantee. If you think you need to verify, the bug is upstream: a wrong hash got into a
gen-NNN.json.
Generations are FIFO-trimmed but current is never collected¶
Manifest.trim_generations(stack, keep=10) removes oldest first but never removes the currently-live generation.
- Enforced by: a test that holds
currentat gen-001, deploys nine more, calls trim, and asserts gen-001 is still on disk. - PR-time rule: if you're adding a new generation-removal code path, it must read
current_generation_number(stack)first and refuse to remove that gen.
Related¶
- Architecture — the structural overview.
- Feature-decision procedure — what to apply before writing the code these invariants will check.
- Reference: decisions — the constitutional layer; each decision implies at least one invariant above.
- Reference: conformance — the tests that enforce these.