# 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, `<module>.<Class>` 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<String>,
},

/// mq.1 (canonical-class-names): a qualified class reference of
/// the form `<owner>.<class>` was encountered, but `<owner>` is
/// not a known module in the workspace, or `<owner>` 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, `<module>.<Class>` 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, `<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, `<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, `<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<String, String>, // 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<String> = 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<String, String>` (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 <field> 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<String, String> = BTreeMap::new();
let mut type_def_module: BTreeMap<(String, String), String> = BTreeMap::new();
let mut class_by_name: BTreeMap<String, &ClassDef> = 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<String, String> = BTreeMap::new();
let mut type_def_module: BTreeMap<(String, String), String> = BTreeMap::new();
let mut class_by_name: BTreeMap<String, &ClassDef> = 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(|| "<unknown-class>".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(|| "<unknown-class>".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 `<defining_module>.<Class>`;
/// 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 <module>.<Class> (in <module>)"
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.
