iter mq.tidy: close 4 actionable drift items from audit-mq

T1 [high-1]: refine_multi_candidate_residual rigid-var filter now
requires class + type-unification via constraint_type_matches, so
forall a b. prelude.Show a, userlib.Show b => ... shapes discriminate
by which typevar the residual is on. Discharge-time only; synth-time
twin doesn't have a residual type to unify against (fresh metavar
constructed after dispatch).

T2 [high-2]: qualifier_is_class_shape extracted as pub(crate) helper
adjacent to parse_method_qualifier; broadened to accept PascalCase
single-segment qualifiers like Show.show alongside module-qualified
ones. Same-module bare-class call sites now reachable; symmetric to
mq.1 canonical-form rule.

T3 [medium-1]: any_candidate_class_has_instance pub(crate) helper
gates the class-method-shadowed-by-fn warning closure on registry
instance presence (any candidate class, any type). Conservative
tightening of the spec rule per Boss Q2 decision.

T4 [medium-2]: four DESIGN.md Data Model schema fragments
(SuperclassRef, InstanceDef.class, Type::Con.name, Constraint.class)
annotated with canonical-form cross-references.

Two trip-wires fixed inline: parse_method_qualifier docstring
coherence, mq3 in-crate test fixture-repair (registry instance
injection — analogous to mq.3 typeclass_22b3 pattern).

