# iter mq.1 — Canonical-form extension for class-ref fields + workspace internal qualification

**Date:** 2026-05-13
**Started from:** 1a5f8289b7fbad3ae5bdd6badde27c80eca6dad3
**Status:** DONE
**Tasks completed:** 7 of 7

## Summary

mq.1 lands the canonical-form rule for the three class-reference
schema fields (`InstanceDef.class`, `Constraint.class`,
`SuperclassRef.class`) — bare for same-module, `<module>.<Class>`
for cross-module — symmetric to ct.1's `Type::Con.name` rule.
`ClassDef.name` stays bare (defining site, like `TypeDef.name`).
ct.1's `validate_canonical_type_names` validator gains a sibling
walk for class refs via the new `check_class_ref` helper, with
two new sibling diagnostics `BareCrossModuleClassRef` and
`BadCrossModuleClassRef`. Workspace-internal class-name keys
(`class_def_module`, `class_by_name`, registry `entries.0`,
`ClassMethodEntry.class_name`, `class_superclasses`, mono's
`class_index`, `Origin::Class.class_name`) all carry the qualified
form post-mq.1; the on-disk schema value is the canonical form (bare
or qualified) that `qualify_class_ref` lifts to the workspace key
at every lookup boundary. `MethodNameCollision` and the dispatch
path are unchanged in this iter (iter 2 + 3 land them).

## Per-task notes

- iter mq.1.1: two new diagnostic variants
  `BareCrossModuleClassRef` / `BadCrossModuleClassRef` sibling to the
  type-ref variants; two Diagnostic-shape Display arms in `ail/main.rs`
  mirroring the existing arm. 2 RED unit tests, both GREEN.
- iter mq.1.2: doc-comments on the four class-reference fields
  in `ast.rs` (`ClassDef.name` stays bare with cross-reference;
  `SuperclassRef.class` / `InstanceDef.class` / `Constraint.class`
  document canonical form). Zero semantic effect.
- iter mq.1.3: `check_class_ref` helper plus three per-def walks
  inside `validate_canonical_type_names`; `check_class_name_fields`
  narrowed to `ClassDef.name` only; four in-test pin tests inverted
  (qualified is now accepted); on-disk `test_ct1_qualified_class_rejected`
  fixture-test repurposed (now expects `OrphanInstance` —
  the natural post-mq.1 rejection mode for the same shape); positive
  on-disk fixture pair `mq1_xmod_constraint_class{,_dep}` shipped
  plus the corresponding pin test. 4 new bare-xmod tests + 1 positive
  qualified test, all GREEN.
- iter mq.1.4: `qualify_class_ref` helper at module scope; `build_registry`
  Pass-1 re-keyed (qualified); Pass-2 lookups use `qualify_class_ref` to
  lift `inst.class` to the registry-key shape; superclass walk threads
  `current_class_module` to qualify bare `SuperclassRef.class`;
  `Origin::Class.class_name` qualified (one construction site, not two as
  the plan claimed); type-leg of coherence check now also handles
  qualified head names symmetrically (split-and-lookup).
- iter mq.1.5: `ClassMethodEntry.class_name` insert qualifies in
  `build_module_globals`; `class_superclasses` key + value both
  qualified; mono's `build_class_index` keyed qualified (plan said
  "no code change needed in mono.rs consumers" — that statement was
  wrong, the build-side needed the matching qualification).
- iter mq.1.6: test assertions updated for the new qualified shape —
  the two `MethodNameCollision` pin tests, `typeclass_22b2.rs:42`
  (`class_method_class("show")` now `Some("...Show")`),
  `typeclass_22b3.rs:151` (`MonoTarget.class` qualified). The other
  lines plan flagged (217/232/295/301/341/353) are local-construction
  tests that never round-trip through the workspace — left as-is.
- iter mq.1.7: full `cargo test --workspace` 520 passed / 0 failed;
  `examples/prelude.ail.json` zero diff (confirms spec assumption 17:
  intra-prelude refs stay bare); `bench/compile_check.py` exit 0,
  `bench/cross_lang.py` exit 0; `bench/check.py` exit 1 due to 4
  improvements-beyond-tolerance (audit-ratifiable per convention,
  not a regression). Roundtrip on the new fixture: `ail check
  examples/mq1_xmod_constraint_class.ail.json` → "ok (16 symbols
  across 3 modules)", exit 0.

## Concerns

- **Boss-note recon claim was empirically wrong.** The boss-note
  said "every existing `examples/test_22b*.ail.json` class-ref field
  is intra-module under the canonical-form rule, so existing fixtures
  carry zero diff". Five test_22b* fixtures actually carried bare
  cross-module class refs and required migration: `test_22b1_orphan_third.ail.json`
  (bare `Show` from imported classmod), `test_22b1_dup_a.ail.json` +
  `test_22b1_dup_b.ail.json` (the duplicate-instance pair structurally
  relied on the pre-mq.1 bare cross-module shape), and
  `test_22b2_unbound_constraint_var.ail.json` (constraint references
  a class that doesn't exist anywhere → fired BareCrossModuleClassRef
  before reaching UnboundConstraintTypeVar). Six non-test_22b
  fixtures also needed migration (`eq_ord_polymorphic`, `eq_ord_user_adt`,
  `cmp_max_smoke`, `ctt2_collision_lib`, `ctt2_collision_main`). All
  migrations are minimal and aligned with the canonical-form rule;
  no proactive "fixture cleanup" added.
