diff --git a/docs/plans/2026-05-10-23.1-ordering-and-prelude-skeleton.md b/docs/plans/2026-05-10-23.1-ordering-and-prelude-skeleton.md new file mode 100644 index 0000000..a3c4514 --- /dev/null +++ b/docs/plans/2026-05-10-23.1-ordering-and-prelude-skeleton.md @@ -0,0 +1,742 @@ +# 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.rs` — `load_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.rs` — `check_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: + +```json +{ + "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** + +```bash +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: + +```rust +#[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::>() + ); + 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: + +```rust +/// 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: + +```rust + 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): + +```rust + /// 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** + +```bash +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. + +```rust +/// 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: + +```rust + // 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 = 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[]` 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** + +```bash +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: + +```json +{ + "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: + +```rust +#[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** + +```bash +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 `` with the short SHA from Step 1: + +```markdown +## 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: + +- `` iter 23.1.1: examples/prelude.ail.json — Ordering ADT skeleton +- `` iter 23.1.2: load_workspace auto-injects prelude module +- `` iter 23.1.3: check_in_workspace implicitly imports prelude into every user module +- `` iter 23.1.4: E2E fixture — bare LT match via implicit prelude import +``` + +- [ ] **Step 3: Commit** + +```bash +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:** `` / `` / `` / + `` 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_callee` ↔ `lower_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.