# 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
