Quartz Guard — Source-Only Build Verification
Problem
Quartz is self-hosted: the committed binary at every commit was built from that commit’s source by a previous binary, which may have had compiled-in definitions that no longer exist in the current source. When references to those definitions survive in the source but the definitions are gone, the source becomes fossilized — it only builds if you use a pre-existing binary that still remembers the ghost. A pure source-only build (start from a known-good gen0, compile every forward commit in order) will fail at the fossil commit and every commit downstream of it.
This is exactly what the 2026-03 → 2026-04 Linux bootstrap walk exposed. Multiple fossils were discovered and have since been fully resolved (verified Apr 12, 2026):
| Fossil | Introduced | Status (Apr 12, 2026) |
|---|---|---|
ast_func_is_cconv_c | 015b0305 (Apr 3) | ✅ Stub at ast.qz:1489 (returns 0, intentional) |
TYPE_MAP constant | 97d088ce (Apr 6) | ✅ Defined at type_constants.qz:90 |
NODE_IS_CHECK constant | ~Apr 6 | ✅ Defined at node_constants.qz:257 |
tc_type_is_hashable / tc_struct_is_hashable / tc_hashable_rejection_reason / tc_struct_hashable_rejection | Apr 6 | ✅ Full impls in typecheck_helpers.qz:527-596 |
mir_register_poll_callee | 015b0305 | ✅ Full impl at mir.qz:62 |
mir_func_set_cconv_c / mir_func_is_cconv_c | 015b0305 | ✅ Full impl at mir.qz:1156-1168 |
mir_block_set_switch / mir_block_get_switch_values / mir_block_get_switch_targets | Pre-54eb4965 | ✅ Full impl at mir.qz:1479-1511 |
cg_emit_struct_hash_eq_functions / cg_emit_custom_map_functions | Pre-54eb4965 | ✅ Full impls in codegen_runtime.qz:6351/6499 |
Source-only build verified: gen1 (1,679,869 lines IR) == gen2 byte-identical. The standing patch set has been retired — no patches are required for a clean source-only build from HEAD.
Each fossil originally cost hours of bootstrap walking to identify and patch around. The resolution ensures this class of problem cannot recur undetected.
The Guard — invariant to maintain
For every commit on
trunk, compilingself-hosted/quartz.qzfrom source using the previous commit’s binary (plus any required source-level patches that are intentional forward steps) must succeed.
The “plus patches” escape hatch is for legitimate cases where a commit introduces new syntax or builtins — the patch set at that commit would be the minimal diff a user would apply to cross-compile the feature. The important property: no dangling references to definitions that don’t exist anywhere in the source tree.
Implementation — three phases
Phase 1 — pre-commit local check (fast, per-commit)
Before committing, run:
./self-hosted/bin/quake guard:local
This task (to be added to Quakefile.qz) runs:
git stashthe working treegit checkout HEAD~1 -- self-hosted/bin/quartz self-hosted/bin/quake(use the previous commit’s binary)git stash pop- Run
./self-hosted/bin/quake build(the previous binary compiling the current source) - If it fails, report which symbols couldn’t be resolved and abort the commit
Budget: ~30 seconds (debug build). Acceptable as a pre-commit hook for the ~20 commits
per day on active feature work. For commits that intentionally introduce a new feature
with a chicken-and-egg gap, the developer adds a guard-patch: line to the commit trailer
pointing at a .quartz-guard/NNN-patch.sh script that the guard runs before the build.
Phase 2 — CI forward-rebuild (slow, per-PR)
On every PR, run guard:forward:
- Check out the merge base with
trunk - Use that commit’s committed binary as gen0
- For each commit in the PR, in order:
- Apply any
.quartz-guard/NNN-patch.shscripts present in the diff - Build from source using the previous gen binary
- Run
quake fixpointon the last commit
- Apply any
- Report the first commit (if any) where source-only build fails
Budget: 5-15 minutes depending on PR size. GitHub Actions, triggered on push to any PR branch.
Phase 3 — CI historical sweep (weekly, full history)
Weekly job that walks the last 30 days of commits on trunk source-only, starting from
a known-good binary 30 days back. Catches fossils that made it past both Phase 1 and
Phase 2 (e.g., cross-commit interactions where two separately-valid commits combine
into a fossil). On failure, the CI opens an issue on GitHub with the commit range and
the missing symbol.
Budget: ~2 hours per run. Once a week on Sunday night.
The “Guard” binary — bootstrap-quality compiler
A second committed binary at self-hosted/bin/quartz-guard that is specifically
built and maintained to be fixpoint-stable from the minimal fossil-free source
tree. Its job is only to serve as the gen0 for source-only rebuilds. It doesn’t
need to be fast, doesn’t need mimalloc, doesn’t need the latest optimizations — it
just needs to faithfully compile the source.
Whenever a fossil is introduced and then resolved, the guard binary is rebuilt from the post-resolution commit so Phase 2 has a clean starting point moving forward.
Patch-set protocol for intentional chicken-and-egg commits
When a commit introduces a feature where the new source can’t build with the previous
binary (e.g., 97d088ce introduced map_new registration AND the intrinsic registry
initializer using map_new() at the same time), the commit author provides a
.quartz-guard/<commit-hash>-patch.sh script that:
- Mutates the new source just enough to make it buildable with the previous binary
- Documents why (what compiled-in state the patch is substituting for)
- The Phase 2 CI applies the patch, builds, and the resulting binary is what’s used for the NEXT commit in the sequence
The patch file is committed alongside the code change. Once a second commit lands that moves past the chicken-and-egg, the patch file can be deleted.
Known patch set — RETIRED (Apr 12, 2026)
All permanent fossils have been resolved in source. The standing patch set is no longer needed. For historical reference, the patches that were required before resolution:
—standing/01-strip-cconv-c.shast_func_is_cconv_cnow has a stub definition—standing/02-type-map-const.shTYPE_MAPnow defined intype_constants.qz—standing/03-node-is-check-const.shNODE_IS_CHECKnow defined innode_constants.qz—standing/04-hashable-stub.shtc_type_is_hashableand friends now have full implementations— no longer referenced as a fossilstanding/05-tc-all-builtin-names.sh
The source tree is now source-only-buildable by any prior binary without patches.
Relationship to the Binary Backup Protocol
The existing Binary Backup Protocol (CLAUDE.md) protects against broken builds destroying the committed binary. The Quartz Guard protects against fossils making the source tree unbootstrappable even when the committed binary is fine. They’re orthogonal:
- Binary Backup Protocol: “NEVER overwrite
quartz-goldenuntil fixpoint is verified” - Quartz Guard: “NEVER let source reference a symbol that doesn’t exist in source”
Both must hold simultaneously for the language to remain in a state where any developer can cross-compile from scratch without a pre-existing binary.
Golden binary backups from the Linux bootstrap walk
self-hosted/bin/backups/ contains the checkpoint binaries from the Linux bootstrap
walk that rediscovered these fossils. Each is a Linux x86_64 ELF, fixpoint-verified
at its commit:
quartz-linux-x64-<commit>-golden # the compiler
quake-linux-x64-<commit>-golden # the build tool
Walk order (forward in time):
bce5e646 → bbded285 → 27b133a1 → ac0ef57c → d12ea4b6 → 780a0079
→ 015b0305 → 32fa81b3 → 97d088ce → aef2610b → 0c73a2fc
→ 54eb4965 → afad28c0 → 7ba5fa12 (HEAD)
Any of these can serve as the gen0 for a future Linux bootstrap without mimalloc and without the cache-pattern codegen bug described in HANDOFF_LINUX_BOOTSTRAP.md.
Future work
Fix permanent fossils in source— ✅ DONE (Apr 12, 2026). All 11 fossils resolved.Add— ✅ DONE. Automated clean-room source-only build + fixpoint verification + 3 smoke tests (brainfuck, expr_eval, style_demo).quake guard:sourcetoQuakefile.qz- Add
quake guard:forward(per-PR CI) andquake guard:history(weekly sweep) - Enable Phase 1 as a pre-commit hook via
.claude/settings.jsonhooks - Enable Phase 2 in GitHub Actions via
.github/workflows/quartz-guard.yml - Fix the
as_int(string)/as_string(int)round-trip codegen defect that caused the final Linux bootstrap blocker — this is a real runtime bug, not a source fossil