- **Duplicate-instance test restructured by structural necessity.**
  Pre-mq.1 the test relied on two coherent instances on the same
  `(class, type)` declared from different modules; post-mq.1, the
  orphan-freedom invariant prevents this shape (any second instance
  in a module that owns neither the class nor the type fires
  `OrphanInstance` first). The new `test_22b1_dup_same_module.ail.json`
  fixture lands both instances in the same module — the only post-mq.1
  way to land two on the same canonical key. The old dup_a/dup_b/entry
  fixtures stay as supporting modules (referenced by hash.rs's
  canonical-form hash pin) but no longer participate in the duplicate
  test. This is arguably a strengthening of the architecture: duplicate
  instances are now structurally impossible across coherent modules.
- **Plan's "no code change needed" claims in Task 4 Step 8 and Task 5
  Step 5 were wrong.** Task 4 Step 8 said `Origin::Class.class_name`
  qualified at "both sites" but there's only one construction site
  (the plan's recon was off-by-one). Task 5 Step 5 said mono's
  `class_index.get(class)` lookups would just work post-rekey, but
  `build_class_index` itself was bare-keyed. Both fixed inline.
- **Several adjacent fixes were necessary that the plan didn't anticipate:**
  (a) `build_module_globals` rejected `inst.class` containing `.` —
  added a `Def::Instance` carve-out so qualified class refs survive.
  (b) `check_fn`'s declared-constraint matching compared bare
  `c.class` to qualified residual `r.class` → added an inline lift via
  `qualify_class_ref_in_check`. (c) `NoInstance` Float-aware message
  arm matched on bare `"Eq" / "Ord"` — updated to `"prelude.Eq" /
  "prelude.Ord"`. (d) Coherence check's type-leg lookup couldn't
  resolve qualified head names — extended with a split-and-lookup
  fallback. None of these widened the contract; each is a one-line
  bridge keeping the post-mq.1 qualified shape consistent across the
  pipeline.
- **Tasks 3-6 ran as one coherent fix.** The plan structured them as
  sequential tasks, but Task 3 alone left several existing tests red
  (registry still bare-keyed, residual still bare). Walking the plan
  literally and expecting "green after Task 3" per the plan's Step 9
  was impossible; the four tasks form one consistent rekey.

## Known debt

- The `test_22b1_dup_a` / `test_22b1_dup_b` / `test_22b1_dup_entry`
  fixtures are now orphan from the duplicate-instance test (which
  uses `test_22b1_dup_same_module` instead). They're still
  referenced by `hash.rs:293` for the canonical-form hash pin. A
  cleanup pass could either retire them entirely (and replace the
  hash pin with the new fixture) or keep them as historical
  reference. Deferred — not load-bearing.
- `qualify_class_ref_in_check` in `ailang-check/src/lib.rs` is a
  one-line duplicate of `workspace::qualify_class_ref`. The
  alternative is exposing `qualify_class_ref` as `pub(crate)` or
  re-exporting; the duplication was the minimum-edit path. A
  consolidation tidy could land in iter 2.

## Files touched

Code:
- crates/ail/src/main.rs
- crates/ail/tests/ct1_check_cli.rs
- crates/ail/tests/typeclass_22b2.rs
- crates/ail/tests/typeclass_22b3.rs
- crates/ailang-check/src/lib.rs
- crates/ailang-check/src/mono.rs
- crates/ailang-check/tests/env_construction_pin.rs
- crates/ailang-core/src/ast.rs
- crates/ailang-core/src/hash.rs
- crates/ailang-core/src/workspace.rs
- crates/ailang-core/tests/ctt2_registry_rekey.rs

Fixtures (existing, migrated):
- examples/cmp_max_smoke.ail.json
- examples/ctt2_collision_lib.ail.json
- examples/ctt2_collision_main.ail.json
- examples/eq_ord_polymorphic.ail.json
- examples/eq_ord_user_adt.ail.json
- examples/test_22b1_dup_a.ail.json
- examples/test_22b1_dup_b.ail.json
- examples/test_22b1_dup_entry.ail.json
- examples/test_22b1_orphan_third.ail.json
- examples/test_22b2_unbound_constraint_var.ail.json

Fixtures (new):
- examples/mq1_xmod_constraint_class.ail.json
- examples/mq1_xmod_constraint_class_dep.ail.json
- examples/test_22b1_dup_classmod.ail.json
- examples/test_22b1_dup_same_module.ail.json

## Stats

bench/orchestrator-stats/2026-05-13-iter-mq.1.json
