Files
AILang/docs/plans/0022-23.1-ordering-and-prelude-skeleton.md
T
Brummel 832375f2ac convention: counter-prefix file naming across docs/specs/, docs/plans/, design/contracts/, design/models/
All 176 files in the four accumulating directories now use a
zero-padded 4-digit counter prefix that reflects creation order
(`NNNN-slug.md`). The counter is assigned per directory in strict
git-log creation order; ties broken alphabetically by original name.
The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is
dropped — the date is recoverable from git log and the counter
carries the ordering.

A file's counter is stable for the life of the file: never reassigned,
never reused, never compacted. Deleted files retire their counter;
subsequent files do not fill the gap. This is the property that lets
cross-references stay literal — refs use the full filename including
the counter (`design/contracts/0007-honesty-rule.md`) so they grep
cleanly and resolve directly without a glob step.

313 cross-references updated across .md/.rs/.toml/.c/.json files
(test pins, include_str! paths, design-INDEX entries, baseline notes,
runtime C comments, inter-contract markdown links incl. bare basename
and `../models/foo.md` forms).

CLAUDE.md gets a new "File-naming convention" section spelling out
the rule and rationale. skills/brainstorm/SKILL.md and
skills/planner/SKILL.md updated so new spec/plan creation produces
counter-prefixed names from the start.

The full test suite (cargo test --workspace) passes.
2026-05-28 13:31:31 +02:00

27 KiB

Iteration 23.1 — Ordering ADT + Prelude Skeleton + Auto-Load — Implementation Plan

Parent spec: docs/specs/2026-05-10-23-eq-ord-prelude.md

For agentic workers: REQUIRED SUB-SKILL: use skills/implement to run this plan. Steps use - [ ] checkboxes for tracking.

Goal: Ship the Ordering ADT (data Ordering = LT | EQ | GT) inside a new prelude module at examples/prelude.ail.json, auto-loaded into every workspace via load_workspace, with the prelude implicitly imported into every user module so its ctors (LT, EQ, GT) are bare-resolvable. Verify end-to-end via a fixture that pattern-matches on a hard-coded LT value and prints 1.

Architecture:

  1. The prelude module file examples/prelude.ail.json is embedded into crates/ailang-core at compile time via include_str!, so the loader needs no on-disk resolution; the same file remains the LLM-author-readable artefact.
  2. load_workspace parses and inserts the embedded prelude into ws.modules after the DFS over the user's imports finishes (and before validate_classdefs / build_registry), so every workspace sees the prelude.
  3. check_in_workspace adds the string "prelude" to env.imports for every non-prelude module before body-check runs, so the existing bare-ctor fallback (Iter 15a, lib.rs:2399) resolves LT / EQ / GT through env.module_types["prelude"].
  4. An E2E fixture exercises Ordering through the full pipeline (typecheck → codegen → link → run); existing ADT codegen handles zero-field ctors with no new arms.

Tech Stack: Rust (ailang-core, ailang-check, ail), JSON fixture authoring, no new C runtime work.

Files this plan creates or modifies:

  • Create: examples/prelude.ail.json — Form-A JSON, module name prelude, contains only the Ordering type def (three 0-arg ctors LT, EQ, GT).
  • Modify: crates/ailang-core/src/workspace.rsload_workspace embeds and injects the prelude module; new private load_prelude() helper parses the embedded JSON; new test test_load_workspace_auto_injects_prelude asserts presence after load.
  • Modify: crates/ailang-check/src/lib.rscheck_in_workspace adds "prelude" to env.imports for every non-prelude module during env construction.
  • Create: examples/ordering_match.ail.json — fixture: user module with no explicit imports, pattern-matches on a hard-coded LT ctor in (match LT { LT => 1, EQ => 2, GT => 3 }), prints the result via io/print_int.
  • Modify: crates/ail/tests/e2e.rs — new test test_23_1_ordering_match_via_prelude that builds and runs the fixture, asserts stdout = "1\n".

Task 1: Author the prelude module file

Files:

  • Create: examples/prelude.ail.json

  • Step 1: Write the prelude JSON

Create examples/prelude.ail.json with the following exact contents:

{
  "schema": "ailang/v0",
  "name": "prelude",
  "imports": [],
  "defs": [
    {
      "kind": "type",
      "name": "Ordering",
      "vars": [],
      "doc": "Result of a three-way comparison: LT (less than), EQ (equal), GT (greater than). Ships in milestone 23 as the codomain of Ord.compare.",
      "ctors": [
        { "name": "LT", "fields": [] },
        { "name": "EQ", "fields": [] },
        { "name": "GT", "fields": [] }
      ]
    }
  ]
}
  • Step 2: Verify the JSON parses as a Module

