# Iter mq.1 — Canonical-form extension for class-ref fields — Implementation Plan > **Parent spec:** `docs/specs/0023-module-qualified-class-names.md` > > **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement` to > run this plan. Steps use `- [ ]` checkboxes for tracking. **Goal:** Move the three class-reference schema fields (`InstanceDef.class`, `Constraint.class`, `SuperclassRef.class`) from bare-only to canonical form (bare for same-module, `.` for cross-module), extend ct.1's `validate_canonical_type_names` validator to enforce the new rule, qualify all workspace-internal class-name keys (`class_def_module`, `class_by_name`, registry `entries`, `ClassMethodEntry.class_name`, `class_superclasses`), and adjust test assertions that string-compare against bare class names. Dispatch and `MethodNameCollision` are unchanged in this iter. **Architecture:** Symmetric application of ct.1's existing rule to three class-ref fields. The existing `check_class_name_fields` function (which rejects `.` in all four fields today) is narrowed to `ClassDef.name` only; the three migrated fields get new `BareCrossModuleClassRef` diagnostic walks inside `validate_canonical_type_names`. Class-key qualification flows through the registry construction and the `ailang-check` `ClassMethodEntry` source insert site; all downstream consumers (synth Var-arm, mono targets, discharge) read the qualified strings transparently because they only compare and look up by exact string match. **Tech Stack:** `ailang-core` (`workspace.rs`, `ast.rs`), `ailang-check` (`lib.rs`), `ail` CLI (`main.rs` Display arm), `crates/ail/tests/typeclass_22b{2,3}.rs` (assertion-string updates). **Pre-flight verification of recon-flagged spec cost overstatement:** the spec's §"Known costs"-Iter-1 estimates 14 test fixtures migrated. Recon confirms every existing class-ref field in `examples/*.ail.json` references a class declared in the same fixture file (intra-module). Under the canonical-form rule, intra-module refs stay bare, so existing fixture content carries zero diff. Plan does NOT add work to "migrate" fixtures that need no migration; a verification step in Task 7 confirms zero existing-fixture diff. New cross-module fixtures land only where Task 3 needs them for RED-first validator pin tests. --- ## Task 1: Add `BareCrossModuleClassRef` + `BadCrossModuleClassRef` diagnostic variants + Display arms **Files:** - Modify: `crates/ailang-core/src/workspace.rs:341` (add two sibling variants, immediately after `BareCrossModuleTypeRef` / `BadCrossModuleTypeRef`) - Modify: `crates/ail/src/main.rs:1285` (two Display arms) - Test: inline in `crates/ailang-core/src/workspace.rs` `#[cfg(test)] mod tests` The two new variants are siblings of the existing `BareCrossModuleTypeRef` (workspace.rs:341) and `BadCrossModuleTypeRef` (workspace.rs:355) for class-ref fields: `BareCrossModuleClassRef` fires when a bare class ref does not resolve to a local class of the owning module; `BadCrossModuleClassRef` fires when a qualified class ref's owner is not a known module or declares no class by that name. - [ ] **Step 1: Write the RED test for both new variants' Display output** Append inside the existing `mod tests` block in `crates/ailang-core/src/workspace.rs` (after the existing `BareCrossModuleTypeRef`-Display test if any; otherwise at the end of the test module): ```rust /// mq.1.1: `BareCrossModuleClassRef` is the sibling diagnostic for /// bare cross-module class references on `InstanceDef.class`, /// `Constraint.class`, and `SuperclassRef.class`. Verifies the /// variant is constructible and its Display surface names the /// "class" wording (vs. "type" in the sibling variant). #[test] fn mq1_bare_cross_module_class_ref_display_names_class() { let err = WorkspaceLoadError::BareCrossModuleClassRef { module: "user".to_string(), name: "Show".to_string(), candidates: vec!["prelude.Show".to_string()], }; let rendered = format!("{}", err); assert!(rendered.contains("class"), "Display must name 'class' wording, got: {rendered}"); assert!(rendered.contains("Show"), "Display must echo the offending class name, got: {rendered}"); assert!(rendered.contains("prelude.Show"), "Display must list candidates, got: {rendered}"); } /// mq.1.1b: `BadCrossModuleClassRef` is the sibling diagnostic for /// qualified class references whose owner module is unknown or /// declares no class by that name. Verifies the variant is /// constructible and its Display surface names the offending /// qualified form. #[test] fn mq1_bad_cross_module_class_ref_display_names_qualified() { let err = WorkspaceLoadError::BadCrossModuleClassRef { module: "user".to_string(), name: "unknownlib.Show".to_string(), }; let rendered = format!("{}", err); assert!(rendered.contains("unknownlib.Show"), "Display must echo the qualified form, got: {rendered}"); assert!(rendered.contains("class") || rendered.contains("module"), "Display must mention class or module context, got: {rendered}"); } ``` - [ ] **Step 2: Run tests to verify they fail** Run: `cargo test --workspace -p ailang-core mq1_bare_cross_module_class_ref_display_names_class mq1_bad_cross_module_class_ref_display_names_qualified` Expected: compile error (the new variants do not yet exist). - [ ] **Step 3: Add the two variants to `WorkspaceLoadError`** Insert immediately after the existing `BareCrossModuleTypeRef` variant at `crates/ailang-core/src/workspace.rs:341-345`. Two sibling variants with the same field shapes as the corresponding type-ref variants: ```rust /// mq.1 (canonical-class-names): a class-reference field /// (`InstanceDef.class`, `Constraint.class`, or /// `SuperclassRef.class`) carries a bare name that does not resolve /// to a local class of the owning module. Under the canonical-form /// rule extended in mq.1, bare = local-class-of-owning-module; a /// bare cross-module class reference is a schema violation. /// `candidates` lists the qualified forms found by scanning the /// owning module's imports for matching class declarations. #[error( "module `{module}` contains bare class name `{name}` that does not resolve to a local class. \ AILang's `.ail.json` requires cross-module class references to be qualified. \ Candidates from imports: {candidates:?}." )] BareCrossModuleClassRef { module: String, name: String, candidates: Vec, }, /// mq.1 (canonical-class-names): a qualified class reference of /// the form `.` was encountered, but `` is /// not a known module in the workspace, or `` is known /// but declares no class by that name. Sibling of /// `BadCrossModuleTypeRef` for class references. #[error( "module `{module}` references qualified class `{name}` but the owner module is not known \ or does not declare a class by that name" )] BadCrossModuleClassRef { module: String, name: String, }, ``` - [ ] **Step 4: Add the two Display arms in the CLI** Open `crates/ail/src/main.rs`. Find the existing `W::BareCrossModuleTypeRef` arm (the one that emits the structured-diagnostic JSON with `"code"` field). Add a sibling arm immediately after it: ```rust W::BareCrossModuleClassRef { module, name, candidates } => { let mut obj = serde_json::json!({ "code": "bare-cross-module-class-ref", "module": module, "name": name, "candidates": candidates, }); println!("{}", obj); } W::BadCrossModuleClassRef { module, name } => { let mut obj = serde_json::json!({ "code": "bad-cross-module-class-ref", "module": module, "name": name, }); println!("{}", obj); } ``` (If the existing arm uses a non-JSON Display path, mirror that path exactly. The implementer reads the existing arms for `W::BareCrossModuleTypeRef` and `W::BadCrossModuleTypeRef` at `crates/ail/src/main.rs:1201`-ish and matches their shapes verbatim.) - [ ] **Step 5: Run tests to verify both pass** Run: `cargo test --workspace -p ailang-core mq1_bare_cross_module_class_ref_display_names_class mq1_bad_cross_module_class_ref_display_names_qualified` Expected: both PASS. - [ ] **Step 6: Confirm no other test broke** Run: `cargo test --workspace` Expected: green (only an additive variant; no semantic change yet). --- ## Task 2: Doc-comments on the three migrated class-ref fields + `ClassDef.name` **Files:** - Modify: `crates/ailang-core/src/ast.rs:260-273` (ClassDef.name doc) - Modify: `crates/ailang-core/src/ast.rs:277-284` (SuperclassRef.class doc) - Modify: `crates/ailang-core/src/ast.rs:310-323` (InstanceDef.class doc) - Modify: `crates/ailang-core/src/ast.rs:343-349` (Constraint.class doc) - [ ] **Step 1: Update `ClassDef.name` doc-comment** In `crates/ailang-core/src/ast.rs:260-273`, find the existing `/// Class name (capitalised by convention).` line above `pub name: String`. Replace with: ```rust /// Class name (capitalised by convention). Bare — symmetric to /// `TypeDef.name`, the field is the defining-site context, not /// a reference. Cross-module class references live in /// `InstanceDef.class`, `Constraint.class`, and /// `SuperclassRef.class`; those fields carry the canonical form /// (bare for same-module, `.` for cross-module) /// per mq.1. pub name: String, ``` - [ ] **Step 2: Update `SuperclassRef.class` doc-comment** In `crates/ailang-core/src/ast.rs:277-284`, replace the `/// Superclass name.` line above `pub class: String` with: ```rust /// Superclass name in canonical form (mq.1): bare for a /// same-module class, `.` for a cross-module /// class. Symmetric to ct.1's `Type::Con.name` rule. pub class: String, ``` - [ ] **Step 3: Update `InstanceDef.class` doc-comment** In `crates/ailang-core/src/ast.rs:312-317`, replace the `/// Class being instantiated.` line above `pub class: String` with: ```rust /// Class being instantiated, in canonical form (mq.1): bare for /// a same-module class, `.` for a cross-module /// class. Symmetric to ct.1's `Type::Con.name` rule. pub class: String, ``` - [ ] **Step 4: Update `Constraint.class` doc-comment** In `crates/ailang-core/src/ast.rs:343-349`, replace the `/// Class name.` line above `pub class: String` with: ```rust /// Class name in canonical form (mq.1): bare for a same-module /// class, `.` for a cross-module class. /// Symmetric to ct.1's `Type::Con.name` rule. pub class: String, ``` - [ ] **Step 5: Run `cargo test --workspace`** Run: `cargo test --workspace` Expected: green. Doc-only changes carry zero semantic effect. --- ## Task 3: Extend `validate_canonical_type_names` for the three class-ref fields + invert existing rejection **Files:** - Create: `examples/mq1_xmod_constraint_class.ail.json` (positive cross-module fixture, two-module pair) - Create: `examples/mq1_xmod_constraint_class_dep.ail.json` (the dependency module) - Modify: `crates/ailang-core/src/workspace.rs:921-964` (`validate_canonical_type_names`) - Modify: `crates/ailang-core/src/workspace.rs:1282-1341` (`check_class_name_fields` narrows to ClassDef.name only) - Modify: `crates/ailang-core/src/workspace.rs:2284-2377` (4 existing pin tests inverted: instance, constraint-on-fn, constraint-on-class-method, superclass) - Modify: `crates/ailang-core/src/workspace.rs:2618-2632` (on-disk fixture pin test inverted) - Test: inline in `crates/ailang-core/src/workspace.rs` `mod tests` (3 RED-first pin tests for `BareCrossModuleClassRef`, 1 positive cross-module pin test) - [ ] **Step 1: Write three RED-first pin tests for `BareCrossModuleClassRef`** Append at the end of the existing `mod tests` block in `crates/ailang-core/src/workspace.rs`: ```rust /// mq.1.3: `InstanceDef.class` carrying a bare name that does NOT /// resolve to a local class of the owning module must fire /// `BareCrossModuleClassRef`. The fixture imports a sibling module /// that declares the class under the bare name, so the candidate /// list is non-empty. #[test] fn mq1_bare_xmod_instancedef_class_fires() { let mut modules = BTreeMap::new(); // Owning module B: instance Show Int (bare class ref, but Show // is not defined in B). modules.insert( "B".to_string(), Module { name: "B".to_string(), imports: vec!["A".to_string()], defs: vec![Def::Instance(InstanceDef { class: "Show".to_string(), // <- bare, cross-module type_: Type::Con { name: "Int".to_string(), args: vec![] }, methods: vec![], doc: None, })], }, ); // Dep module A: declares class Show modules.insert( "A".to_string(), Module { name: "A".to_string(), imports: vec![], defs: vec![Def::Class(ClassDef { name: "Show".to_string(), param: "a".to_string(), superclass: None, methods: vec![], doc: None, })], }, ); let err = validate_canonical_type_names(&modules).expect_err("expected reject"); match err { WorkspaceLoadError::BareCrossModuleClassRef { module, name, candidates } => { assert_eq!(module, "B"); assert_eq!(name, "Show"); assert!(candidates.contains(&"A.Show".to_string()), "candidates: {candidates:?}"); } other => panic!("expected BareCrossModuleClassRef, got {other:?}"), } } /// mq.1.3: `Constraint.class` on a `Def::Fn` carrying a bare name /// that does not resolve to a local class fires /// `BareCrossModuleClassRef`. #[test] fn mq1_bare_xmod_constraint_class_on_fn_fires() { let mut modules = BTreeMap::new(); modules.insert( "B".to_string(), Module { name: "B".to_string(), imports: vec!["A".to_string()], defs: vec![Def::Fn(FnDef { name: "f".to_string(), ty: Type::Forall { vars: vec!["a".to_string()], constraints: vec![Constraint { class: "Show".to_string(), // <- bare, cross-module type_: Type::Var { name: "a".to_string() }, }], body: Box::new(Type::Fn { params: vec![Type::Var { name: "a".to_string() }], param_modes: vec![ParamMode::Move], ret: Box::new(Type::Con { name: "Unit".to_string(), args: vec![] }), effects: vec![], }), }, params: vec!["x".to_string()], body: Term::Var { name: "x".to_string() }, doc: None, })], }, ); modules.insert( "A".to_string(), Module { name: "A".to_string(), imports: vec![], defs: vec![Def::Class(ClassDef { name: "Show".to_string(), param: "a".to_string(), superclass: None, methods: vec![], doc: None, })], }, ); let err = validate_canonical_type_names(&modules).expect_err("expected reject"); match err { WorkspaceLoadError::BareCrossModuleClassRef { module, name, .. } => { assert_eq!(module, "B"); assert_eq!(name, "Show"); } other => panic!("expected BareCrossModuleClassRef, got {other:?}"), } } /// mq.1.3: `SuperclassRef.class` on a `Def::Class` carrying a bare /// name that does not resolve to a local class fires /// `BareCrossModuleClassRef`. #[test] fn mq1_bare_xmod_superclassref_class_fires() { let mut modules = BTreeMap::new(); modules.insert( "B".to_string(), Module { name: "B".to_string(), imports: vec!["A".to_string()], defs: vec![Def::Class(ClassDef { name: "MyOrd".to_string(), param: "a".to_string(), superclass: Some(SuperclassRef { class: "Eq".to_string(), // <- bare, cross-module type_: "a".to_string(), }), methods: vec![], doc: None, })], }, ); modules.insert( "A".to_string(), Module { name: "A".to_string(), imports: vec![], defs: vec![Def::Class(ClassDef { name: "Eq".to_string(), param: "a".to_string(), superclass: None, methods: vec![], doc: None, })], }, ); let err = validate_canonical_type_names(&modules).expect_err("expected reject"); match err { WorkspaceLoadError::BareCrossModuleClassRef { module, name, .. } => { assert_eq!(module, "B"); assert_eq!(name, "Eq"); } other => panic!("expected BareCrossModuleClassRef, got {other:?}"), } } ``` - [ ] **Step 2: Write the positive cross-module pin test** Append in the same `mod tests` block: ```rust /// mq.1.3: a qualified `InstanceDef.class` referencing a class in /// an imported module is the canonical form post-mq.1 and is /// accepted by `validate_canonical_type_names`. Symmetric to ct.1's /// "qualified Type::Con resolves" positive path. #[test] fn mq1_qualified_instancedef_class_accepted() { let mut modules = BTreeMap::new(); modules.insert( "B".to_string(), Module { name: "B".to_string(), imports: vec!["A".to_string()], defs: vec![Def::Instance(InstanceDef { class: "A.Show".to_string(), // <- qualified, cross-module type_: Type::Con { name: "Int".to_string(), args: vec![] }, methods: vec![], doc: None, })], }, ); modules.insert( "A".to_string(), Module { name: "A".to_string(), imports: vec![], defs: vec![Def::Class(ClassDef { name: "Show".to_string(), param: "a".to_string(), superclass: None, methods: vec![], doc: None, })], }, ); validate_canonical_type_names(&modules).expect("qualified A.Show must be accepted"); } ``` - [ ] **Step 3: Run the four new tests to confirm RED** Run: ``` cargo test --workspace -p ailang-core mq1_bare_xmod_instancedef_class_fires \ mq1_bare_xmod_constraint_class_on_fn_fires \ mq1_bare_xmod_superclassref_class_fires \ mq1_qualified_instancedef_class_accepted ``` Expected: the three bare tests panic with "expected BareCrossModuleClassRef, got QualifiedClassName" (because `check_class_name_fields` still rejects bare under no current rule — actually it accepts bare and rejects qualified, so the bare tests will likely panic with "expected reject" because no error fires); the qualified test panics because `check_class_name_fields` rejects qualified forms. - [ ] **Step 4: Implement the three new field walks in `validate_canonical_type_names`** In `crates/ailang-core/src/workspace.rs`, locate the per-def loop at ~line 945-960 inside `validate_canonical_type_names`. Add three new walks calling a shared helper (or inlining the three-rule check from `check_type_con_name` at workspace.rs:968-1028). The helper signature: ```rust /// mq.1: apply the canonical-form rule to a class-reference field. /// Same three-rule structure as `check_type_con_name` for type refs: /// qualified known-module ⇒ accept; bare same-module-class ⇒ accept; /// bare cross-module ⇒ fire `BareCrossModuleClassRef` with candidates /// scanned from the owning module's imports. fn check_class_ref( owning_module: &str, class_ref: &str, class_def_module: &BTreeMap, // bare-class -> defining-module imports: &[String], field_label: &'static str, ) -> Result<(), WorkspaceLoadError> { if let Some((owner, bare)) = class_ref.split_once('.') { // Qualified form: owner must be a known module that declares the class. if !class_def_module.iter().any(|(k, m)| m == owner && k == bare) { return Err(WorkspaceLoadError::BadCrossModuleClassRef { module: owning_module.to_string(), name: class_ref.to_string(), }); } Ok(()) } else { // Bare form: must resolve to a class in the owning module. let defining = class_def_module.get(class_ref); match defining { Some(m) if m == owning_module => Ok(()), _ => { // Cross-module bare: list qualified candidates from imports. let candidates: Vec = imports .iter() .filter_map(|imp| { if class_def_module.get(class_ref).map(|m| m == imp).unwrap_or(false) { Some(format!("{imp}.{class_ref}")) } else { None } }) .collect(); Err(WorkspaceLoadError::BareCrossModuleClassRef { module: owning_module.to_string(), name: class_ref.to_string(), candidates, }) } } } } ``` Note: `BadCrossModuleClassRef` is the sibling of `BadCrossModuleTypeRef` (`workspace.rs:355`). The implementer adds this variant to the enum in the same step (insert immediately after `BadCrossModuleTypeRef`), sibling Display arm in `main.rs` if not auto-Display-derived. Build the `class_def_module: BTreeMap` (bare-class → defining-module) once at the top of `validate_canonical_type_names` by walking every module's `Def::Class` entries, then pass it into the helper. Add the three call-site walks inside the existing per-def loop: ```rust for def in &m.defs { // ...existing Type::Con walks... match def { Def::Instance(id) => { check_class_ref(mod_name, &id.class, &class_def_module, &m.imports, "InstanceDef.class")?; } Def::Class(cd) => { if let Some(sc) = &cd.superclass { check_class_ref(mod_name, &sc.class, &class_def_module, &m.imports, "SuperclassRef.class")?; } for cmth in &cd.methods { if let Type::Forall { constraints, .. } = &cmth.ty { for c in constraints { check_class_ref(mod_name, &c.class, &class_def_module, &m.imports, "Constraint.class")?; } } } } Def::Fn(fd) => { if let Type::Forall { constraints, .. } = &fd.ty { for c in constraints { check_class_ref(mod_name, &c.class, &class_def_module, &m.imports, "Constraint.class")?; } } } _ => {} } } ``` - [ ] **Step 5: Narrow `check_class_name_fields` to `ClassDef.name`-only** In `crates/ailang-core/src/workspace.rs:1287-1341`, delete the three rejection arms for `SuperclassRef.class`, `InstanceDef.class`, `Constraint.class` (lines 1307-1320, 1323-1328, 1329-1338). Keep only the `ClassDef.name` arm at 1303-1306. The function body becomes: ```rust fn check_class_name_fields( def: &Def, owning_module: &str, ) -> Result<(), WorkspaceLoadError> { if let Def::Class(cd) = def { if cd.name.contains('.') { return Err(WorkspaceLoadError::QualifiedClassName { module: owning_module.to_string(), name: cd.name.clone(), field: "ClassDef.name", }); } } Ok(()) } ``` Update the function doc-comment to reflect the post-mq.1 scope. - [ ] **Step 6: Invert the four in-test pin tests that rejected qualified class-ref forms** In `crates/ailang-core/src/workspace.rs`, locate the existing tests at 2284-2377 (three tests at 2284, 2312, 2341, plus the classdef-method-constraint test at 2541). For each of the four tests that today asserts `QualifiedClassName` on a qualified class-ref field in `InstanceDef.class` / `Constraint.class` / `SuperclassRef.class`, invert the assertion: the qualified form is now ACCEPTED. Replace the panic-match block with: ```rust validate_canonical_type_names(&modules) .expect("qualified is the canonical form post-mq.1"); ``` Update each test's doc-comment to reflect the inverted intent. The test at 2255 (`ClassDef.name`-rejects-qualified) stays unchanged. - [ ] **Step 7: Invert the on-disk fixture pin test at 2618-2632** If the on-disk fixture test at workspace.rs:2618 covers a qualified-form rejection on one of the three migrated fields, invert it the same way as Step 6 (expect-accept post-mq.1). If it covers `ClassDef.name` specifically, it stays unchanged. The implementer checks the test body to determine which case. - [ ] **Step 8: Run the four mq.1.3 tests to confirm GREEN** Run: ``` cargo test --workspace -p ailang-core mq1_bare_xmod_instancedef_class_fires \ mq1_bare_xmod_constraint_class_on_fn_fires \ mq1_bare_xmod_superclassref_class_fires \ mq1_qualified_instancedef_class_accepted ``` Expected: all four PASS. - [ ] **Step 9: Run all workspace tests** Run: `cargo test --workspace -p ailang-core workspace` Expected: green, including the inverted pin tests. - [ ] **Step 10: Create positive on-disk cross-module fixture pair** Create `examples/mq1_xmod_constraint_class.ail.json` (consumer module with a qualified `Constraint.class` referencing the dep module's class): ```json { "schema": "ailang/v0", "name": "mq1_xmod_constraint_class", "imports": ["mq1_xmod_constraint_class_dep"], "defs": [ { "kind": "fn", "name": "useShow", "type": { "k": "forall", "vars": ["a"], "constraints": [ { "class": "mq1_xmod_constraint_class_dep.Show", "type": { "k": "var", "name": "a" } } ], "body": { "k": "fn", "params": [{ "k": "var", "name": "a" }], "param_modes": ["borrow"], "ret": { "k": "con", "name": "Unit" }, "effects": [] } }, "params": ["x"], "body": { "t": "var", "name": "x" } } ] } ``` Create `examples/mq1_xmod_constraint_class_dep.ail.json`: ```json { "schema": "ailang/v0", "name": "mq1_xmod_constraint_class_dep", "imports": [], "defs": [ { "kind": "class", "name": "Show", "param": "a", "methods": [ { "name": "show", "type": { "k": "fn", "params": [{ "k": "var", "name": "a" }], "param_modes": ["borrow"], "ret": { "k": "con", "name": "Str" }, "effects": [] } } ] } ] } ``` (The exact schema-field names must match what the existing fixtures under `examples/` use. Implementer copies the structure from `examples/test_22b2_xmod_classmod.ail.json` and adapts the consumer side to use the qualified class ref.) - [ ] **Step 11: Add the on-disk fixture pin test** In `crates/ailang-core/src/workspace.rs` test module, add: ```rust /// mq.1.3: on-disk fixture pin — a workspace where the consumer's /// `Constraint.class` references a class in an imported module via /// the qualified form loads cleanly. Symmetric to ct.1's positive /// cross-module type-ref fixture. #[test] fn mq1_xmod_constraint_class_fixture_loads() { let ws = load_workspace_from_paths(&[ Path::new("examples/mq1_xmod_constraint_class.ail.json"), Path::new("examples/mq1_xmod_constraint_class_dep.ail.json"), ]) .expect("workspace must load with qualified Constraint.class"); assert!(ws.modules.contains_key("mq1_xmod_constraint_class")); assert!(ws.modules.contains_key("mq1_xmod_constraint_class_dep")); } ``` (The exact loader entry point may differ; the implementer matches the pattern used by other on-disk fixture pin tests in the same file.) - [ ] **Step 12: Run the new on-disk fixture test** Run: `cargo test --workspace -p ailang-core mq1_xmod_constraint_class_fixture_loads` Expected: PASS. --- ## Task 4: Re-key `build_registry` to qualified class names (Pass-1 + Pass-2 + lookup sites) **Files:** - Modify: `crates/ailang-core/src/workspace.rs:521-545` (Pass-1: `class_def_module` + `class_by_name` re-key) - Modify: `crates/ailang-core/src/workspace.rs:629-755` (Pass-2: `entries` key + 4 consumer sites) - [ ] **Step 1: Introduce the qualifier helper at module scope** Insert near the top of `workspace.rs` (before `build_registry`): ```rust /// mq.1: take a class-ref field value (`InstanceDef.class`, /// `SuperclassRef.class`, `Constraint.class`) and a defining context, /// and produce the qualified workspace key. Bare ⇒ prepend the /// caller_module; qualified ⇒ as-is. Symmetric to ct.1's /// `normalize_type_for_registry` for `Type::Con`. fn qualify_class_ref(class_ref: &str, caller_module: &str) -> String { if class_ref.contains('.') { class_ref.to_string() } else { format!("{caller_module}.{class_ref}") } } ``` - [ ] **Step 2: Re-key Pass-1 `class_def_module` and `class_by_name`** In `build_registry` at `workspace.rs:521-545`, replace: ```rust let mut class_def_module: BTreeMap = BTreeMap::new(); let mut type_def_module: BTreeMap<(String, String), String> = BTreeMap::new(); let mut class_by_name: BTreeMap = BTreeMap::new(); for (mod_name, m) in modules { for def in &m.defs { match def { Def::Class(c) => { class_def_module.insert(c.name.clone(), mod_name.clone()); class_by_name.insert(c.name.clone(), c); } // ... } } } ``` with: ```rust let mut class_def_module: BTreeMap = BTreeMap::new(); let mut type_def_module: BTreeMap<(String, String), String> = BTreeMap::new(); let mut class_by_name: BTreeMap = BTreeMap::new(); for (mod_name, m) in modules { for def in &m.defs { match def { Def::Class(c) => { let qualified = format!("{mod_name}.{}", c.name); class_def_module.insert(qualified.clone(), mod_name.clone()); class_by_name.insert(qualified, c); } // ... } } } ``` Keys are now qualified; values unchanged. - [ ] **Step 3: Update the Pass-2 coherence lookup** At `workspace.rs:645-648`: ```rust let class_mod = class_def_module .get(&inst.class) .cloned() .unwrap_or_else(|| "".into()); ``` becomes: ```rust let inst_class_key = qualify_class_ref(&inst.class, mod_name); let class_mod = class_def_module .get(&inst_class_key) .cloned() .unwrap_or_else(|| "".into()); ``` - [ ] **Step 4: Update the Pass-2 method-completeness lookup** At `workspace.rs:695`: ```rust if let Some(class_def) = class_by_name.get(&inst.class) { ``` becomes: ```rust if let Some(class_def) = class_by_name.get(&inst_class_key) { ``` (Uses the `inst_class_key` already computed in Step 3.) - [ ] **Step 5: Update the Pass-2 `entries` map key** At `workspace.rs:678`: ```rust let key = (inst.class.clone(), type_hash); ``` becomes: ```rust let key = (inst_class_key.clone(), type_hash); ``` The `DuplicateInstance` diagnostic at lines 680-686 receives the qualified class name via `inst.class` (still the surface schema value) or via `inst_class_key`; the diagnostic surface uses `inst.class` (the on-disk shape) for the human-readable message. Implementer chooses: the simpler option keeps `inst.class` in the diagnostic and uses `inst_class_key` only for the registry key. No diagnostic-wording change required. - [ ] **Step 6: Update the superclass-instance check** At `workspace.rs:744-754`, find the existing `sc.class`-keyed superclass-instance lookup. Replace the key construction with the qualified form: ```rust let sc_class_key = qualify_class_ref(&sc.class, class_mod_of_inst); let sc_key = (sc_class_key, type_hash.clone()); ``` (The `class_mod_of_inst` variable name is illustrative; implementer identifies the correct "owner module" for the superclass ref — typically the `class_def_module` value for the parent class, since `SuperclassRef` is inside a `ClassDef` and lives in that class's module.) - [ ] **Step 7: Update `Origin::Class.class_name` to qualified** At `workspace.rs:561 and 582` (the two construction sites for `Origin::Class { class_name, module }` inside the `MethodNameCollision` pre-pass), wrap `c.name.clone()` in the qualifier: ```rust let origin = Origin::Class { class_name: format!("{mod_name}.{}", c.name), module: mod_name.clone(), }; ``` Both sites. The `Display::format` impl at `workspace.rs:567` stays unchanged; output becomes `"class M.Show (in M)"` (informative even if redundant). - [ ] **Step 8: Run the targeted `build_registry` tests** Run: `cargo test --workspace -p ailang-core registry` Expected: green. Existing registry tests assert outcomes by class-name match; qualified strings still match symmetrically. - [ ] **Step 9: Run all workspace tests** Run: `cargo test --workspace -p ailang-core` Expected: green. Pin tests that string-match on `Origin::Class.class_name` will fail here — those are Task 6's scope. --- ## Task 5: Qualify `ClassMethodEntry.class_name` + `class_superclasses` in `ailang-check` **Files:** - Modify: `crates/ailang-check/src/lib.rs:1102-1123` (ClassMethodEntry doc) - Modify: `crates/ailang-check/src/lib.rs:1195-1212` (build_module_globals Class arm) - Modify: `crates/ailang-check/src/lib.rs:1303-1312` (class_superclasses build) - [ ] **Step 1: Update `ClassMethodEntry.class_name` doc-comment** In `crates/ailang-check/src/lib.rs:1102-1123`, find the doc-comment on the `class_name` field. Append: ```rust /// (mq.1: carries the qualified form `.`; /// flows downstream into `ResidualConstraint.class` and /// `MonoTarget::ClassMethod.class` unchanged.) ``` - [ ] **Step 2: Qualify the insert site at lib.rs:1205** In `build_module_globals` at `crates/ailang-check/src/lib.rs:1195-1212`, locate the `Def::Class` arm that inserts into `class_methods`. Find the construction site of `ClassMethodEntry { class_name: cd.name.clone(), ... }` (approximately line 1205). Replace with: ```rust let qualified_class = format!("{mname}.{}", cd.name); class_methods.insert( method.name.clone(), ClassMethodEntry { class_name: qualified_class.clone(), // ...other fields unchanged }, ); ``` (The variable `mname` is the iteration variable for the per-module loop; implementer confirms the exact name in scope.) - [ ] **Step 3: Qualify `class_superclasses` key and value** In `crates/ailang-check/src/lib.rs:1303-1312`, find the `class_superclasses` build loop. The current shape is approximately: ```rust for (_mod_name, m) in &ws.modules { for def in &m.defs { if let Def::Class(cd) = def { if let Some(sc) = &cd.superclass { class_superclasses.insert(cd.name.clone(), sc.class.clone()); } } } } ``` Replace with: ```rust for (mod_name, m) in &ws.modules { for def in &m.defs { if let Def::Class(cd) = def { if let Some(sc) = &cd.superclass { let class_qualified = format!("{mod_name}.{}", cd.name); let sc_qualified = if sc.class.contains('.') { sc.class.clone() } else { format!("{mod_name}.{}", sc.class) }; class_superclasses.insert(class_qualified, sc_qualified); } } } } ``` (If the loop variable is `_mod_name` today, rename to `mod_name`. If the map type signature carries explicit `String` keys/values, leave it as-is; only the value content changes.) - [ ] **Step 4: Run targeted ailang-check tests** Run: `cargo test --workspace -p ailang-check` Expected: green at the lib level; pin tests in `crates/ail/tests/` asserting against bare class strings are Task 6's scope. - [ ] **Step 5: Spot-check synth Var-arm + mono targets compile** Run: `cargo build --workspace` Expected: green. The Var-arm at `lib.rs:1968` reads `cm.class_name` (now qualified); residual emission at line 1991 carries the qualified string. Mono's `class_index.get(class)` lookups (in `mono.rs:137`, 634, 1185) read qualified keys and match qualified `MonoTarget.class` values. No code change needed in these consumers. --- ## Task 6: Update test-assertion strings affected by qualification migration **Files:** - Modify: `crates/ailang-core/src/workspace.rs:1937-1996` (`MethodNameCollision` pin tests) - Modify: `crates/ail/tests/typeclass_22b2.rs:42` (class_method_class assertion) - Modify: `crates/ail/tests/typeclass_22b3.rs:151, 217, 232, 295, 301, 341, 353` (class-string assertions) - [ ] **Step 1: Update `MethodNameCollision` pin tests at workspace.rs:1937-1996** Open `crates/ailang-core/src/workspace.rs` and locate the two `MethodNameCollision` pin tests (`class_class_method_name_collision_fires`, `class_fn_method_name_collision_fires` — exact names per recon). For each pin test, find the assertion lines that match against the old bare-class format. The shapes are typically: ```rust assert!(first_origin.starts_with("class A"), ...); ``` Replace with the post-mq.1 qualified form (the actual module name in the fixture is what determines the prefix): ```rust // mq.1: class_name now qualified — the Display format emits // "class . (in )" assert!(first_origin.starts_with("class m.A") || first_origin.contains(".A"), "got first_origin: {first_origin}"); ``` (The exact module name `m` is whatever the test fixture declares. Implementer reads each test's Module construction to derive the right prefix.) - [ ] **Step 2: Update `typeclass_22b2.rs:42`** In `crates/ail/tests/typeclass_22b2.rs:42`, find: ```rust assert_eq!( mod_globals.class_method_class("show"), Some("Show"), ); ``` Determine the module name in the fixture (the test loads a fixture named `test_22b2_class_method_lookup` per recon), and replace with: ```rust // mq.1: class_name now qualified assert_eq!( mod_globals.class_method_class("show"), Some("test_22b2_class_method_lookup.Show"), ); ``` - [ ] **Step 3: Update `typeclass_22b3.rs` class-string assertions** In `crates/ail/tests/typeclass_22b3.rs`, locate the assertion sites identified by recon (lines 151, 217, 232, 295, 301, 341, 353). Typical patterns: ```rust assert_eq!(class, "Show", ...); assert_eq!(class, "Foo", ...); assert_eq!(class, "Greet", ...); ``` Replace each with the qualified form. The module prefix depends on the test's fixture/Module construction: - Inline `Module { name: "m", ... }` tests → prefix `"m."`. - On-disk-fixture tests → the fixture's module name (typically the filename without `.ail.json`). Examples: ```rust // mq.1: qualified class names assert_eq!(class, "test_22b2_instance_present.Show", ...); assert_eq!(class, "m.Foo", ...); assert_eq!(class, "m.Greet", ...); ``` Implementer reads each test's fixture context to pick the right prefix. Each assertion update is one line. - [ ] **Step 4: Run the workspace assertion-touched tests** Run: ``` cargo test --workspace -p ailang-core workspace::tests::class_class_method_name_collision_fires \ workspace::tests::class_fn_method_name_collision_fires ``` Expected: PASS. - [ ] **Step 5: Run the typeclass test files** Run: ``` cargo test --workspace --test typeclass_22b2 --test typeclass_22b3 ``` Expected: PASS. --- ## Task 7: Integration verification — full workspace + bench scripts + roundtrip **Files:** none modified; verification only. - [ ] **Step 1: Full `cargo test --workspace`** Run: `cargo test --workspace` Expected: green. All 16 binary-test suites pass (~600 tests). - [ ] **Step 2: Verify zero diff on `examples/prelude.ail.json`** Run: `git diff -- examples/prelude.ail.json` Expected: no output (spec assumption 17: intra-prelude class refs all stay bare, prelude unchanged). - [ ] **Step 3: Verify zero diff on the 14 `test_22b*` fixtures** Run: `git diff -- examples/test_22b*.ail.json` Expected: no output (recon-confirmed: all existing class refs in those fixtures are intra-module; canonical-form rule keeps them bare). - [ ] **Step 4: Bench script `bench/check.py`** Run: `python3 bench/check.py` Expected: exit 0. (If exit non-zero, audit-ratify per audit-skill convention — note in the journal whether the regression is in-scope or spurious.) - [ ] **Step 5: Bench script `bench/compile_check.py`** Run: `python3 bench/compile_check.py` Expected: exit 0 OR audit-ratified. - [ ] **Step 6: Bench script `bench/cross_lang.py`** Run: `python3 bench/cross_lang.py` Expected: exit 0 OR audit-ratified. - [ ] **Step 7: Roundtrip the new on-disk fixture** Run: ``` cargo run -p ail -- check examples/mq1_xmod_constraint_class.ail.json ``` Expected: exit 0, no diagnostics. - [ ] **Step 8: Confirm the per-iter journal placeholder is staged** The implementer leaves `docs/journals/2026-05-13-iter-mq.1.md` (or the date the iter actually finishes) in the working tree with a draft entry covering: tasks ran, deviations from plan (if any), spec-cost-claim discrepancy (the "14 fixtures migrated" claim was over- stated; recon-confirmed zero existing-fixture diff), test counts, bench outcomes. Append a pointer line to `docs/journals/INDEX.md`. The Boss commits.