# Iter mq.tidy — Implementation Plan > **Parent spec:** `docs/specs/2026-05-13-module-qualified-class-names.md` > > **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement` > to run this plan. Steps use `- [ ]` checkboxes for tracking. **Goal:** Land the four actionable drift items from `audit-mq` (commit `f382931`): rigid-var refinement type-unification leg (high-1); same-module bare-class qualifier shape accepted (high-2); `class-method-shadowed-by-fn` warning tightened to require an instance (medium-1); DESIGN.md Data Model schema fragments cross-reference the canonical-form rule (medium-2). **Architecture:** Three small surgical edits to `crates/ailang-check/src/lib.rs` (rigid-var filter at `lib.rs:2163-2168`, qualifier-shape gate at `lib.rs:2570-2575`, shadow-warning closure at `lib.rs:2479-2502`) plus four DESIGN.md schema-fragment annotations. The discharge-time `refine_multi_candidate_residual` is the only call site that needs the type-unification leg — the synth-time twin `resolve_method_dispatch` is invoked with `concrete_arg_type: None` and constructs the residual's metavar *after* the dispatch call, so the rigid-var leg there has no residual type to unify against (fresh metavar would trivially unify with any declared-constraint type, degenerating to class-only). The class-only filter at synth time is therefore semantically correct, not drift; the carrier's Q1 "fix both" was reconsidered on closer inspection. **Tech Stack:** `crates/ailang-check/src/lib.rs` (synth + discharge + warning closure), `docs/DESIGN.md` (schema fragments). --- ## Files this plan creates or modifies - Modify: `crates/ailang-check/src/lib.rs:2163-2168` — `refine_multi_candidate_residual` rigid-var branch: extend filter from class-only to class + type-unification via `constraint_type_matches`. - Modify: `crates/ailang-check/src/lib.rs:2570-2575` — `qualifier_is_class_shape` predicate: accept bare-class qualifiers (PascalCase first char) in addition to module-qualified-class qualifiers (contains inner dot). - Modify: `crates/ailang-check/src/lib.rs:2479-2502` — `emit_shadow_warning_if_class_method` closure: tighten emission to require at least one candidate class having a registry instance. - Modify: `docs/DESIGN.md:2139` — `SuperclassRef.class` schema fragment annotation. - Modify: `docs/DESIGN.md:2151-2152` — `InstanceDef.class` schema fragment annotation. - Modify: `docs/DESIGN.md:2253` — `Type::Con.name` schema fragment annotation (ct.1 symmetric twin). - Modify: `docs/DESIGN.md:2273` — `Constraint.class` schema fragment annotation. - Test: `crates/ailang-check/src/lib.rs` (in-crate `mod tests` block) — three new RED-first unit tests for T1, T2, T3. No new files. No new fixtures. No new integration tests (the existing mq3 E2E suite already covers the warning-emission and dispatch paths end-to-end; the unit tests pin the precise drift fixes). --- ## Task 1 — Rigid-var refinement type-unification leg **Files:** - Modify: `crates/ailang-check/src/lib.rs:2163-2168` - Test: `crates/ailang-check/src/lib.rs` (in-crate `mod tests` block) The discharge-time `refine_multi_candidate_residual` (sig at `lib.rs:2125-2176`) currently filters declared constraints on `dc.class == c` only at line 2166. When a fn declares two same-class-different-typevar constraints (the canonical example: `forall a b. prelude.Show a, prelude.Show b => (a, b) -> String`, or two distinct classes both declared on different typevars), the filter cannot tell which typevar the residual is on, so both declared constraints survive. The spec rule at §"Constraint-discharge refinement" lines 130-138 says the filter must ALSO unify `dc.type_` with the residual's `type_`. Use the existing `constraint_type_matches` helper at `lib.rs:2273-2283` — it's already the Var/Con recursive matcher calibrated to the post-`subst.apply` residual shape (the doc comment at 2266-2272 names this contract verbatim). - [ ] **Step 1: Write the failing test in the in-crate `mod tests` block.** Add to the existing `mod tests` block in `crates/ailang-check/src/lib.rs` (near the other mq-prefixed dispatch tests, search for `fn mq3_env_class_methods_tuple_keyed`): ```rust #[test] fn mq_tidy_rigid_var_filter_uses_type_unification() { use ailang_core::ast::Constraint; use std::collections::BTreeSet; // Two same-class declared constraints on different typevars, // candidate set has two classes. Without type-unification, the // class-only filter would keep both candidates because both // appear in declared constraints; the spec rule narrows to one. let mut candidates = BTreeSet::new(); candidates.insert("prelude.Show".to_string()); candidates.insert("userlib.Show".to_string()); let residual = ResidualConstraint { class: "prelude.Show".to_string(), type_: Type::Var { name: "a".to_string() }, method: "show".to_string(), candidates: Some(candidates), }; // Active declared constraints: prelude.Show a (matches residual // type), userlib.Show b (does NOT match residual type). let declared = vec![ Constraint { class: "prelude.Show".to_string(), type_: Type::Var { name: "a".to_string() }, }, Constraint { class: "userlib.Show".to_string(), type_: Type::Var { name: "b".to_string() }, }, ]; let registry = std::collections::BTreeMap::new(); let outcome = refine_multi_candidate_residual(&residual, &declared, ®istry); assert_eq!(outcome, RefineOutcome::Resolved("prelude.Show".to_string()), "type-unification leg must drop userlib.Show because its declared type (Var b) does not match the residual type (Var a)"); } ``` - [ ] **Step 2: Run test to verify it fails.** Run: `cargo test --workspace -p ailang-check mq_tidy_rigid_var_filter_uses_type_unification` Expected: FAIL with `assertion failed` — the current class-only filter returns `MissingConstraint` (because two survivors after class-only filter), not `Resolved("prelude.Show")`. - [ ] **Step 3: Apply the type-unification leg edit at lib.rs:2163-2168.** Replace the rigid-var filter at `lib.rs:2163-2168`: ```rust // Rigid-var path: filter against declared constraints. let survivors: Vec = candidates .iter() .filter(|c| declared_constraints.iter().any(|dc| &dc.class == *c)) .cloned() .collect(); ``` with: ```rust // Rigid-var path: filter against declared constraints requiring // BOTH class-name match AND type-unification with the residual's // type_ (per spec §"Constraint-discharge refinement" 130-138). // `constraint_type_matches` is the post-subst.apply Var/Con // recursive matcher; the residual's type_ has been `subst.apply`-d // by the discharge caller, so rigid-var identity (Var("a") vs // Var("b")) is a direct name comparison. let survivors: Vec = candidates .iter() .filter(|c| { declared_constraints.iter().any(|dc| { &dc.class == *c && constraint_type_matches(&dc.type_, &residual.type_) }) }) .cloned() .collect(); ``` - [ ] **Step 4: Run test to verify it passes.** Run: `cargo test --workspace -p ailang-check mq_tidy_rigid_var_filter_uses_type_unification` Expected: PASS. - [ ] **Step 5: Run the existing dispatch + discharge tests to verify no regression.** Run: `cargo test --workspace -p ailang-check mq3_ refine_multi_candidate dispatch_pin` Expected: PASS for all matching tests. The single existing test that exercises the rigid-var path (`mq2_refine_multi_candidate_residual_rigid_var_match` or similar — search the in-crate tests) used a class-only-distinguishable shape (different classes for different residuals); the type-unification leg drops nothing on those inputs because the same typevar is on both sides. Search: Run: `grep -n "refine_multi_candidate_residual" crates/ailang-check/src/lib.rs crates/ailang-check/tests/` Expected: existing tests stay green; the new test is the only one exercising the same-class-different-typevar shape. --- ## Task 2 — Bare-class qualifier shape accepted **Files:** - Modify: `crates/ailang-check/src/lib.rs:2570-2575` - Test: `crates/ailang-check/src/lib.rs` (in-crate `mod tests` block) The `qualifier_is_class_shape` predicate at `lib.rs:2570-2575` currently requires the qualifier prefix to contain an inner dot: ```rust let qualifier_is_class_shape = match &qualifier_opt { None => true, Some(q) => q.contains('.'), }; ``` This excludes same-module bare-class qualifiers like `"Show.show"` (qualifier prefix `"Show"`, no inner dot). The canonical-form rule per mq.1 says class refs are bare for same-module and `.` for cross-module — symmetric on both sides. The current gate is asymmetric. Discriminator between bare-class and bare-fn qualifiers: class names start with uppercase (PascalCase per repo convention; see the existing class refs in `examples/prelude.ail.json` — `Eq`, `Ord` — and the mq-test fixtures — `Show`, `MyEq`). Bare fns start with lowercase (e.g. `std_list.length`). The downstream `resolve_method_dispatch` Step 3 at `lib.rs:2006-2012` already accepts uniform `qualifier_prefix: Option<&str>` and the `candidates.contains(q)` check naturally rejects qualifier strings that don't match any candidate class — so admitting bare-class qualifiers can't accidentally hit fn shapes (those won't be in `candidates`). - [ ] **Step 1: Write the failing test in the in-crate `mod tests` block.** Add adjacent to the Task 1 test: ```rust #[test] fn mq_tidy_bare_class_qualifier_shape_accepted() { // Predicate function under test: extracted as a free helper for // testability, OR test the end-to-end behaviour by checking that // resolve_method_dispatch with a bare-class qualifier "Show" and // a candidate set including "prelude.Show" + "userlib.Show" picks // up "Show" as a class-shape qualifier and dispatches correctly // (it will UnknownClass because neither candidate equals "Show", // but the point is the gate accepts the shape). // // Simpler: directly test the predicate. The implementation will // introduce a free helper `qualifier_is_class_shape(&Option)` // alongside the inline call. assert!(qualifier_is_class_shape(&None)); assert!(qualifier_is_class_shape(&Some("Show".to_string()))); assert!(qualifier_is_class_shape(&Some("prelude.Show".to_string()))); // Bare fn qualifier (lowercase) is NOT class-shape — falls through // to the qualified-fn arm. assert!(!qualifier_is_class_shape(&Some("std_list".to_string()))); } ``` - [ ] **Step 2: Run test to verify it fails (compile error — function not yet extracted).** Run: `cargo test --workspace -p ailang-check mq_tidy_bare_class_qualifier_shape_accepted` Expected: FAIL — `cannot find function `qualifier_is_class_shape` in this scope`. (The current implementation has the predicate inline at `lib.rs:2570-2575`; the test forces the extraction-and-rename.) - [ ] **Step 3: Extract the predicate as a free helper and broaden it.** Add a free helper above the synth's Var-arm dispatch entry (near `parse_method_qualifier` at `lib.rs:2236-2242`): ```rust /// mq.tidy: predicate gating the synth Var-arm's class-method /// dispatch entry on the qualifier shape. Accepts: /// - bare-method form (qualifier absent), e.g. `"show"`. /// - bare-class qualifier, e.g. `"Show.show"` — class names are /// PascalCase per repo convention (first character uppercase). /// - module-qualified class qualifier, e.g. `"prelude.Show.show"`. /// Rejects qualifier shapes that look like cross-module-fn calls /// (`"std_list.length"`): qualifier starts with lowercase, no /// inner dot — falls through to the qualified-fn arm. pub(crate) fn qualifier_is_class_shape(qualifier_opt: &Option) -> bool { match qualifier_opt { None => true, Some(q) => { // Contains inner dot: module-qualified class // (`prelude.Show`). if q.contains('.') { return true; } // Single segment: class iff the first char is uppercase // (PascalCase convention). q.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) } } } ``` - [ ] **Step 4: Replace the inline predicate at lib.rs:2570-2575 with a call to the helper.** Replace: ```rust let qualifier_is_class_shape = match &qualifier_opt { None => true, Some(q) => q.contains('.'), }; qualifier_is_class_shape && env.method_to_candidate_classes.contains_key(method_name) ``` with: ```rust qualifier_is_class_shape(&qualifier_opt) && env.method_to_candidate_classes.contains_key(method_name) ``` - [ ] **Step 5: Run test to verify it passes.** Run: `cargo test --workspace -p ailang-check mq_tidy_bare_class_qualifier_shape_accepted` Expected: PASS. - [ ] **Step 6: Run the existing qualifier-shape-affected tests to verify no regression.** Run: `cargo test --workspace -p ailang-check parse_method_qualifier dispatch_pin synth_var_class_method` Expected: PASS — the broader gate admits more shapes, but `method_to_candidate_classes.contains_key(method_name)` still gates behind real class-method presence; downstream `resolve_method_dispatch` returns `UnknownClass` when the qualifier doesn't match any candidate (test paths unchanged). --- ## Task 3 — `class-method-shadowed-by-fn` warning tightened **Files:** - Modify: `crates/ailang-check/src/lib.rs:2479-2502` - Test: `crates/ailang-check/src/lib.rs` (in-crate `mod tests` block) The `emit_shadow_warning_if_class_method` closure at `lib.rs:2479-2502` currently fires whenever `env.method_to_candidate_classes.get(method_name)` is non-empty — i.e. whenever ANY class declares the method. The spec rule at §"Class-fn collisions" lines 161-162 says the warning should fire only when a class candidate has an instance applicable to the call. The full spec rule requires the arg type, which is unavailable at the Var-arm closure call site (App-arm unification has not run yet); per Boss Q2 decision, this tidy lands the conservative tightening: require at least one candidate class to have a registry entry (any instance, any type). The registry is `env.workspace_registry.entries: BTreeMap<(String, String), RegistryEntry>` keyed by `(qualified_class, type_hash)`. "At least one instance under candidate class C" = "any key `(C, *) ∈ entries`". - [ ] **Step 1: Write the failing test in the in-crate `mod tests` block.** Add adjacent to the Task 2 test: ```rust #[test] fn mq_tidy_shadow_warning_suppressed_when_no_instance() { // Build a minimal Env where candidate class declares the method // but has NO registry instance. The warning closure should NOT // emit. Pre-mq.tidy it would emit unconditionally on // method_to_candidate_classes presence. // // Setup: one class `userlib.Foo` declares method `foo`; no // instance entries in workspace_registry. use std::collections::{BTreeMap, BTreeSet}; let mut method_to_candidate_classes: BTreeMap> = BTreeMap::new(); let mut foo_cands = BTreeSet::new(); foo_cands.insert("userlib.Foo".to_string()); method_to_candidate_classes.insert("foo".to_string(), foo_cands); let workspace_registry_entries: BTreeMap<(String, String), ()> = BTreeMap::new(); // The predicate under test: "any candidate class C has any // (C, *) key in workspace_registry.entries". Extract this as a // free helper for testability: assert!(!any_candidate_class_has_instance( &method_to_candidate_classes, &workspace_registry_entries, "foo", )); // Now add an instance for userlib.Foo Int: let mut registry_with_inst: BTreeMap<(String, String), ()> = BTreeMap::new(); registry_with_inst.insert(("userlib.Foo".to_string(), "Int_hash".to_string()), ()); assert!(any_candidate_class_has_instance( &method_to_candidate_classes, ®istry_with_inst, "foo", )); } ``` - [ ] **Step 2: Run test to verify it fails (compile error — function not yet extracted).** Run: `cargo test --workspace -p ailang-check mq_tidy_shadow_warning_suppressed_when_no_instance` Expected: FAIL — `cannot find function `any_candidate_class_has_instance``. - [ ] **Step 3: Extract the predicate as a free helper.** Add a free helper near `parse_method_qualifier` and `qualifier_is_class_shape`: ```rust /// mq.tidy: predicate for the `class-method-shadowed-by-fn` /// warning emission. The full spec rule (§"Class-fn collisions" /// 161-162) requires a class candidate WITH AN INSTANCE FOR THE /// ACTUAL ARG TYPE; the arg type is unavailable at the synth /// Var-arm where the warning fires (App-arm unification has not /// run yet). This conservative approximation requires only that /// at least one candidate class has SOME instance in the /// workspace registry — stricter than today's "any class /// declares the method" (which fires even for class methods /// with zero instances anywhere), looser than the full spec /// rule. A future tighten can defer the warning emission to a /// later pass where the arg type is known. pub(crate) fn any_candidate_class_has_instance( method_to_candidate_classes: &std::collections::BTreeMap>, workspace_registry_entries: &std::collections::BTreeMap<(String, String), ()>, method_name: &str, ) -> bool { let Some(cands) = method_to_candidate_classes.get(method_name) else { return false; }; cands.iter().any(|c| { workspace_registry_entries .range((c.clone(), String::new())..) .next() .map(|((k, _), _)| k == c) .unwrap_or(false) }) } ``` The `range(..).next()` shape avoids scanning the full map: given the `BTreeMap` ordering is lexicographic on the `(class, type_hash)` tuple, the first entry whose key starts at `(c, "")` is the first entry whose first component equals `c` (if any) — O(log n) per class, O(|candidates| * log n) total. - [ ] **Step 4: Update the warning closure at lib.rs:2479-2502 to call the helper.** The closure today is: ```rust let emit_shadow_warning_if_class_method = |name: &str, owner_module: &str, warnings: &mut Vec| { let (method_name, _) = parse_method_qualifier(name); if let Some(cands) = env.method_to_candidate_classes.get(method_name) { ... emit diagnostic ... } }; ``` The closure needs `workspace_registry.entries` access. The same `registry_unit` view built later at `lib.rs:2599-2604` is the right shape, but the closure runs before that point. Build the view once near the closure definition: ```rust let emit_shadow_warning_if_class_method = |name: &str, owner_module: &str, warnings: &mut Vec| { let (method_name, _) = parse_method_qualifier(name); // mq.tidy: tighten emission per spec §"Class-fn collisions" — // fire only when at least one candidate class has a registry // instance (any type). Without this, the warning fires on // any class declaring the method, including class methods // with zero instances anywhere in the workspace. let registry_unit_view: std::collections::BTreeMap<(String, String), ()> = env .workspace_registry .entries .keys() .map(|k| (k.clone(), ())) .collect(); if !any_candidate_class_has_instance( &env.method_to_candidate_classes, ®istry_unit_view, method_name, ) { return; } if let Some(cands) = env.method_to_candidate_classes.get(method_name) { let mut candidate_list: Vec = cands.iter().cloned().collect(); candidate_list.sort(); let d = crate::diagnostic::Diagnostic::warning( "class-method-shadowed-by-fn", format!( "free fn `{name}` in module `{owner_module}` shadows class method `{method_name}` \ declared in classes {candidate_list:?}. \ To call the class method instead, write `.{method_name} ...`.", ), ) .with_def(in_def.to_string()) .with_ctx(serde_json::json!({ "name": name, "method": method_name, "fn_owner_module": owner_module, "candidate_classes": candidate_list, })); warnings.push(d); } }; ``` Note that the `registry_unit_view` is rebuilt on every closure invocation — the closure runs once per Var-arm; for typical workspaces, registry entries are O(10-100), so the per-call cost is bounded. A future tighten could hoist this view above the closure if profiling shows the Var-arm hot. - [ ] **Step 5: Run test to verify it passes.** Run: `cargo test --workspace -p ailang-check mq_tidy_shadow_warning_suppressed_when_no_instance` Expected: PASS. - [ ] **Step 6: Run existing warning-fire tests to verify the positive case still fires.** Run: `cargo test --workspace mq3_class_method_shadowed_by_fn_warning_fires mq3_class_eq_vs_fn_eq mq3_two_show shadow_warning` Expected: PASS — the existing positive fixtures (`mq3_class_eq_vs_fn_eq`, `typeclass_22b3::rewrite_walker_skips_locally_shadowed_class_method`) all ship registry instances for the shadowed class, so the tightened predicate continues to admit the warning emission. - [ ] **Step 7: Run the full mq.3 E2E suite to verify no regression.** Run: `cargo test --workspace -p ailang-cli mq3` Expected: PASS — three mq3 multi-class E2E tests + the warning suite remain green. --- ## Task 4 — DESIGN.md schema fragments cross-reference canonical-form rule **Files:** - Modify: `docs/DESIGN.md:2139` (SuperclassRef) - Modify: `docs/DESIGN.md:2151-2152` (Instance class field) - Modify: `docs/DESIGN.md:2253` (Type::Con name field — ct.1 twin) - Modify: `docs/DESIGN.md:2273` (Constraint class field) No test (doc-only). - [ ] **Step 1: Read the canonical-form prose anchor.** Read `docs/DESIGN.md` lines 1145-1170 (the "Class names" paragraph where the canonical-form rule is named). Note the exact section title and the line numbers — the cross-references should point at the section title rather than line numbers (line numbers drift as the document grows). Run: `grep -n "Class names" docs/DESIGN.md | head -5` Expected: at least one match around line 1156 (per recon) with the section header containing "Class names" verbatim. - [ ] **Step 2: Annotate SuperclassRef at DESIGN.md:2139.** Current line (per recon): ```text "superclass": null, // or { "class": "", "type": "" } ``` Replace with: ```text "superclass": null, // or { "class": "", "type": "" } — "class": canonical form (bare for same-module, "." for cross-module; see §"Class names") ``` - [ ] **Step 3: Annotate InstanceDef at DESIGN.md:2151-2152.** Locate the `"kind": "instance"` schema fragment around line 2151-2152. Run: `awk 'NR>=2150 && NR<=2155' docs/DESIGN.md` Expected: a block containing `"kind": "instance", "class": ""`. Replace the `"class": ""` line with: ```text "class": "", // canonical form (bare for same-module, "." for cross-module; see §"Class names") ``` (Trailing-comment style consistent with the SuperclassRef annotation; exact whitespace per the surrounding block.) - [ ] **Step 4: Annotate Type::Con at DESIGN.md:2253.** Run: `awk 'NR>=2250 && NR<=2260' docs/DESIGN.md` Expected: a block containing `Type::Con` schema fragment with a `"name"` field. Annotate the `"name"` line with a parenthetical that cross-references the ct.1 canonical-form section (the structurally symmetric rule for type names): ```text "name": "", // canonical form (bare for same-module, "." for cross-module; see §"Type names") ``` Note: the §"Type names" section is the ct.1 anchor that already exists; this is the structural twin of the class-ref annotation. Verify the section title by grepping for it before writing the cross-reference text: Run: `grep -n "Type names\|Canonical type names" docs/DESIGN.md | head -5` If the section title is different (e.g. "Canonical type names"), update the cross-reference text in the annotation to match. - [ ] **Step 5: Annotate Constraint at DESIGN.md:2273.** Locate the `Constraint` schema fragment around line 2273. Run: `awk 'NR>=2270 && NR<=2280' docs/DESIGN.md` Expected: a block containing `"constraints": [{ "class": "", ... }]`. Replace the `"class"` field annotation with: ```text "constraints": [{ "class": "", "type": "" }, ...] // "class": canonical form (bare for same-module, "." for cross-module; see §"Class names") ``` - [ ] **Step 6: Verify DESIGN.md still parses cleanly.** Run: `head -1 docs/DESIGN.md && wc -l docs/DESIGN.md && grep -c "canonical form" docs/DESIGN.md` Expected: file header unchanged, line count increased by ~4-8 lines (each annotation may have shifted a comment to a new line), "canonical form" count increased by 4. --- ## Task 5 — Integration verification **Files:** none modified; runs all tests + bench scripts. - [ ] **Step 1: Run full workspace test suite.** Run: `cargo test --workspace --no-fail-fast 2>&1 | grep -E "^test result" | awk '{p+=$4; f+=$6} END{print "passed:", p, "failed:", f}'` Expected: `passed: 548 failed: 0` (was 545 + 3 new RED-then-GREEN tests from Tasks 1-3; Task 4 has no tests). - [ ] **Step 2: Run bench/compile_check.py.** Run: `bench/compile_check.py 2>&1 | tail -3` Expected: `summary: 24 metrics; 0 regressed, 0 improved beyond tolerance, 24 stable` and exit code 0. - [ ] **Step 3: Run bench/cross_lang.py.** Run: `bench/cross_lang.py 2>&1 | tail -3` Expected: `summary: 25 metrics; 0 regressed, 0 improved beyond tolerance, 25 stable` and exit code 0. - [ ] **Step 4: Run bench/check.py.** Run: `bench/check.py 2>&1 | tail -3` Expected: `summary: 63 metrics; N regressed, M improved beyond tolerance, K stable` with N+M small (audit-mq established the noise envelope at metric identity shifting between runs across runs 1-4; 5th consecutive audit baseline-pristine convention applies if regressions land in the same noise class). If N is 0, that's a clean pass; if N is small (1-3) and metrics overlap with the audit-mq noise classes (`bench_list_sum.bump_s`, `bench_closure_chain.bump_s`, `latency.implicit_at_rc.*` max-tail), document in the per-iter journal as "noise envelope continues, baseline pristine". Tidy iter does NOT typically touch runtime metrics — the four drift fixes are all typecheck-side. - [ ] **Step 5: Prelude roundtrip check.** Run: `cargo run --bin ail -- check examples/prelude.ail.json` Expected: `ok` and exit code 0. Sanity gate that the schema-fragment annotations didn't break DESIGN.md → prelude consistency (annotations are comment-only and don't touch the JSON, but verify the canonical form on the prelude still works). --- ## Self-review 1. **Spec coverage:** All four actionable drift items from audit-mq are covered: T1 (high-1), T2 (high-2), T3 (medium-1), T4 (medium-2). The two roadmap-backlog items ([low-1], [low-2]) and the acknowledged-debt item ([medium-3] `synth(...)` signature growth) are deliberately not in scope per the carrier — captured in audit-mq journal as deferred. 2. **Placeholder scan:** No "TBD", no "TODO", no "implement later", no "similar to Task N", no "add appropriate error handling". 3. **Type consistency:** `qualifier_is_class_shape` named once (helper + replacement), `any_candidate_class_has_instance` named once, `constraint_type_matches` referenced by its existing name per `lib.rs:2273`. `refine_multi_candidate_residual` and `resolve_method_dispatch` are referenced by their existing names. The carrier's Q1 "fix both call sites" was revised on closer inspection: only the discharge-time site has a residual type to unify against (synth-time uses a fresh metavar created AFTER the dispatch call). The plan documents this revision in the Architecture paragraph and only touches `refine_multi_candidate_residual`. 4. **Step granularity:** Each step is a single edit, test run, or verification command bounded at 2-5 minutes. No "implement the feature" steps. 5. **No commit steps:** No `git add` or `git commit` in any task. Implement leaves the work in the working tree; Boss commits at iter close.