Run: cargo test --workspace -p ailang-core -- workspace::tests::loads_example_workspace_happy_path Expected: PASS (this test loads examples/ws_main.ail.json; if it breaks, the new prelude file is malformed in a way that affects JSON shape detection).

Then a manual sanity check that the file is valid JSON:

Run: python3 -c "import json; json.load(open('examples/prelude.ail.json'))" Expected: no output, exit 0.

  • Step 3: Commit
git add examples/prelude.ail.json
git commit -m "iter 23.1.1: examples/prelude.ail.json — Ordering ADT skeleton"

Task 2: load_workspace auto-injects the prelude module

Files:

  • Modify: crates/ailang-core/src/workspace.rs

  • Step 1: Write the failing test

In the mod tests block of crates/ailang-core/src/workspace.rs (near loads_example_workspace_happy_path), add:

#[test]
fn loads_workspace_auto_injects_prelude() {
    // Iter 23.1: the prelude module is implicitly part of every
    // workspace, regardless of whether the user's modules import
    // it. Loading any well-formed workspace must result in
    // `ws.modules["prelude"]` being present with the Ordering
    // type def.
    let workspace_root =
        std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
    let entry = workspace_root.join("examples").join("ws_main.ail.json");

    let ws = load_workspace(&entry).expect("load workspace");
    assert!(
        ws.modules.contains_key("prelude"),
        "prelude module must be auto-injected; modules present: {:?}",
        ws.modules.keys().collect::<Vec<_>>()
    );
    let prelude = &ws.modules["prelude"];
    assert_eq!(prelude.name, "prelude");
    assert!(
        prelude.defs.iter().any(|d| matches!(
            d,
            crate::ast::Def::Type(t) if t.name == "Ordering"
        )),
        "prelude must contain Ordering type def"
    );
}
  • Step 2: Run test to verify it fails

Run: cargo test --workspace -p ailang-core -- workspace::tests::loads_workspace_auto_injects_prelude Expected: FAIL with prelude module must be auto-injected; modules present: ["ws_main", "ws_lib"] (or similar — prelude key missing).

  • Step 3: Add the prelude-loading helper

Inside crates/ailang-core/src/workspace.rs, immediately above pub fn load_workspace (which starts around line 305), add:

/// Iter 23.1: the prelude module is embedded into the binary at
/// compile time so the loader needs no on-disk resolution. The
/// canonical source-of-truth file remains
/// `examples/prelude.ail.json` (the file the LLM-author edits).
const PRELUDE_JSON: &str = include_str!(
    "../../../examples/prelude.ail.json"
);

/// Iter 23.1: parse the embedded prelude source into a `Module`.
/// Panics on parse failure — the prelude is build-time-validated;
/// a failure here means the embedded file is malformed and is a
/// build-correctness bug, not a runtime concern.
fn load_prelude() -> Module {
    serde_json::from_str(PRELUDE_JSON)
        .expect("examples/prelude.ail.json must parse as a Module")
}
  • Step 4: Inject the prelude in load_workspace

In pub fn load_workspace (currently around line 305-353), locate the lines AFTER the visit(entry_module, ...) call and BEFORE validate_classdefs(&modules)?. Insert the injection so the registry-build pass sees the prelude:

    visit(
        entry_module,
        &root_dir,
        &mut modules,
        &mut visiting,
        &mut visiting_set,
    )?;

    // Iter 23.1: inject the prelude module. It is implicitly
    // part of every workspace, so the user's import graph
    // does not name it. A user module that happens to be named
    // "prelude" would collide here; reject explicitly so the
    // failure is informative rather than silent.
    if modules.contains_key("prelude") {
        return Err(WorkspaceLoadError::ReservedModuleName {
            name: "prelude".to_string(),
        });
    }
    let prelude = load_prelude();
    modules.insert("prelude".to_string(), prelude);

    // Iter 22b.2: class-schema validation runs before registry
    ...
  • Step 5: Add the new error variant

