# iter ctt.2 — `Registry.type_def_module` re-key to tuple-keyed map

**Date:** 2026-05-12
**Started from:** 548ebf8d51758ab9f156b266e0baa59c78b14034
**Status:** DONE
**Tasks completed:** 3 of 3

## Summary

`Registry.type_def_module` re-keyed from `BTreeMap<String, String>` to
`BTreeMap<(String, String), String>` keyed by `(owning_module,
bare_name)`. A new `caller_module: &str` parameter threads through
`normalize_type_for_registry` and `Registry::normalize_type_for_lookup`,
and all four `ailang-check` consumer sites pass the correct
module-context per the plan's Design Notes block. A regression test
plus a three-module fixture exercise the bare-name collision the
re-key fixes — two modules each declaring `type Foo` now register
under distinct canonical keys instead of tripping the silent-overwrite
defect. `cargo test --workspace` green (16 binary-test suites,
~600 tests total, zero failures).

## Per-task notes

- iter ctt.2.1: RED-first — three fixture JSON files
  (`ctt2_collision_{cls,lib,main}.ail.json`) plus
  `crates/ailang-core/tests/ctt2_registry_rekey.rs`. Test goes
  RED today. Failure shape was `OrphanInstance` rather than the
  plan's predicted `DuplicateInstance` (a) or missing-hash (b) —
  see Concerns.
- iter ctt.2.2: GREEN — atomic re-key edit pass. Touched the field
  type + doc-comment, `Registry::normalize_type_for_lookup`
  signature + body, pass-1 declaration + insert,
  pass-2 coherence-check lookup + canonical-form hashing call,
  `normalize_type_for_registry` signature + doc-comment + body
  (recursive calls thread caller_module everywhere), the inline
  `ct1_5a_normalize_recurses_into_forall_constraints` test
  at workspace.rs:2543, and all four `ailang-check` consumer sites
  at the module-context dictated by the plan's Design Notes
  (lib.rs:1640 → `env.current_module.as_str()`, mono.rs:121 →
  `defining_module.as_str()`, mono.rs:624 and :1175 → `module_name`).
  Clean `cargo build --workspace` at the end of the pass.
- iter ctt.2.3: workspace-wide test gate. New test goes GREEN.
  Full `cargo test --workspace` green, no test in any crate
  regressed. The plan's Step 2 hypothesis ("a fixture under
  `examples/` might legitimately exercise a bare cross-module
  type reference") did not materialise — the existing corpus
  uses canonical-form (qualified) cross-module type refs
  exclusively, so the tightening had no surface.

## Concerns

- Task 1 RED-failure shape diverged from the plan's predicted
  shapes (a) `DuplicateInstance` and (b) missing-`contains_key`.
  The plan's verbatim two-module fixture pair (lib imports main +
  main imports lib + lib-side instance with qualified class
  `ctt2_collision_main.MyC`) had two independent structural
  defects against the loaded codebase invariants:
  1. The workspace loader rejects import cycles
     (`WorkspaceLoadError::Cycle`) at module-name granularity —
     the plan's claim "Two-way import is supported; workspace
     loader DFS handles cycles at module-name granularity" is
     empirically false. Confirmed by inspection at
     `workspace.rs:1410-1413` and `:1451-1454`.
  2. `validate_canonical_type_names` rejects qualified
     `InstanceDef.class` with `QualifiedClassName` (see test
     `ct1_validator_rejects_qualified_instancedef_class` at
     workspace.rs:2321) — the plan's lib-side instance with
     `class: "ctt2_collision_main.MyC"` would have been rejected
     even without the cycle.

  Implementer-phase repair: introduced a third module
  `ctt2_collision_cls.ail.json` carrying `class MyC`, broke the
  cycle (both lib and main import cls only; main also imports
  lib so the loader DFS reaches it), and dropped the qualifier
  from both instance heads (both declare bare `class: "MyC"`
  satisfying coherence via the type's module). The plan's
  *core* RED-condition is preserved: two modules each declaring
  `type Foo` with `instance MyC Foo` collide on the pre-ctt.2
  bare-name `type_def_module` key.

  The actually-observed RED shape was `OrphanInstance` rather
  than `DuplicateInstance` because the pass-2 coherence check
  (`workspace.rs:656-659`) uses the bare-name lookup *before*
  the duplicate-uniqueness check fires — lib's `Foo` and main's
  `Foo` both resolve to whichever module won the alphabetical
  race (`ctt2_collision_main` beats `ctt2_collision_lib`
  lexicographically), so lib's instance is no longer coherent
  ("type lives in main, class lives in cls, instance lives in
  lib — neither matches"). The re-key fixes this consumer site
  simultaneously with the duplicate-check site because both go
  through the same `type_def_module` lookup, now tuple-keyed.

  The deviation from the plan's verbatim fixtures is an
  implementer-phase NEEDS_CONTEXT repair that the
  user-no-stop-instruction permitted in-line; the plan's *intent*
  (RED-first against the bare-name collision) is achieved and
  the GREEN-state matches the plan's acceptance criteria
  unchanged. Boss may want to back-edit the plan or amend the
  spec to note the multi-module fixture shape if this kind of
  collision regression returns later.

## Known debt

- The `OrphanInstance` consumer site at `workspace.rs:656-659`
  is fixed by ctt.2 but the diagnostic-shape change (orphan
  instead of duplicate-on-loser) is not separately tested. The
  new test asserts the GREEN state directly (both entries land
  under distinct keys) which transitively exercises both
  consumer sites, but neither the orphan nor the duplicate
  diagnostic shapes are pinned by a dedicated RED test. Adding
  one is plausibly out-of-scope for ctt.2.

## Files touched

Modified (3):
- `crates/ailang-check/src/lib.rs` — single call-site update at
  the NoInstance check inside `check_fn`.
- `crates/ailang-check/src/mono.rs` — three call-site updates
  (monomorphise_workspace at :121, collect_mono_targets at :624,
  collect_residuals_ordered at :1175).
- `crates/ailang-core/src/workspace.rs` — field re-key,
  `Registry::normalize_type_for_lookup` signature,
  `normalize_type_for_registry` signature + body,
  pass-1 declaration + insert,
  pass-2 coherence-check lookup + canonical-form hashing call,
  inline test `ct1_5a_normalize_recurses_into_forall_constraints`
  at :2543; doc-comments at field-decl and at both
  `normalize_…` fns.

Added (4):
- `crates/ailang-core/tests/ctt2_registry_rekey.rs` — regression test.
- `examples/ctt2_collision_cls.ail.json` — class-bearing module
  (not in plan; required to break the cycle in the plan's
  verbatim fixture design).
- `examples/ctt2_collision_lib.ail.json` — `type Foo = MkLib` +
  bare `instance MyC Foo`.
- `examples/ctt2_collision_main.ail.json` — `type Foo = MkMain` +
  bare `instance MyC Foo`, plus imports of both `cls` and `lib`.

## Stats

bench/orchestrator-stats/2026-05-12-iter-ctt.2.json
