Batch C — Implementation Plan
Branch target: trunk Baseline: b61da0f4 (Batch A+B complete, fixpoint 2281, smoke 4/4 + 22/22)
Sprint size: 5 commits. Strict order: C1 → C2 → C3 → C4 → C5.
Protocol: quake guard + quake smoke + per-item regression sweep before each commit. No --no-verify, no skipping fixpoint, no dropping the fix-specific backup until the commit lands.
Context
The Batch A+B sprint (Apr 14, 2026) landed 9 of 9 items across two sessions. Five follow-ups were filed in docs/ROADMAP.md during execution — three for real bugs discovered during sprint work, two for polish/guardrails the sprint explicitly deferred. This batch clears those five items in one overnight run, following the exact same execution pattern as A+B:
- Discrete, well-scoped commits
- Per-commit
quake guard+ smoke + regression sweep - Honest reporting (regression-lock + ROADMAP filing if a fix doesn’t land)
- Fix-specific binary backups before each item
Items in strict execution order (easiest → hardest):
| # | Item | Est | Risk | Has reproducer? |
|---|---|---|---|---|
| C1 | QUAKE-STDERR-AUDIT — migrate 9 remaining 2>/dev/null sites to sh_buffered | 1h | Low (mechanical) | N/A (polish) |
| C2 | B3-DIRECT-INDEX-FIELD — va[0].y direct access produces QZ0301: Unknown struct: Struct, Int | 1–2h | Low-med | Yes, in pending test |
| C3 | IMPL-NOT-SEND — impl !Send for X explicit opt-out | 2–3h | Medium (parser change) | N/A (new feature) |
| C4 | POST-RESOLVE-IDENT-ASSERTION — promote unresolved NODE_IDENT at MIR entry to QZ9501 hard error | 1–2h | Medium (may false-positive) | N/A (guardrail) |
| C5 | B4-UNWRAP-IN-LOOP — step! in while loop miscompile (see separate handoff) | 2–4h | High (2 failed attempts already) | Yes, minimal 11-line repro |
Total estimate: ~8–12 quartz-hours. Fits one overnight run. Ordering is easiest-first so that even if C5 consumes its full budget without landing, 4 commits still reach trunk.
Prime directives (non-negotiable, from CLAUDE.md)
- Pick highest-impact, not easiest — but respect the sequencing. Easy-first here is for commit safety, not value ordering.
- Design before building; research first — each item has a fix sketch below. Verify each sketch against the current codebase before editing.
- Pragmatism ≠ cowardice; shortcuts = cowardice — if a fix turns out bigger than planned, follow the A2/B3/B4 pattern: regression-lock what works, file the deeper gap in ROADMAP, move on.
- Work spans sessions — context budget isn’t infinite. If C5 alone exceeds 4h, stop, commit what’s done, file the rest.
- Report reality, not optimism — “should work” without verification is a lie. Every commit runs the full per-item verification before shipping.
- Holes get filled or filed — every gap discovered during execution goes into ROADMAP immediately.
- Delete freely, no compat layers — zero users, zero compat shims.
- Binary discipline = source discipline —
quake guardmandatory, fix-specific backups mandatory. - Quartz-time estimation — traditional ÷ 4.
- Corrections are calibration, not conflict — no rationalizing past a failure. If you can’t fix it, file it.
Cross-cutting findings
-
Follow the A2/B3/B4 precedent for
rm -rf .quartz-cachediscipline. Every item starts with:cp self-hosted/bin/quartz self-hosted/bin/backups/quartz-pre-cN-golden && rm -rf .quartz-cache. The incremental cache can hold stale MIR/IR from before a fix landed and produce false reproductions. Three items in Batch A+B (A2, B3, maybe B4) were originally misdiagnosed because of cache staleness. -
C3 (IMPL-NOT-SEND) and C4 (POST-RESOLVE-IDENT-ASSERTION) both touch the typechecker registry at similar points. If you land C3 first, C4’s assertion infrastructure may interact with the new
neg_implspath. Verify C3 passes the full regression sweep before starting C4. -
C1 (QUAKE-STDERR-AUDIT) requires a launcher rebuild, not a compiler rebuild.
std/quake.qzalready hassh_bufferedfrom A4. The migration is inQuakefile.qzwhich is compiled fresh by the launcher each run. You do NOT needquake guardfor C1 — just verifyquake buildandquake smokework after the edit. But DO run guard at the end of C1 for fixpoint discipline. -
B4 has its own detailed handoff at
docs/handoff/b4-unwrap-in-loop-handoff.md. That doc has the reproducer, the IR analysis, the 2 failed fix attempts, and a 4-phase investigation plan. Treat it as the authoritative source for C5 — this plan just references it.
Per-item design
C1 — QUAKE-STDERR-AUDIT: migrate 2>/dev/null sites in Quakefile.qz
Goal. Every build/link site in Quakefile.qz uses the A4-introduced sh_buffered(cmd, step_name) helper from std/quake.qz instead of 2>/dev/null. On failure, users see the real tool stderr instead of silently failing or getting the old “llc not found” false positive.
Sites to migrate (from the Explore audit during A+B planning):
Quakefile.qz:504-505— Gen1 build’s llc + link commandsQuakefile.qz:849— guard task’s llc invocationQuakefile.qz:935— guard:source task’s llc invocationQuakefile.qz:1148-1157— build task’s final link (both-gand non--gpaths, both2>/dev/nulland2>>tmp/build.err)Quakefile.qz:1260— build:separate task’s link commandQuakefile.qz:1301— compile_qz_tool helper’s link command
Fix pattern. Before:
sh("#{_linker()} -g #{_stack_flag()} tmp/gen1.o -o tmp/gen1#{_mimalloc_flags()} 2>/dev/null")
After:
var rc = sh_buffered("#{_linker()} -g #{_stack_flag()} tmp/gen1.o -o tmp/gen1#{_mimalloc_flags()}", "link-gen1")
if rc != 0
fail("link-gen1 failed")
end
Note: sh_buffered already prints the command + captured stderr on failure, so the fail() wrapper only needs to abort the build. It does NOT need to re-print the error.
Step names should be short and descriptive: llc-gen1, link-gen1, llc-guard, link-guard, llc-build, link-build, link-build-separate, link-compile-tool, etc. These show up in failure messages.
Test plan. No new spec file. Manual probe: inject a puts("DEBUG_LEAK") into a compile-time path of the compiler, run quake build, verify the error message surfaces the real llc rejection instead of “llc failed”. Revert the injection.
Tensions. None — this is pure polish. The A4 helper does the work; this commit just exercises it.
Steps.
- Read the 9 sites in
Quakefile.qzto confirm they still match the line numbers above (file may have drifted since the Explore audit). - Migrate each site one at a time. Don’t batch — each edit should be followed by
quake buildto confirm the Quakefile still parses. - After all 9 sites migrate: inject a debug
putsprobe into the compiler source, runquake build, verify the error message improved, revert the probe. rm -rf .quartz-cache && cp self-hosted/bin/quartz self-hosted/bin/backups/quartz-pre-c1-golden.quake guard— fixpoint should still verify at 2281 (no compiler source changed).quake smoke— brainfuck 4/4 + expr_eval 22/22.- Update ROADMAP row
QUAKE-STDERR-AUDITto RESOLVED. - Commit:
C1: Migrate Quakefile.qz build/link sites to sh_buffered.
Complexity. S (1h). Risk. Low — mechanical, sh_buffered is already tested in A4.
C2 — B3-DIRECT-INDEX-FIELD: va[0].y direct access resolves to wrong struct
Goal. var va: Vec<VA> = vec_new(); va.push(VA{x:10, y:42}); va[0].y == 42 compiles and returns 42 without requiring an intermediate var a = va[0] binding. The pending test direct vec[i].field access reads correct offset in spec/qspec/vec_element_type_spec.qz flips from it_pending to it and passes.
Root cause hypothesis (from the B3 investigation in Session 2):
The error is QZ0301: Unknown struct: Struct, Int. Critical observation: “Struct, Int” is a comma-joined pair of type names from tc_type_name() — “Struct” (TYPE_STRUCT) + “Int” (TYPE_INT). This string shape matches the output of tc_infer_type_param_mapping at typecheck_generics.qz:387-402 where it builds a comma-separated result string from tc_type_name(field_types_vec[i]).
Hypothesis: va[0] is going through a generic UFCS rewrite (maybe va.get(0) → Vec$get$2(va, 0) via the intrinsic dispatch). The dispatch machinery uses tc_infer_type_param_mapping to build the generic concrete-type string, and one of the field type IDs returns TYPE_STRUCT with no attached name — so it stringifies to the bare word “Struct”. The “Int” part comes from the index type. The resulting “Struct, Int” string then gets passed to tc_check_field_access as the struct name, which naturally fails to look up.
Investigation starting points.
-
First, verify the pending test still reproduces on current trunk after
rm -rf .quartz-cache. If it’s already fixed (unlikely but possible), flip theit_pendingtoitand commit as a pure regression-lock update. -
Add a debug print in
tc_check_field_accessattypecheck_generics.qz:520:eputs("[DBG-C2] tc_check_field_access struct='#{struct_name}' field='#{field_name}'\n")Rebuild, run the failing test, see what
struct_nameactually is. Confirms the “Struct, Int” hypothesis. -
Trace the caller. Grep for
tc_check_field_accesscallers intypecheck_expr_handlers.qzandtypecheck_generics.qz. The two callers are lines 1133 and 1311. The struct_name is built bytc_resolve_expr_struct_name(line 485) which callstc_infer_expr_type_annotation(line 162). Add debug prints at each return point. Find where “Struct, Int” gets assembled. -
Likely fix site:
tc_find_field_type_name_for_paramattypecheck_generics.qz:703— already has a fallback totc_type_name(field_types_vec[i])that returns “Struct” for TYPE_STRUCT. If this helper is what’s producing the broken name, fix it to calltc_infer_expr_type_annotationon the field initializer AST node (same pattern as theSTRUCT-GENERIC-ARG-INFERENCEfix did for the struct-init case at line 710-715).OR: the fix may be higher up in
tc_infer_expr_type_annotation’s NODE_INDEX branch at line 278-290. Check whether it loses the Vec’s element struct name when the result is immediately field-accessed (vs. bound to a var, which works).
Fix sketch (tentative — verify against investigation). In tc_resolve_expr_struct_name for NODE_INDEX receivers, thread the Vec’s element type through via tc_ptype_name(tc, array_type) the same way tc_expr_index at line 443-445 already does. The element struct name is known at the NODE_INDEX typecheck point; it just needs to be preserved in the returned annotation.
Test plan. The pending test already exists at spec/qspec/vec_element_type_spec.qz test 5 (“vec[i].field direct access reads correct offset”). Flip it_pending → it once the fix lands. Add 1-2 more variants:
vec[i].method()— direct method call without intermediate binding- Nested:
va[i].inner.field— field chain through indexed element
Tensions. The tc_resolve_expr_struct_name path is shared with other code — changing its NODE_INDEX handling could affect other field-access call sites. Run the full B-sprint regression set (impl_trait + abstract_trait + traits + hybrid_ufcs_traits + collection_stubs + arity_overload + match_unknown_variant + iterable_trait + iterator_protocol + enum_ctor_in_impl + closure_intrinsic_shadow + send_auto_trait + vec_element_type + unwrap_in_loop + sched_lifecycle) after the fix.
Steps.
cp self-hosted/bin/quartz self-hosted/bin/backups/quartz-pre-c2-golden && rm -rf .quartz-cache.- Re-run the pending test case manually (extract it from the spec file to
/tmp/c2_repro.qz). Verify it still fails withQZ0301: Unknown struct: Struct, Int. - Add debug prints to pin the actual “Struct, Int” string source.
- Apply fix, rebuild, verify reproducer passes, revert debug prints.
- Flip
it_pending→itinspec/qspec/vec_element_type_spec.qztest 5. Add 1-2 more variants. - Rebuild, run the spec, verify all tests pass.
quake guard→ smoke → full B-sprint regression sweep.- Update ROADMAP row
B3-DIRECT-INDEX-FIELDto RESOLVED with commit SHA. - Commit:
C2: Fix B3-DIRECT-INDEX-FIELD — vec[i].field reads correct struct offset.
Complexity. M (1–2h). Risk. Low-medium — the investigation path is clear, the fix site is probably one of two known functions, but the “Struct, Int” error string might route through an unexpected call path.
C3 — IMPL-NOT-SEND: explicit impl !Send for X opt-out
Goal. Users can write impl !Send for X end to mark a type as NOT Send even when structural field recursion would auto-derive it. The negative impl always wins — tc_type_is_send consults the neg_impls set FIRST, returns 0 if the type is negatively marked, THEN runs the structural walker. Same semantics for !Sync.
Design.
- Parser:
impl !Send for Counter end—!token afterimplkeyword, followed by trait name. Parse asNODE_IMPL_BLOCKwith a newis_negativeflag (new AST slot or repurposeint_val). - Typecheck registry: new
tc_registry.neg_impls: Vec<Vec<String>>— each entry is[trait_name, for_type]. Populated duringtc_register_impl_blockwhen the negative flag is set. tc_type_is_send(tc, type_id, struct_name): early-return 0 if(Send, struct_name)is inneg_impls. Same fortc_type_is_sync.- Same early-return check in
tc_check_trait_boundfor the Send/Sync fallthrough path — a negatively-marked type should fail a bound check regardless of structural analysis.
Test plan. New spec spec/qspec/impl_not_send_spec.qz with 5 tests:
struct S { n: Int } impl !Send for S end+def accept<T: Send>(v: T): Int = 0+accept(S{n:1})→ compile errorQZ0200: Type 'S' does not implement trait 'Send'(or similar, whatever B1 produces for negative cases).- Same but without
impl !Send→ compiles (auto-derivation works per B1). impl !Sync for S end— Sync analog.- Both
!SendandSendfor the same type → parser or typecheck error (contradictory). impl !Send for S end; impl Send for S end→ contradictory, error.
Parser changes. In self-hosted/frontend/parser.qz, find the impl block parse (search for NODE_IMPL_BLOCK or ast_impl_block). Insert optional ! handling after the impl keyword:
# After TOK_IMPL:
var is_negative = 0
if ps_check(ps, TOK_BANG)
ps_advance(ps)
is_negative = 1
end
# ...parse trait name...
Pass is_negative through to ast_impl_block (add a new argument or use int_val slot).
Typecheck changes in self-hosted/middle/typecheck.qz’s tc_register_impl_block (line ~2895):
if ast_get_int_val(ast_storage, node) == 1 # negative flag
# Register as negative impl — bypasses method registration
var entry = vec_new()
entry.push(trait_name)
entry.push(for_type)
tc.registry.neg_impls.push(entry)
return # skip the method registration below
end
In tc_type_is_send at typecheck_registry.qz:2746:
# Check negative impl first
if str_byte_len(struct_name) > 0
if tc_lookup_neg_impl(tc, "Send", struct_name) >= 0
return 0
end
end
# ... existing structural walk ...
Add the tc_lookup_neg_impl helper next to tc_lookup_impl.
Tensions.
- Parser ambiguity:
!is already used as postfix unwrap and as the logical NOT unary prefix. Inimpl !TraitNameposition, the!is unambiguous becauseimplis a statement-level keyword and what follows is a type/trait expression, not a value expression. Still, verify the lexer treats!in this position as TOK_BANG and not as something else. - Recursive negative impls: what if a type has
impl !Send for Aand A contains a fieldBwhich hasimpl Send for B end? A should still be negative — the negative impl on A wins over any structural or positive analysis. This is the whole point of the feature.
Steps.
cp self-hosted/bin/quartz self-hosted/bin/backups/quartz-pre-c3-golden && rm -rf .quartz-cache.- Write the 5 test cases in
spec/qspec/impl_not_send_spec.qz— 4 positive (Send and Sync, with and without negative impl), 1 negative (contradictory impls). Confirm test 1 currently fails with “compile error expected but got success” (because B1 auto-derives Send from Int fields). - Parser: add
!handling afterimplkeyword. Useint_valslot onNODE_IMPL_BLOCKto record the negative flag (check first that int_val isn’t already used for something else on impl blocks — if it is, add a new AST slot). - Registry: add
neg_impls: Vec<Vec<String>>field totc_registryintypecheck_util.qz(find the registry struct definition). Initialize to empty vec intc_new(). tc_register_impl_block: short-circuit when negative, push toneg_impls.- Add
tc_lookup_neg_impl(tc, trait_name, concrete_type): Inthelper. Returns index if found, -1 if not. tc_type_is_send/tc_type_is_sync: consulttc_lookup_neg_implFIRST, return 0 if found.- Rebuild, run the new spec, verify all 5 tests pass.
quake guard→ smoke → full regression sweep (especiallysend_auto_trait_spec— make sure B1’s positive derivation still works for types WITHOUT a negative impl).- Update ROADMAP row
IMPL-NOT-SENDto RESOLVED with commit SHA. - Commit:
C3: IMPL-NOT-SEND — explicit impl !Send / !Sync opt-out.
Complexity. M (2–3h). Risk. Medium — parser change always carries regression risk; full QSpec sweep is advised but the focused regression set should catch obvious breakage.
C4 — POST-RESOLVE-IDENT-ASSERTION: hard error on unresolved NODE_IDENT at MIR entry
Goal. Any NODE_IDENT reaching MIR lowering without a resolved binding produces a hard QZ9501: internal compiler error: identifier 'X' reached codegen unresolved instead of silently emitting %X undef IR that later llc rejects. Guardrail against the class of bug OPTION-CTOR-IN-IMPL-BODY was in — where a resolver rib omission produced silent codegen garbage.
Design.
- New helper in
self-hosted/backend/mir_lower.qz:mir_assert_ident_resolved(ctx, name, line, col): Void. - Called at the top of the NODE_IDENT dispatch in
mir_lower_expr(line ~1279). - Assertion logic: try
mir_ctx_lookup_var(ctx, name),mir_lookup_function(ctx, name), and the arity-mangled variants. If ALL return “not found” AND the name is not a recognized constant / global / intrinsic, emitQZ9501via the diagnostic module and return TYPE_INT to avoid cascading errors. - New error code
QZ9501— “Internal compiler error: identifierXreached codegen unresolved. This indicates a resolver scope-push omission. Please file an issue.” - Explain entry in
self-hosted/error/explain.qz(or wherever explain entries live).
Risk — the reason this wasn’t done in A2. A2’s plan originally scoped this assertion but I deliberately DIDN’T implement it because OPTION-CTOR-IN-IMPL-BODY was already fixed on trunk and there was no failing test to justify speculative infrastructure (per Directive 5). C4’s justification is different: it’s a prophylactic guardrail against a whole class of future bugs, not a fix for a current bug. The risk is FALSE POSITIVES — the assertion may fire on legitimate code paths where the resolver defers a binding decision:
- Closure captures discovered late — when a lambda body references an outer variable, the capture walker may add it to the capture set at a later phase than initial typecheck. At MIR lowering time, is the NODE_IDENT resolved yet?
- Open-UFCS receivers —
v.map(f)wheremapis a free function in scope, not a method. The NODE_IDENT formapmay be treated as unresolved at the initial walk and resolved later via UFCS rewrite. - Macro-expanded gensyms —
$unwrapgenerates__macro_N__names whose bindings come from match pattern destructuring, not scope. Are they “resolved” at MIR entry?
Mitigation: run the FULL QSpec sweep (not just the focused regression set) before committing. If the assertion fires on any currently-passing test, the assertion logic needs refinement — maybe it should check additional fallback paths, or maybe it should warn instead of error for specific known-safe patterns.
Test plan. Two phases:
- Negative test (positive for the guardrail): synthesize an AST that produces an unresolved NODE_IDENT by manually constructing a
.qzinput that would have tripped the old OPTION-CTOR-IN-IMPL-BODY pattern. The assertion should fire withQZ9501. Add asspec/qspec/post_resolve_assertion_spec.qzwithassert_compile_error(src, "QZ9501"). - Regression sweep: run the full existing QSpec suite, confirm zero new failures. Budget a few hours for debugging any false positives.
Steps.
cp self-hosted/bin/quartz self-hosted/bin/backups/quartz-pre-c4-golden && rm -rf .quartz-cache.- Read
self-hosted/backend/mir_lower.qz:1279-1302(current NODE_IDENT dispatch) to understand all the resolution paths it already tries. Don’t duplicate work — the assertion should fire only when ALL existing lookups would fail. - Read
self-hosted/error/explain.qz(or the explain registry) to understand how to registerQZ9501. - Add
mir_assert_ident_resolvedhelper. Call it at the top of the NODE_IDENT handler. - Register
QZ9501explain entry. - Write 1 positive test: the synthesized unresolved NODE_IDENT should fire QZ9501.
- Rebuild, run positive test, confirm it fires QZ9501.
- Run focused regression sweep (B-sprint set). Budget: if ANY test fails because of a false positive, revert and investigate. The assertion logic must be tightened until all existing tests pass.
- If focused sweep is clean, run the full QSpec sweep — IMPORTANT, because closure captures / UFCS / generators / async / match bindings are all potential false-positive sources. Give the user the command to run in their terminal:
./self-hosted/bin/quake qspec 2>&1 | tail -50and have them paste results back. - If full sweep is clean →
quake guard→ smoke → commit. - If full sweep reveals ANY new failures, document the false-positive pattern, narrow the assertion (e.g. “skip if the NODE_IDENT is inside a closure body that hasn’t finished capture analysis”) or REVERT and file a deeper ROADMAP entry.
- Commit:
C4: POST-RESOLVE-IDENT-ASSERTION — hard error on unresolved NODE_IDENT at MIR entry.
Complexity. M (1–3h, depending on how many false positives surface). Risk. Medium-high — this is the item most likely to need a narrowed scope or a full revert. If you find yourself whack-a-mole’ing false positives for more than 1h, STOP and file as “partial C4: design needs rework” rather than keep patching.
C5 — B4-UNWRAP-IN-LOOP: step! in while loop miscompile
Goal. var step = Option::Some(10); while i < 3; sum += step!; step = Option::Some(...); i += 1; end returns the correct sum instead of 0. The pending test step! inside while loop with reassignment (B4-UNWRAP-IN-LOOP) in spec/qspec/unwrap_in_loop_spec.qz flips from it_pending to it and passes.
AUTHORITATIVE REFERENCE: docs/handoff/b4-unwrap-in-loop-handoff.md. Read it first. It contains:
- The 11-line minimal reproducer
- Full IR analysis showing
sum += %v0where%v0is a constant 0 defined at function entry - The three-condition bug trigger (macro + while loop + subject reassignment)
- Two failed fix attempts from Session 2 (payloads=0 sentinel, hoist-into-block wrapper)
- A 4-phase investigation plan (AST dump comparison, MIR context state check, constant-folding pass audit, alternate desugar strategies)
- Success criteria and a backup binary recipe
Execution: follow the phases in b4-unwrap-in-loop-handoff.md in order. Do Phase 1 (AST dump comparison) first — it’s the most likely to pinpoint the root cause quickly. If Phase 1 doesn’t yield within 1 hour, move to Phase 2. If Phases 1–3 don’t yield within 3 hours total, drop to Phase 4 (alternate desugar — the cleanest is Option C: add an option_unwrap intrinsic and make ! lower to it, bypassing the match entirely).
Do NOT retry the two failed approaches documented in the handoff. They’re documented for a reason.
Steps.
- Read
docs/handoff/b4-unwrap-in-loop-handoff.mdfully. Don’t skim. cp self-hosted/bin/quartz self-hosted/bin/backups/quartz-pre-c5-golden && rm -rf .quartz-cache.- Verify the reproducer still fails on current trunk (post-C1..C4). If C4’s assertion fires on it, that’s a CLUE — it means the macro-generated match has an unresolved NODE_IDENT for the gensym binding, and the C4 + C5 fixes are related. Note the interaction and proceed.
- Phase 1: AST dump comparison (per handoff).
- Phase 2+ if needed.
- Apply fix, verify reproducer returns 211.
- Flip
it_pending→itinspec/qspec/unwrap_in_loop_spec.qz. quake guard→ smoke → full B-sprint regression sweep.- Update ROADMAP row
B4-UNWRAP-IN-LOOPto RESOLVED with commit SHA and a root-cause summary (so future sessions learn from it). - Commit:
C5: Fix B4-UNWRAP-IN-LOOP — $unwrap macro in while loop with reassignment.
Complexity. L (2–4h). Risk. High — two failed attempts already, the bug has defied straightforward AST-shape fixes. Budget accordingly. If C5 burns 4h without a fix, STOP, commit a partial “investigation notes” update to the ROADMAP row with what you learned, and hand off for a future session. Don’t compromise the other 4 commits by starving them of verification time.
Dependency graph
C1 (Quakefile migration) ─── independent, no compiler source change
C2 (B3-DIRECT-INDEX-FIELD) ─── independent, touches typecheck_generics + typecheck_expr_handlers
C3 (IMPL-NOT-SEND) ─── touches parser + typecheck_registry, uses B1's structural walker
C4 (POST-RESOLVE-ASSERTION) ─ touches mir_lower, may interact with C3's new error paths
C5 (B4-UNWRAP-IN-LOOP) ─── touches macro_expand or mir_lower, may be EXPOSED by C4's assertion
Order rationale:
- C1 first: safest warm-up, no compiler source change, builds confidence
- C2 second: small bug fix with a clear reproducer, bounded scope
- C3 third: medium-complexity parser work, standalone
- C4 fourth: needs full QSpec sweep, best done when previous items are stable
- C5 last: highest uncertainty, gets whatever budget remains
Critical sequencing note: if C4’s assertion fires spuriously on a currently-passing test, that’s potentially a pre-existing bug (the resolver really DID forget to push a rib somewhere). Don’t suppress the assertion to make the test pass — investigate and fix the underlying rib omission instead. But if that investigation exceeds 1 hour, revert C4 and file as “partial: assertion logic needs refinement for X case.”
Verification matrix
| Item | Probe | New spec | Regression sweep |
|---|---|---|---|
| C1 | Inject debug puts → run quake build → check for real llc error | (none) | smoke + guard |
| C2 | Pending test in vec_element_type_spec.qz flips to passing | vec_element_type_spec.qz (flip + add 2 variants) | B-sprint set |
| C3 | impl_not_send_spec.qz tests 1+3 show negative impl overrides auto-Send | impl_not_send_spec.qz (5 tests) | B-sprint set + send_auto_trait_spec |
| C4 | post_resolve_assertion_spec.qz synthetic unresolved NODE_IDENT fires QZ9501 | post_resolve_assertion_spec.qz (1+ tests) | FULL QSpec sweep (mandatory) |
| C5 | Pending test in unwrap_in_loop_spec.qz flips to passing | (flip existing pending) | B-sprint set + iterator_protocol_spec + iterable_trait_spec |
Mandatory regression sweep for every commit (the B-sprint set):
impl_trait_spec abstract_trait_spec traits_spec hybrid_ufcs_traits_spec
collection_stubs_spec arity_overload_spec match_unknown_variant_spec
iterable_trait_spec iterator_protocol_spec enum_ctor_in_impl_spec
closure_intrinsic_shadow_spec send_auto_trait_spec vec_element_type_spec
unwrap_in_loop_spec sched_lifecycle_spec
Workflow rules (mandatory per commit)
- Snapshot binary:
cp self-hosted/bin/quartz self-hosted/bin/backups/quartz-pre-cN-golden. - Clean cache:
rm -rf .quartz-cache. CRITICAL — this is the #1 cause of false reproductions. - Read affected files before editing (don’t assume line numbers from this plan — the file may have drifted).
- Edit the source.
- Rebuild:
./self-hosted/bin/quake build. - Probe test: manually verify the specific fix before running the full spec.
quake guard: fixpoint verified, 2281 ± 30 functions.quake smoke: brainfuck 4/4, expr_eval 22/22.- New spec: run the item’s new spec file, all tests pass.
- Regression sweep: run the B-sprint set above, all green.
- Update ROADMAP: mark the row RESOLVED with commit SHA and a one-line summary.
- Commit: include commit message body with root cause, fix, verification, and any follow-ups discovered.
On failure: restore from quartz-pre-cN-golden, document what was tried, decide whether to retry or file as a partial commit with notes.
Never use --no-verify, --no-gpg-sign, or skip quake guard. Never run the full qspec suite from Claude Code — give the user the command and have them paste the result back (the 10-min suite hangs in Claude Code’s PTY).
Session-start checklist
cd /Users/mathisto/projects/quartz
# 1. Verify baseline
git log --oneline -3 # should show b61da0f4 B2 at top
git status # should be clean
./self-hosted/bin/quake guard:check # "Fixpoint stamp valid"
./self-hosted/bin/quake smoke 2>&1 | tail -8 # 4/4 + 22/22
# 2. Read the key docs
cat docs/handoff/batch-c-implementation-plan.md # this document
cat docs/handoff/b4-unwrap-in-loop-handoff.md # C5 investigation plan
grep -A 2 "B3-DIRECT-INDEX-FIELD\|B4-UNWRAP-IN-LOOP\|IMPL-NOT-SEND\|QUAKE-STDERR-AUDIT\|POST-RESOLVE" docs/ROADMAP.md | head -60
# 3. Read the A+B sprint results (for context on the workflow + discipline)
grep -A 10 "Batch A + Batch B sprint summary" docs/ROADMAP.md
# 4. Clear cache and start C1
rm -rf .quartz-cache
cp self-hosted/bin/quartz self-hosted/bin/backups/quartz-pre-c1-golden
Success criteria at handoff
- Minimum viable: 3 of 5 items committed (C1, C2, C3). The Batch A+B sprint’s pattern showed that 3 commits is enough progress for one session even if the harder items stall.
- Target: 4 of 5 items committed (above + C4 OR C5).
- Stretch: all 5 items committed.
Each committed item must:
- Have
quake guardfixpoint verified (±30 from 2281) - Pass
quake smoke(4/4 + 22/22) - Pass the B-sprint regression sweep
- Either have a regression test locking in the fix, OR a clear reason a test isn’t feasible
- Have an updated ROADMAP row with commit SHA
- Have any new error codes registered with explain entries
Deferred items (if any) must:
- Have the ROADMAP row updated with a status note (“investigated 2h, blocked on X”) and enough detail that a future session can pick up from where execution stopped
- NOT be silently skipped — always document what was attempted
Wake-up report format
At session end, post a summary to the chat mirroring the Batch A+B session 2 wake-up:
- What landed: N of 5 items, commit SHAs, fixpoint count.
- What didn’t: any deferred items with reason + investigation notes.
- Surprises: anything that contradicted the plan (e.g. C2 already fixed on trunk, C4 assertion fired on unexpected paths).
- Follow-ups filed: any new ROADMAP entries created during execution.
- Next session recommendation: one sentence on what the next logical target is.
Mirrors the structure I used for Batch A+B’s session 2 wake-up, which was the right shape for multi-item overnight sprints.
Estimated quartz-time
Traditional ÷ 4 calibration:
- C1: 1h (mechanical migration)
- C2: 1–2h (pinpoint + small fix)
- C3: 2–3h (parser + typecheck + test file)
- C4: 1–3h (depends on false positives)
- C5: 2–4h (budgeted max, likely more)
Total: ~7–13h quartz-time. Overnight-sized for autonomous execution. If the session stalls or runs long on C5, commit what’s done and file the rest — consistent with Directive 4 (work spans sessions).
Prime directives, in one sentence: World-class only, no shortcuts, no silent compromises, honest reports, holes get filled or filed, delete freely, binary discipline = source discipline, quartz-time estimation, corrections are calibration.
Go.