The WorkspaceLoadError enum uses thiserror::Error derive (see line 104: #[derive(Debug, thiserror::Error)]), so the Display message is on the variant itself via #[error(...)]. No separate impl Display block exists.

In crates/ailang-core/src/workspace.rs, inside the pub enum WorkspaceLoadError { ... } block (currently lines 105 onwards), add this new variant at the end of the enum (after the existing MethodOnNonClass or similar terminal variant — locate the last }, before the closing } of the enum and insert before it):

    /// Iter 23.1: a user module attempted to use the reserved
    /// module name `prelude`. The prelude is auto-injected by
    /// the loader, so a user module of the same name would
    /// collide silently. Reject explicitly.
    #[error("module name {name:?} is reserved (auto-injected by the loader)")]
    ReservedModuleName { name: String },

No impl Display edit is required — the #[error] attribute generates it automatically via the thiserror derive.

  • Step 6: Run test to verify it passes

Run: cargo test --workspace -p ailang-core -- workspace::tests::loads_workspace_auto_injects_prelude Expected: PASS.

Also run the existing happy-path test to verify nothing regresses:

Run: cargo test --workspace -p ailang-core -- workspace::tests::loads_example_workspace_happy_path Expected: PASS.

  • Step 7: Run the full ailang-core test suite

Run: cargo test --workspace -p ailang-core Expected: all green. Any regression here is a sign that an existing test built a workspace by hand and didn't expect a prelude entry — fix that test by either updating its assertion (if the count of modules is asserted directly) or filtering on module names.

  • Step 8: Commit
git add crates/ailang-core/src/workspace.rs
git commit -m "iter 23.1.2: load_workspace auto-injects prelude module"

Task 3: check_in_workspace implicitly imports the prelude

Files:

  • Modify: crates/ailang-check/src/lib.rs

  • Step 1: Write the failing test

In the mod tests block of crates/ailang-check/src/lib.rs (near the existing cross_module_* tests around line 3386-3700), add this test. It uses the same struct shapes as the existing cross_module_ws helper at line 3390 and the fn_def helper at line 2628.

/// Iter 23.1: the prelude module is implicitly imported into
/// every user module. A user module that pattern-matches on
/// `LT` / `EQ` / `GT` without an explicit
/// `import { module: "prelude" }` must typecheck cleanly.
#[test]
fn user_module_can_pattern_match_on_prelude_ordering_bare() {
    let prelude = Module {
        schema: SCHEMA.into(),
        name: "prelude".into(),
        imports: vec![],
        defs: vec![Def::Type(TypeDef {
            name: "Ordering".into(),
            vars: vec![],
            ctors: vec![
                Ctor { name: "LT".into(), fields: vec![] },
                Ctor { name: "EQ".into(), fields: vec![] },
                Ctor { name: "GT".into(), fields: vec![] },
            ],
            doc: None,
            drop_iterative: false,
        })],
    };
    let consumer = Module {
        schema: SCHEMA.into(),
        name: "user".into(),
        imports: vec![],
        defs: vec![fn_def(
            "from_lt",
            Type::Fn {
                params: vec![],
                ret: Box::new(Type::int()),
                effects: vec![],
                param_modes: vec![],
                ret_mode: ParamMode::Implicit,
            },
            vec![],
            Term::Match {
                scrutinee: Box::new(Term::Ctor {
                    type_name: "Ordering".into(),
                    ctor: "LT".into(),
                    args: vec![],
                }),
                arms: vec![
                    Arm {
                        pat: Pattern::Ctor {
                            ctor: "LT".into(),
                            fields: vec![],
                        },
                        body: Term::Lit { lit: Literal::Int { value: 1 } },
                    },
                    Arm {
                        pat: Pattern::Ctor {
                            ctor: "EQ".into(),
                            fields: vec![],
                        },
                        body: Term::Lit { lit: Literal::Int { value: 2 } },
                    },
                    Arm {
                        pat: Pattern::Ctor {
                            ctor: "GT".into(),
                            fields: vec![],
                        },
                        body: Term::Lit { lit: Literal::Int { value: 3 } },
                    },
                ],
            },
        )],
    };
    let mut modules = BTreeMap::new();
    modules.insert("prelude".into(), prelude);
    modules.insert("user".into(), consumer);
    let ws = Workspace {
        entry: "user".into(),
        modules,
        root_dir: std::path::PathBuf::from("."),
        registry: ailang_core::workspace::Registry::default(),
    };
    let diags = check_workspace(&ws);
    assert!(
        diags.is_empty(),
        "user module must typecheck bare-LT without explicit prelude import; got: {:?}",
        diags
    );
}

The use super::*; and use ailang_core::SCHEMA; lines at the top of the mod tests block (lines 2625-2626) bring Module, Def, TypeDef, Ctor, Type, ParamMode, Term, Arm, Pattern, Literal, Workspace, BTreeMap, check_workspace, and SCHEMA into scope. No new use declarations needed.

  • Step 2: Run test to verify it fails

