Design decisions¶
Each decision below is a load-bearing design choice that affects multiple parts of the codebase. Together they're the constitutional layer — change one and you have to update the conformance suite. The decisions are frozen in their respective stages; this page is a single index.
The D# identifiers are stable references for use in commit messages, code comments, and other docs; the decision name and its rationale are what matter for the reader.
Frontend / IR¶
D1 — Default store root¶
User-overridable per command (--store-root). Not in the canonical hash — same artifact hashes identically regardless of where the store lives.
D2 — Token-equality drives equality¶
The frontend uses token-level equality for type structural equality (comparing the canonicalised representation, not name-based identifiers). This is what makes "rename a module" a no-op for the canonical derivation.
D3 — No subtyping, no first-class type members¶
The decidability proof of the inheritance calculus requires both. Adding either is rejected by theorem, not by review preference. A feature that seems to require subtyping should be re-classified per the feature-decision procedure — it's almost always a (b) (provably-host) feature in disguise.
D4 — No imports; whole-tree composition¶
PCL programs compose all *.pcl files in scope into one namespace. No import keyword. Conflicting top-level module names are [E3] (located).
D5 — Recipe identity is in the canonical hash¶
canonical.recipe = { name, source_hash } — the source_hash is the content hash of the recipe's .py + sibling files. Editing recipes_lib/std/autotools.py::_postinstall.sh ⇒ every package using std.autotools gets a new store path ⇒ rebuild on next punix build.
This is the cache-staleness fix the v2 architecture makes structural. The bug it kills: pre-Stage-3 builds cached "across" recipe edits (you edited the recipe but old artifacts still resolved).
Realise¶
D6 — Sandbox is bubblewrap if available, else fall-through¶
The realise layer's sandbox is BubblewrapSandbox when bwrap is on PATH (Linux); SimpleSandbox (chroot-and-rebind) is the default. The choice is automatic; the recipe contract is identical.
D7 — Conformance + corpus is the success bar, not package count¶
A "one-tier 200+ package" pitch is marketing. v2's success bar is core ≈ 20 packages green on both archs + the demos deploy. The community corpus is best-effort; not a release gate.
Deploy¶
D8 — gen-NNN.json is the rollback contract¶
Generations carry everything rollback needs:
store_paths(the pinned closure),services(with environment as refs, not values — hash-exclusion),config_files(path + sha256),source_hash(the SHA-256 of the PCL bytes at deploy time),backend,deployed_at,generation,stack_name.
Rollback never re-evaluates. O(1) is the property.
D9 — Single os.replace of current is the only state-switch¶
Every deploy mutates exactly one observable piece of state: the current symlink. That mutation is one Transport.atomic_symlink_switch call. Crash at any earlier point ⇒ previous gen wholly live.
Reviewer rule: a PR that adds a second mutable state-switch point — even one — breaks D9. The "kill mid-deploy" conformance test monkeypatches the atomic primitive and asserts current survives. The test catches the violation; the rule above pre-empts it.
D10 — --target ssh://user@host[:port] URL scheme¶
Chosen over --ssh-host / --ssh-port separately. The --key flag is separate (not embedded in the URL) so the same --target value works whether you're using key auth, ssh-agent, or a different key per environment.
D11 — Closure-push idempotency via transport.exists(p)¶
A push is if not transport.exists(p): transport.push(p, p). For a content-addressed store path, existence on the target IS correctness — same hash-name ⇒ same bytes by construction. No per-file checksum needed. See Concepts: transport.
D12 — Environment= in unit, default¶
Secrets land in Environment=KEY=VALUE lines inside the unit file (0644 by default). The cleaner alternative is EnvironmentFile= <sidecar> (0600), at the cost of one extra file per stack. D12 default is Environment=; re-evaluate before the v2.0 evidence run.
D13 — Reject \n / " in env values¶
Values containing raw newlines or unescapable double-quotes are rejected by the systemd generator with a located message. The operator base64-encodes (or otherwise encodes) at the source. The alternative — escape cleverly — has been a bug-source in every config system that tried it.
D14 — Cross-arch deploys fail clearly¶
When the deploy host's arch doesn't match the target's, the closure has been built for the wrong arch. v0.6 explicitly does NOT solve this — the deploy fails before pushing with an arch-mismatch error. The clean fix is Stage 8's RealiseRemote(transport, system) (build on the target).
When a decision changes¶
Most are frozen. A handful are revisitable:
- D12 — opt-in
EnvironmentFile=flag for v0.7+ deploys. - D6 — eventually a third sandbox (
Docker-based)? - D14 — Stage 8 lifts to
RealiseRemote.
The rest are foundational. D3 specifically is never lifted — the decidability proof would no longer go through.
How they're enforced¶
Every decision above has at least one test in the conformance suite that would fail if the decision were silently reversed. The D# identifiers themselves serve as stable references in commit messages and code comments.
Related¶
- Concepts: Eval/realise seam — the design philosophy these decisions implement.
- Conformance — the tests that enforce them.