5/5 tasks. 548 tests green (was 545 + 3 new mq_tidy_* unit tests).
bench/compile_check.py + cross_lang.py exit 0; check.py exit 0
both runs with noise-class metric migration on
latency.implicit_at_rc.* max-tail (6th-consecutive observation
since audit-cma). Baseline pristine.
This commit is contained in:
2026-05-13 02:49:26 +02:00
parent 64d3feeb97
commit 1b6cbcb68b
5 changed files with 484 additions and 18 deletions
+4 -4
View File
@@ -2136,7 +2136,7 @@ are real surface forms.
{ "kind": "class",
"name": "<id>", // class name (e.g. "Show")
"param": "<id>", // single class parameter, kind *
"superclass": null, // or { "class": "<id>", "type": "<param>" }
"superclass": null, // or { "class": "<id>", "type": "<param>" } — "class": canonical form (bare for same-module, "<module>.<Class>" for cross-module; see §"Class names" / mq.1)
"methods": [
{ "name": "<id>",
"type": Type, // FnSig over the class param
@@ -2149,7 +2149,7 @@ are real surface forms.
// instance (typeclass instance; see Decision 11)
{ "kind": "instance",
"class": "<id>", // class being instantiated
"class": "<id>", // class being instantiated; canonical form (bare for same-module, "<module>.<Class>" for cross-module; see §"Class names" / mq.1)
"type": Type, // concrete type expression (never the class param)
"methods": [
{ "name": "<id>", "body": Term }
@@ -2250,7 +2250,7 @@ Patterns are linear: each pattern variable may appear at most once.
```jsonc
// Type-constructor application. `args` omitted when empty
// (hash-stable when omitted, for non-parameterised cases like Int, Bool, ...).
{ "k": "con", "name": "<id>", "args": [Type...] }
{ "k": "con", "name": "<id>", "args": [Type...] } // "name": canonical form (bare for same-module / primitives, "<module>.<TypeName>" for cross-module; see §"Type::Con name scoping" / ct.1)
// Function type. Decision 10 added paramModes/retMode as
// metadata on Type::Fn — they are NOT separate Type variants, so every
@@ -2270,7 +2270,7 @@ Patterns are linear: each pattern variable may appear at most once.
// constraints (Decision 11); omitted when empty (hash-stable when omitted).
{ "k": "forall",
"vars": ["<id>"...],
"constraints": [{ "class": "<id>", "type": "<id>" }, ...],
"constraints": [{ "class": "<id>", "type": "<id>" }, ...], // "class": canonical form (bare for same-module, "<module>.<Class>" for cross-module; see §"Class names" / mq.1)
"body": Type }
```
+219
View File
@@ -0,0 +1,219 @@
# iter mq.tidy — Close 4 actionable drift items from audit-mq
**Date:** 2026-05-13
**Started from:** 64d3feeb972f4ddfe9bd48484585c69a457e7e1e
**Status:** DONE
**Tasks completed:** 5 of 5
## Summary
mq.tidy closes the four actionable drift items audit-mq surfaced
(commit `f382931`), moving the module-qualified-class-names milestone
from "structurally complete" to "drift-clean ready for next milestone".
T1 extends the discharge-time `refine_multi_candidate_residual`
rigid-var filter from class-only to class + type-unification via the
existing `constraint_type_matches` helper, so two same-class declared
constraints on different typevars are correctly discriminated by the
residual's typevar identity (high-1). T2 extracts the inline
`qualifier_is_class_shape` predicate as a free `pub(crate)` helper and
broadens it to accept PascalCase single-segment qualifiers
(`"Show.show"`) alongside module-qualified ones (`"prelude.Show.show"`)
— same-module bare-class call sites are now reachable, symmetric to
the canonical-form rule at the schema level (high-2). T3 introduces
the `any_candidate_class_has_instance` predicate and gates the
`class-method-shadowed-by-fn` warning closure on it, so class methods
with zero registry instances anywhere no longer fire the warning;
conservative approximation of the full spec rule per Boss Q2 (the arg
type required for the strict version is unavailable at the Var-arm)
(medium-1). T4 annotates the four Data Model schema fragments
(`SuperclassRef`, `InstanceDef.class`, `Type::Con.name`,
`Constraint.class`) with the canonical-form cross-reference, so a
schema-section reader without the prose context no longer sees
bare-only (medium-2).
T5 verification: 548 tests green (was 545 pre-iter + 3 new
mq_tidy_* unit tests). `bench/compile_check.py` exit 0 (24/24 stable
after a one-run noise blip absorbed under tolerance on re-run).
`bench/cross_lang.py` exit 0 (25/25 stable). `bench/check.py` exit 0
both runs; metric-identity-migrating noise on the
`latency.implicit_at_rc.*` max-tail cluster — 5th-consecutive audit
observation per audit-mq's lineage, baseline pristine. The
`mq3_class_method_shadowed_by_fn_warning_fires` in-crate test was a
trip-wire: pre-tidy it used `Registry::default()` (no instances) and
the warning fired anyway because the old gate didn't check instances;
post-tidy it correctly suppresses, so the fixture was updated to ship
a `clsmod.Show Int` registry instance — fixture-repair as a coherence
consequence of the deliberately tightened spec rule. mq3 E2E (3 tests)
+ typeclass_22b3 (18 tests) all stay green; the E2E fixtures already
ship instances for the shadowed class.
## Per-task notes
- **mq.tidy.1** — Rigid-var refinement type-unification leg.
`refine_multi_candidate_residual` filter at lib.rs:2163-2180 extends
from `dc.class == c` to `dc.class == c && constraint_type_matches(&dc.type_, &residual.type_)`.
New unit test `mq_tidy_rigid_var_filter_uses_type_unification` pins
the same-class-different-typevar discrimination
(`prelude.Show a` + `userlib.Show b` declared, residual on Var `a`
`Resolved("prelude.Show")`, `userlib.Show` drops because its
declared type `Var b` doesn't unify with the residual's `Var a`).
Architecture decision per plan: only the discharge-time call site
needs the type-unification leg; the synth-time `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 filter). The class-only filter at synth time is therefore
semantically correct, not drift.
- **mq.tidy.2** — Bare-class qualifier shape accepted. Inline
`qualifier_is_class_shape` predicate at the synth Var-arm dispatch
entry (originally lib.rs:2570-2575) extracted as a free
`pub(crate) fn qualifier_is_class_shape(&Option<String>) -> bool`
helper adjacent to `parse_method_qualifier`. Predicate body:
`None` → true (bare method); `Some(q)` with inner dot → true
(`<module>.<Class>`); `Some(q)` single-segment → first char
uppercase (PascalCase). Bare-fn qualifiers (`"std_list"`, lowercase)
correctly reject and fall through to the cross-module-fn arm.
Call site at the original location reduced to a one-liner. New
unit test `mq_tidy_bare_class_qualifier_shape_accepted` covers all
four shapes.
- **mq.tidy.3** — `class-method-shadowed-by-fn` warning tightened.
New `pub(crate) fn any_candidate_class_has_instance(...)` helper
using BTreeMap range-scan (O(log n) per candidate, O(|candidates|
* log n) total) checks the workspace registry for at least one
instance under any candidate class. Warning closure at
lib.rs:2479-2502 (now ~2498-2540 post-T2-insertion-shift) builds
a `registry_unit_view: BTreeMap<(String, String), ()>` per
closure invocation and calls the helper before emitting. New
unit test `mq_tidy_shadow_warning_suppressed_when_no_instance`
pins both directions (empty registry → suppress; one instance
→ fire). The existing `mq3_class_method_shadowed_by_fn_warning_fires`
in-crate test was a trip-wire — its fixture used
`Registry::default()` (no instances) and pre-tidy the warning
fired anyway because the old gate didn't check instances. Post-tidy
the spec rule correctly suppresses; fixture repaired by injecting
a `clsmod.Show Int` registry entry directly into `ws.registry.entries`
so the test continues to assert positive-fire behaviour under the
post-tidy rule. The three mq3 E2E fixtures (`mq3_class_eq_vs_fn_eq`
+ `mq3_two_show_*`) already ship instances on the shadowed class,
so they stayed green without modification.
- **mq.tidy.4** — DESIGN.md schema fragments. Four trailing-comment
annotations: `SuperclassRef` (`"superclass"` field at line 2139),
`InstanceDef.class` (line 2152), `Type::Con.name` (line 2253), and
`Constraint.class` (line 2273). Class-ref annotations cross-reference
§"Class names" / mq.1; the Type::Con annotation cross-references
§"Type::Con name scoping" / ct.1 (the actual section title — the
plan's "§Type names" / "Canonical type names" search did not match
exactly; used the canonical anchor title verified via grep). Doc
count of "canonical form" went from 1 → 5 (+4 as planned). All
annotations are inline trailing-comment appends; no line-count
change in DESIGN.md (2678 → 2678).
- **mq.tidy.5** — Integration verification.
`cargo test --workspace --no-fail-fast` → 548 passed, 0 failed
(was 545 pre-iter + 3 new mq_tidy_* unit tests). `bench/compile_check.py`
exit 0; first run showed 1 regression
(`build_O0_ms.bench_list_sum_explicit` +5.43% within 20.0% tolerance,
classified `ok`; the summary's "1 regressed" count is a
false-positive on the first run, second run shows 0 regressed,
24/24 stable). `bench/cross_lang.py` exit 0 both runs, 25/25 stable.
`bench/check.py` exit 0 both runs; run 1 had 3 regressed / 3
improved (all `latency.implicit_at_rc.*` max-tail metrics — same
noise class the audit-mq journal documented across 4 consecutive
runs); run 2 had 0 regressed / 1 improved / 62 stable — exactly
the metric-identity-migration pattern the audit named as variance,
not signal. Baseline pristine per the conservative-call convention
(6th-consecutive observation now). Plan Step 5 (prelude roundtrip
via `cargo run --bin ail -- check examples/prelude.ail.json`)
returned exit 1 with "module name 'prelude' is reserved" — but
this is identical to pre-edit behaviour (verified via `git stash`),
not a regression from this iter. The plan's expectation was
inaccurate; the actual sanity gate (DESIGN.md doc edits do not
break prelude consistency) is satisfied by the workspace test
suite passing 548/548.
## Concerns
- **Plan Task 3 Step 6 anticipation was off for one in-crate test.**
The plan said: "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." This
was correct for those two fixtures (both E2E), but the in-crate
unit test `mq3_class_method_shadowed_by_fn_warning_fires` builds
its workspace with `Registry::default()` (zero instances) and
pre-tidy relied on the looser rule. Post-tidy it correctly
suppresses; the fixture was repaired inline by injecting a
`clsmod.Show Int` registry entry. This is exactly analogous to the
`typeclass_22b3::rewrite_walker_skips_locally_shadowed_class_method`
trip-wire mq.3 documented — same pattern, different test.
Fixture-repair as a coherence consequence of a deliberately
tightened spec rule; the test's intent ("warning fires when a
free fn shadows a class method") is unchanged.
- **Plan Task 5 Step 5 prelude check expectation was off.** The plan
said `cargo run --bin ail -- check examples/prelude.ail.json`
expected `ok` exit 0; the actual behaviour is exit 1 with the
"module name 'prelude' is reserved (auto-injected by the loader)"
error. This is identical to pre-edit behaviour (verified via
`git stash`); the prelude file is the loader's auto-injected
module, not a workspace entry, so `ail check` against it has
always failed by design. The right sanity gate for "DESIGN.md
doc edits do not break prelude consistency" is the workspace
test pass (548/548), which exercises every workspace test that
uses the prelude. The plan's prescribed command can be removed
from future tidy plans.
- **T2 docstring update on `parse_method_qualifier` was not
explicitly in the plan.** The plan asked for adding the new
`qualifier_is_class_shape` helper but did not enumerate the
paragraph update on `parse_method_qualifier`'s docstring. The
old docstring said "Callers gate on the qualifier shape: a class
qualifier is always qualified per mq.1's canonical-form rule" —
which the broadened gate directly contradicts (bare PascalCase
qualifiers now go to class-dispatch). Updating the existing
helper's documentation to reference the new helper and remove
the now-incorrect claim is strictly required to keep the helper's
documentation coherent with the requested behaviour change; it
is the docstring analog of "code edits strictly required to make
the requested changes compile." Defensible as a coherence
consequence, not unrequested scope creep.
## Known debt
- **bench/check.py max-tail noise envelope.** 6th-consecutive
observation of the `latency.implicit_at_rc.*` max-tail metric
identity migrating between runs. The conservative-call convention
has held the baseline pristine across 5 prior audits; this iter
adds run 6 to the lineage. A future audit may consider ratification
via `--update-baseline` if the pattern persists for another 2-3
audits; until then, the lineage is the documentation.
- **mq.tidy did not deliver the [low-1] (Trajectory B + D E2E)
or [low-2] (mono env shape) roadmap-backlog items.** Same scope
as audit-mq's deferral; these wait for the multi-class workspace
shape to land downstream.
- **mq.tidy did not refactor `synth(...)`'s 10-mut-ref signature.**
audit-mq's [medium-3] item was explicitly acknowledged-debt, not
in tidy scope. Same disposition; future iter may revisit if a
natural refactor opportunity arises.
## Files touched
Code:
- crates/ailang-check/src/lib.rs
Docs:
- docs/DESIGN.md
- docs/journals/2026-05-13-iter-mq.tidy.md (this file, new)
No new test files; the three new mq_tidy_* unit tests are inline in
`crates/ailang-check/src/lib.rs`'s `mod tests` block.
## Stats
bench/orchestrator-stats/2026-05-13-iter-mq.tidy.json
+1
View File
@@ -46,3 +46,4 @@
- 2026-05-13 — iter mq.2: type-driven dispatch mechanism installed (mechanism-before-exercise) — `Env.method_to_candidate_classes` workspace-flat inverse index built alongside `class_methods`; two new `CheckError` variants `AmbiguousMethodResolution` + `UnknownClass` (Display+code+ctx) plus additive `NoInstance.candidate_classes` field; `ResidualConstraint` extended with `candidates: Option<BTreeSet<String>>` (visibility bumped `pub(crate)``pub` for test-crate access); pure `resolve_method_dispatch` helper implementing the spec's 5-step rule (qualifier → singleton → type-driven filter → constraint-driven filter → `Multi` for discharge-time refinement); synth Var-arm class-method branch rewritten via `parse_method_qualifier` with inner-dot gate (qualifier must be `<module>.<Class>` form, single-dot names like `std_list.length` fall through to the existing qualified-fn path); `refine_multi_candidate_residual` wired into `check_fn` discharge using `expanded` (post-superclass-expansion constraints) for the rigid-var path; `resolve_residual_class_for_mono` wired into mono's `collect_residuals_ordered` residual-to-target mapping. With `MethodNameCollision` still gating real workspaces, the new multi-candidate branches are exercised exclusively by 15 unit tests: 6 in `tests/method_dispatch_pin.rs` covering the 5-step rule's six cases, 6 in `lib.rs` `mod tests` covering variants + field shapes + discharge refinement, 3 in `mono.rs` `mod tests` covering the mono helper. Real workspaces continue producing single-class residuals (`candidates: None`); every pre-mq.2 fixture typechecks unchanged. Plan-invented `format_type_for_display` replaced with `ailang_core::pretty::type_to_string` (one less duplicate). Synth-time `declared_constraints: &[]` is a deliberate gap documented as known debt — load-bearing only post-mq.3 for the rigid-var fallback (env-plumbing the active fn's constraints into the Var arm is a ~10-line edit slated for mq.3). 9/9 tasks, 539 tests green (was 520 at start of mq.2; +19 = 15 new + 4 pre-existing); `bench/compile_check.py` + `cross_lang.py` clean; `bench/check.py` 1 regression (latency noise, runtime cannot be touched by typecheck-side iter). `MethodNameCollision` retirement lands in mq.3 → 2026-05-13-iter-mq.2.md
- 2026-05-13 — iter mq.3: `MethodNameCollision` retired + multi-class E2E + DESIGN.md sync — milestone close. Deletes `WorkspaceLoadError::MethodNameCollision` variant + `Origin` enum + per-def collision loop in `build_registry` (workspace.rs 596-681) + Display arm in `main.rs:1201`; the two in-workspace.rs pin tests relocate to `crates/ailang-check/tests/method_collision_pin.rs` with inverted assertions (loads cleanly + `env.method_to_candidate_classes["foo"]` has two qualified-class entries). Resolves both mq.2 known-debt items: (1) `Env.active_declared_constraints: Vec<Constraint>` plumbed pre-synth in `check_fn` so the post-superclass-expansion constraint set reaches the synth Var-arm's `resolve_method_dispatch` constraint-driven filter; (2) `ModuleGlobals.class_methods` + `Env.class_methods` re-keyed from `BTreeMap<MethodName, ClassMethodEntry>` to `BTreeMap<(QualifiedClass, MethodName), ClassMethodEntry>` with new `class_method_candidates(name) -> Vec<(&class, &entry)>` accessor; mono's two presence-check sites (`rewrite_mono_calls`, `interleave_slots`) switched to consult `method_to_candidate_classes` natively (method-keyed, preserves the presence-check signature shape). New `synth(...)` warnings channel via `warnings: &mut Vec<Diagnostic>` out-parameter threaded through 15+ recursive callsites + 5 external callers (check_fn, check_const, 3 in builtins.rs, 1 in lift.rs, 2 in mono.rs, 1 in `check`); new structured warning `class-method-shadowed-by-fn` (kebab-case code, structured ctx carrying `name`/`method`/`fn_owner_module`/`candidate_classes`) fires at all three fn-precedence branches (locals, same-module fn, implicit-import fn). Implicit-import-fn branch reordered ABOVE the class-method branch per spec §"Class-fn collisions" so fn-wins precedence is structural. Three new positive E2E fixtures + integration tests in `crates/ail/tests/mq3_multi_class_e2e.rs`: (a) `mq3_two_show_ambiguous` (two `Show` classes, both with `Show Int`, bare `show 42``AmbiguousMethodResolution`); (b) `mq3_two_show_qualified` (same workspace, `mq3_two_show_ambiguous_a.Show.show 42` → clean); (c) `mq3_class_eq_vs_fn_eq` (`class MyEq { myeq }` + `fn myeq` + bare `myeq 1 2` → fn wins, warning fires). DESIGN.md sync: class-names paragraph rewritten to point at mq.1 canonical-form + mq.3 dispatch model; `MethodNameCollision` bullet struck from "Workspace-load (registry-build) diagnostics" with forward-pointing note; `AmbiguousMethodResolution` / `UnknownClass` / `class-method-shadowed-by-fn` + `NoInstance.candidate_classes` added to "Typecheck diagnostics"; `AmbiguousInstance` paragraph reworded to distinguish registry-level (per-class coherence via `DuplicateInstance`) from call-site (cross-class method ambiguity, new diagnostic); new `### Method dispatch` subsection anchors the 5-step rule with `method_to_candidate_classes` as the load-bearing data structure, the class-fn precedence rule, and the post-mq.3 tuple-keyed `class_methods` shape. Roadmap P2 milestone → `[x]` with three-iter summary; milestone-24 `depends on:` line struck and entry annotated "ready for re-brainstorm". Plan defects fixed inline: `check_fn` signature is `Result<()>` not `Result<(CheckedFn, Vec<Diagnostic>)>` — adopted mut-ref-accumulator pattern matching existing `Vec<CheckError>` shape; `class_method_candidates` returns `Vec<(...)>` not `impl Iterator<...>` (the `use<'a, '_>` opaque-type-capture syntax not yet idiomatic in this crate); instance-method body shape draft was `Term::App` but existing-convention is `Term::Lam` with `paramTypes`/`retType` — three fixtures repaired inline. One existing E2E test (`crates/ail/tests/typeclass_22b3.rs:rewrite_walker_skips_locally_shadowed_class_method`) now correctly fires the new warning (the fixture intentionally shadows a class method); test's `assert!(diags.is_empty())` relaxed to filter the new warning with a naming comment. 9/9 tasks, 545 tests green (was 539; +6 net = 3 new mq.3.x lib + 2 method_collision_pin + 3 mq3_multi_class_e2e 2 deleted workspace.rs pin tests); `bench/compile_check.py` + `cross_lang.py` exit 0; `bench/check.py` exit 1 with 2 noise-class regressions (3rd-consecutive `bench_list_sum.bump_s` persistence + max_us tail metric, both runtime-uncoupled-to-typecheck-iter); prelude zero-diff. Module-qualified-class-names milestone structurally closed → 2026-05-13-iter-mq.3.md
- 2026-05-13 — audit-mq: milestone close (module-qualified-class-names) — architect drift report surfaces 4 actionable items routing to `mq.tidy` (2× [high]: rigid-var refinement type-unification leg missing in `refine_multi_candidate_residual` for `forall a b. Show a, Show b => ...` shapes; same-module bare-class qualifier `Show.show` unreachable because `qualifier_is_class_shape = q.contains('.')` excludes the no-dot case contradicting mq.1 canonical-form symmetry; 2× [medium]: `class-method-shadowed-by-fn` warning over-fires on locals shadowing prelude method names without class-candidate-for-arg-type check; DESIGN.md Data Model schema fragments don't carry the canonical-form rule for `InstanceDef.class` / `Constraint.class` / `SuperclassRef.class`), plus 1× [medium] acknowledged debt without fix (`synth(...)` 10-mut-ref-parameter growth — consistent with crate's existing accumulator pattern; refactor cost disproportionate to gain), plus 2× [low] roadmap-backlog (no E2E for Trajectories B + D; `collect_mono_targets` rebuilds env without `active_declared_constraints` — currently latent because mono residuals are concrete-type). Bench mixed: `compile_check.py` + `cross_lang.py` exit 0; `check.py` exit 1 across 4 consecutive re-runs with metric identity shifting between runs (3 → 1 → 1 → 0 regressions, different metrics each) — pattern consistent with 5th-consecutive audit noise-class observation since audit-cma; baseline pristine for 5th consecutive audit (the metric-migration-between-runs is itself attribution evidence variance not signal) → 2026-05-13-audit-mq.md
- 2026-05-13 — iter mq.tidy: close 4 actionable drift items from audit-mq — T1 extends `refine_multi_candidate_residual`'s rigid-var filter at `lib.rs:2163-2168` from class-only (`dc.class == c`) to class + type-unification (`dc.class == c && constraint_type_matches(&dc.type_, &residual.type_)`) via the existing `constraint_type_matches` helper at `lib.rs:2273`, so `forall a b. prelude.Show a, userlib.Show b => (a, b) -> String`-shape declared-constraint sets correctly discriminate by which typevar the residual is on (high-1; spec §"Constraint-discharge refinement" 130-138). Plan revision noted at the architecture paragraph: synth-time `resolve_method_dispatch` is invoked with `concrete_arg_type: None` and constructs the residual metavar AFTER the dispatch call, so the rigid-var leg there has no residual type to unify against — class-only filter at synth time is semantically correct, not drift; fix lands only at the discharge-time site. T2 extracts the inline `qualifier_is_class_shape` predicate from synth Var-arm at `lib.rs:2570-2575` as a free `pub(crate) fn qualifier_is_class_shape(&Option<String>) -> bool` adjacent to `parse_method_qualifier`; broadened to accept PascalCase single-segment qualifiers (`"Show.show"`) alongside module-qualified ones (`"prelude.Show.show"`), so same-module bare-class call sites are now reachable — symmetric to mq.1's canonical-form rule at the schema level (high-2). Discriminator: class names start with uppercase per PascalCase convention; bare-fn qualifiers (`"std_list"`, lowercase) correctly reject and fall through to the cross-module-fn arm. T3 introduces `pub(crate) fn any_candidate_class_has_instance(...)` using a BTreeMap range-scan (O(|candidates| * log n)) and gates the `class-method-shadowed-by-fn` warning closure at `lib.rs:2479-2502` on it, so class methods with zero registry instances anywhere no longer fire the warning — conservative Boss-Q2-decision tightening of the spec rule (full rule would require the arg type, unavailable at the Var-arm before App-arm unification) (medium-1). T4 annotates four DESIGN.md Data Model schema fragments (`SuperclassRef` 2139, `InstanceDef.class` 2152, `Type::Con.name` 2253, `Constraint.class` 2273) with trailing-comment canonical-form cross-references — Type::Con annotation points at the actual existing section title `§"Type::Con name scoping"` (ct.1 anchor verified via grep, plan's text guess was off) (medium-2). Two trip-wires fixed inline: (a) `parse_method_qualifier` docstring updated to remove the now-incorrect "class qualifier is always qualified per mq.1" claim that the broadened gate directly contradicts — coherence-required not unrequested; (b) in-crate `mq3_class_method_shadowed_by_fn_warning_fires` test built workspace with `Registry::default()` (no instances) and pre-tidy fired anyway because old gate didn't check instances — post-tidy correctly suppresses, fixture repaired by injecting a `clsmod.Show Int` registry entry directly into `ws.registry.entries` (analogous to mq.3's `typeclass_22b3` trip-wire pattern). 5/5 tasks, 548 tests green (was 545 + 3 new `mq_tidy_*` unit tests); `bench/compile_check.py` exit 0 (24/24 stable after one-run noise blip absorbed under tolerance on re-run), `cross_lang.py` exit 0 (25/25 stable), `check.py` exit 0 both runs with metric-identity-migrating noise on `latency.implicit_at_rc.*` max-tail cluster — 6th-consecutive observation of the audit-mq-named noise envelope, baseline pristine. Plan Step 5 expectation `ail check examples/prelude.ail.json` exit 0 was inaccurate (actual: exit 1 "module name 'prelude' is reserved", identical to pre-edit, prelude is loader-auto-injected and never a workspace entry); 548/548 workspace test pass is the right sanity gate → 2026-05-13-iter-mq.tidy.md