Run: cargo test --workspace -p ailang-check -- user_module_can_pattern_match_on_prelude_ordering_bare Expected: FAIL — the test asserts diags.is_empty(), but the bare LT in the user module fires UnknownCtorInPattern("LT") (per lib.rs:2419) because env.imports for the user module is empty and the bare-ctor fallback finds no candidate.

  • Step 3: Inject implicit prelude import in check_in_workspace

Locate fn check_in_workspace (around line 1216 in crates/ailang-check/src/lib.rs). It calls build_check_env which populates env.module_imports from each module's declared imports (around lines 1176-1183 inside build_check_env).

The cleanest place to add the implicit prelude import is inside build_check_env, in the loop that builds env.module_imports, right after the per-module import map is constructed and before it is inserted. Modify the loop:

    // env.module_imports — per-module alias-or-name -> module-name.
    // Per-module because aliases collide across modules; sibling
    // shape to `module_globals`.
    for m in ws.modules.values() {
        let mut import_map: BTreeMap<String, String> = BTreeMap::new();
        for imp in &m.imports {
            let key = imp.alias.clone().unwrap_or_else(|| imp.module.clone());
            import_map.insert(key, imp.module.clone());
        }
        // Iter 23.1: the prelude module is implicitly imported
        // into every non-prelude module. A user-declared explicit
        // `import { module: "prelude" }` would already place
        // "prelude" in the map above (idempotent insert). The
        // prelude module itself does not import itself.
        if m.name != "prelude" {
            import_map.entry("prelude".to_string())
                .or_insert_with(|| "prelude".to_string());
        }
        env.module_imports.insert(m.name.clone(), import_map);
    }

NOTE: this overlay lives in build_check_env, which builds the workspace-wide env. The per-module overlay in check_in_workspace (lines 1224+) clears env.types/env.ctor_index and rebuilds them with only the local module's entries, but env.module_imports (used by the ctor-fallback at line 2405) is set workspace-wide and not cleared per module, so the implicit prelude import is in scope for every module's body-check.

Also need to plumb the local module's import map into env.imports during body-check. Look for where the per-module body-check reads env.module_imports[<module>] into env.imports. (If the existing code already drives env.imports from env.module_imports[current_module], then no further wiring is needed; this step is a code-reading verification, not a code change.)

Read crates/ailang-check/src/lib.rs around line 1260-1320 (the end of check_in_workspace) and confirm that env.imports is populated from env.module_imports[m.name] before each body-check call. If yes, no further changes. If no, plumb it through (extend the local overlay).

  • Step 4: Run test to verify it passes

Run: cargo test --workspace -p ailang-check -- user_module_can_pattern_match_on_prelude_ordering_bare Expected: PASS.

  • Step 5: Run the full ailang-check test suite

Run: cargo test --workspace -p ailang-check Expected: all green. Watch in particular for regressions in the cross_module_pat_ctor_ambiguous_errors test (line 3536) — if any of those workspaces happened to contain a ctor named LT / EQ / GT, the implicit prelude import could now create a new ambiguity. Read the test fixtures; if a clash exists, rename the clashing ctor in the test fixture (not in the prelude).

  • Step 6: Commit
git add crates/ailang-check/src/lib.rs
git commit -m "iter 23.1.3: check_in_workspace implicitly imports prelude into every user module"

Task 4: E2E fixture + ail-crate test

Files:

  • Create: examples/ordering_match.ail.json

  • Modify: crates/ail/tests/e2e.rs

  • Step 1: Author the fixture

Create examples/ordering_match.ail.json with the following exact contents:

{
  "schema": "ailang/v0",
  "name": "ordering_match",
  "imports": [],
  "defs": [
    {
      "kind": "fn",
      "name": "main",
      "type": {
        "k": "fn",
        "params": [],
        "ret": { "k": "con", "name": "Unit" },
        "effects": ["IO"]
      },
      "params": [],
      "doc": "Iter 23.1 fixture: pattern-match on prelude Ordering ctor without explicit prelude import.",
      "body": {
        "t": "do",
        "op": "io/print_int",
        "args": [
          {
            "t": "match",
            "scrutinee": {
              "t": "ctor",
              "type": "Ordering",
              "ctor": "LT",
              "args": []
            },
            "arms": [
              {
                "pat": { "p": "ctor", "ctor": "LT", "fields": [] },
                "body": { "t": "lit", "lit": { "kind": "int", "value": 1 } }
              },
              {
                "pat": { "p": "ctor", "ctor": "EQ", "fields": [] },
                "body": { "t": "lit", "lit": { "kind": "int", "value": 2 } }
              },
              {
                "pat": { "p": "ctor", "ctor": "GT", "fields": [] },
                "body": { "t": "lit", "lit": { "kind": "int", "value": 3 } }
              }
            ]
          }
        ]
      }
    }
  ]
}
  • Step 2: Write the failing E2E test

In crates/ail/tests/e2e.rs, add a new test function. Locate an existing E2E test (e.g. check_workspace_resolves_import at line 863) to match the helper-usage style, and add this test function nearby:

#[test]
fn ordering_match_via_prelude_prints_1() {
    // Iter 23.1: examples/ordering_match.ail.json pattern-matches
    // on the prelude Ordering ctor `LT` without an explicit
    // prelude import. End-to-end: typecheck → codegen → link →
    // run → stdout "1\n".
    let stdout = run_example("examples/ordering_match.ail.json")
        .expect("ordering_match should build and run");
    assert_eq!(stdout, "1\n");
}

NOTE: the run_example helper name above must match the one already in use in e2e.rs (read the file's top to confirm — some test files name it differently, e.g. build_and_run, exec_fixture, or run_fixture). Use whatever name the existing E2E tests in this file already use to compile and run a .ail.json fixture and capture stdout.

  • Step 3: Run test to verify it fails (and isolate why)

Run: cargo test --workspace -p ail -- ordering_match_via_prelude_prints_1 Expected: FAIL. The failure should be either: (a) the test infrastructure is fine but the binary outputs something wrong (real failure mode worth investigating); (b) the test infrastructure compiles successfully and stdout is "1\n" — meaning Tasks 1-3 already chain through correctly and the test is GREEN on first run. That is the expected end state.

If (b) — test passes on first run — go directly to Step 5.

If (a) — investigate. Most likely causes:

  • The fixture failed to typecheck → indicates Task 3 didn't fully wire env.imports to include "prelude" at body-check time. Re-read Task 3 Step 3's NOTE about plumbing env.module_imports[current_module] into env.imports.

  • The fixture typechecks but codegen fails on the cross-module ctor reference → indicates codegen needs the cross-module resolution too. Symptom: the IR emit step reports UnknownCtor("LT") or similar.

  • Step 4: Apply minimal fix (only if Step 3 fell into case (a))

If codegen reports UnknownCtor("LT"), the codegen lookup path for ctors does not honour the implicit prelude import. Locate the codegen ctor-resolution (likely in crates/ailang-codegen/src/lib.rs, grep for UnknownCtor or ctor_index to find the relevant arm). The fix mirrors Task 3: add "prelude" to whatever import-set codegen consults for the current module's ctor resolution.

If the failure is something else, treat as a NEEDS_CONTEXT escalation back to the orchestrator.

  • Step 5: Run test to verify it passes

Run: cargo test --workspace -p ail -- ordering_match_via_prelude_prints_1 Expected: PASS, stdout captured as "1\n".

  • Step 6: Run the full ail-crate test suite

Run: cargo test --workspace -p ail Expected: all green.

  • Step 7: Run the full workspace test suite

Run: cargo test --workspace Expected: all green.

  • Step 8: Commit
git add examples/ordering_match.ail.json crates/ail/tests/e2e.rs
git commit -m "iter 23.1.4: E2E fixture — bare LT match via implicit prelude import"

Task 5: JOURNAL entry for iter 23.1

Files:

  • Modify: docs/JOURNAL.md

  • Step 1: Get commit hashes

Run: git log --oneline -10 Expected: top 4 commits are iter 23.1.1, 23.1.2, 23.1.3, 23.1.4 (in that order, most recent first).

  • Step 2: Append JOURNAL entry

Append the following block at the end of docs/JOURNAL.md, replacing <hash N> with the short SHA from Step 1:

## 2026-05-10 — Iteration 23.1: Ordering ADT + prelude skeleton + auto-load

First iteration of the milestone-23 Eq/Ord Prelude. Lays the
groundwork — no classes or instances ship yet — by establishing
the prelude module as a real loaded module that every workspace
implicitly contains, with bare-name resolution for its ctors via
the existing import-fallback machinery (Iter 15a,
`ailang-check/src/lib.rs:2399`).

Three mechanisms compose:

1. **Embedded prelude.** `examples/prelude.ail.json` is the
   LLM-author-readable source; the loader embeds it via
   `include_str!` at compile time. No runtime file IO, no CWD
   dependency, no fixture-not-found failure mode.
2. **Loader injection.** `load_workspace` inserts the prelude
   into `ws.modules` after the user's import DFS finishes and
   before `validate_classdefs` / `build_registry`, so the
   workspace-wide registry passes see it like any other module.
   A user module trying to claim the reserved name `prelude`
   fails fast with `WorkspaceLoadError::ReservedModuleName`.
3. **Implicit import.** `build_check_env` adds `prelude → prelude`
   to every non-prelude module's `module_imports` map. The
   existing bare-ctor fallback in `Pattern::Ctor` resolution
   (Iter 15a) finds prelude ctors through this path, so the user
   writes `LT` instead of `prelude.LT`.

Verified end-to-end through `examples/ordering_match.ail.json`
(pattern-matches a hard-coded `LT` value, prints `1`). The
ail-crate E2E test plus the per-crate unit tests catch any
regression on the three mechanisms independently.

Out of scope for this iter (covered by later 23.x):
- Eq / Ord class definitions (23.2 / 23.3).
- Free top-level utility functions (23.4).
- Float-NoInstance diagnostic + DESIGN.md amendment (23.5).

Per-task commits:

- `<hash 1>` iter 23.1.1: examples/prelude.ail.json — Ordering ADT skeleton
- `<hash 2>` iter 23.1.2: load_workspace auto-injects prelude module
- `<hash 3>` iter 23.1.3: check_in_workspace implicitly imports prelude into every user module
- `<hash 4>` iter 23.1.4: E2E fixture — bare LT match via implicit prelude import
  • Step 3: Commit
git add docs/JOURNAL.md
git commit -m "iter 23.1.5: JOURNAL entry"

Self-review checklist

  1. Spec coverage: Component row 23.1 in the parent spec names four concrete artefacts — Ordering ADT, prelude module skeleton, check_workspace auto-load wire, codegen+match-lowering exercised by unit test. Task 1 ships the ADT and skeleton. Task 2 ships the auto-load. Task 3 ships the implicit import (the part that makes auto-load useful). Task 4 exercises the end-to-end pipeline through the unit + E2E fixture. Task 5 records the JOURNAL entry. ✓
  2. Placeholder scan: <hash 1> / <hash 2> / <hash 3> / <hash 4> are commit-hash placeholders, resolved by the implementer at Task 5 Step 1 by reading actual git log output. No "TBD", no "TODO", no "implement later", no "similar to Task N". Task 4 Step 4 says "apply minimal fix only if Step 3 fell into case (a)" — that is a conditional path with explicit triggering criteria, not a placeholder. ✓
  3. Type / path consistency: examples/prelude.ail.json (Task 1) is the same path in Task 2 Step 3's include_str! path component. ws.modules["prelude"] is consistent in Task 2 Step 4 (insert) and Task 3 Step 1 (assert). Module name "prelude" appears consistently in Tasks 2, 3, and 4 (skip-self condition in Task 3 Step 3, fixture import-list in Task 4 — fixture has empty imports because the implicit import is the point). ✓
  4. Step granularity: every step is one file edit or one command, 2-5 minutes. Task 3 Step 3 is the longest (read + modify env-construction); split into NOTE-driven sub-actions if the implementer flags it as not bite-sized. ✓

Notes for the implementer

  • The run_example helper name in Task 4 Step 2 — the test file crates/ail/tests/e2e.rs uses a specific helper to compile and run a fixture. Read the file's existing tests to find the right name (most likely compile_and_run or run_example). Match the existing style; do NOT introduce a new helper.
  • Cross-module ctor resolution at codegen time (Task 4 Step 4) — if it surfaces, it is the same "lockstep across files" pattern the architect-iron-law extension just guarded against (is_static_calleelower_app, pre_desugar_validation ↔ desugar). For ctors: the codegen ctor-resolution path likely has its own import-list consultation, mirroring check_in_workspace's. If the fix in Task 3 fully resolves Task 4 without codegen changes (case (b) in Step 3), no further lockstep-update is needed — but report this in the status payload so the orchestrator can record it.
  • Existing loads_example_workspace_happy_path test — this test currently asserts a count of modules. After Task 2 it will see one extra module (the prelude). Either: (i) the test does NOT assert a count → unaffected; (ii) the test DOES assert a count → it will break, and Task 2 Step 7 is the place to fix it (update the count or filter the prelude key out). Read the test first, before assuming.