Files
AILang/docs/plans/0066-iter-pd.3.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

24 KiB

pd.3 — retire prelude.ail.json + carve-out cleanup — Implementation Plan

Parent spec: docs/specs/0028-prelude-decouple.md

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

Goal: Close the prelude-decouple milestone by deleting examples/prelude.ail.json from the working tree and sweeping the remaining include_str! consumers + spec text + carve-out inventory that depended on it. After this iter the only on-disk source of the prelude is examples/prelude.ail, the form-a-default-authoring spec's §C4 (b) carve-out category is empty, and a new pin test guarantees the JSON file stays gone.

Architecture: Pure deletion + adapt iter. Two include_str! of prelude.ail.json survive pd.2: (a) the migrate-bare-cross-module-refs defensive include in crates/ail/src/main.rs:472-484 (a documented no-op — the inserted entry is unconditionally skipped by the loop's if mod_name == "prelude" && path.as_os_str().is_empty() { continue; } guard at lines 500-505); (b) the cross-form-identity preflight test in crates/ailang-surface/tests/prelude_module_hash_pin.rs (which the spec explicitly scoped as pd.2-only — its purpose was to ratify pd.3's load-bearing assumption, which it did at hash 3abe0d3fa3c11c99). Both must go before the JSON file. Plus one in-mod test in crates/ailang-core/src/workspace.rs (~line 2654 area, pd.2-introduced) re-points from inline serde_json::from_str(include_str!("prelude.ail.json")) to ailang_surface::parse_prelude() via the existing core→surface dev-dep edge.

Tech Stack: ail CLI (one block delete + lockstep skip-branch delete), ailang-surface/tests/ (one test fn + supporting bytes delete + new carve-out pin), ailang-core (one in-mod test re-point), ailang-core/tests/carve_out_inventory.rs (list + comment update), docs/specs/0025-form-a-default-authoring.md (§C4 (b) status note), examples/prelude.ail.json (deleted).


Files this plan creates or modifies:

  • Create: crates/ailang-surface/tests/prelude_decouple_carve_out_pin.rs — asserts examples/prelude.ail.json does NOT exist on disk.
  • Modify: crates/ailang-surface/tests/prelude_module_hash_pin.rs — delete the prelude_ail_and_json_parse_to_identical_module fn and its supporting PRELUDE_JSON const + PRELUDE_AIL orphan-import
    • the const _: &str = PRELUDE_AIL; shape-pin. Trim the module doc-comment to describe only the surviving long-term hash anchor.
  • Modify: crates/ailang-core/src/workspace.rs — re-point the in-mod test that uses include_str!("../../../examples/prelude.ail.json") (around line 2654 area; pd.2 introduced this as a replacement for the deleted load_prelude() call) to ailang_surface::parse_prelude() via the core→surface dev-dep edge.
  • Modify: crates/ail/src/main.rs:472-505 — delete the defensive include_str! block (472-484) AND its lockstep skip-branch (500-505, the comment + the if mod_name == "prelude" && path.as_os_str().is_empty() { continue; } guard). The guard is dead-code without the synthetic insert.
  • Modify: crates/ailang-core/tests/carve_out_inventory.rs — drop the "prelude.ail.json" entry from the EXPECTED list (line 25); rewrite the doc-comment block (lines 6-11) to "Seven carve-outs" and "§C4 (a) subject-matter: 7 fixtures (canonical-form rejection / unbound / class-def rejection); §C4 (b) compile-time-embed: retired by prelude-decouple milestone, 2026-05-14"; update the header sentence at line 2 from "eight" to "seven".
  • Modify: docs/specs/0025-form-a-default-authoring.md:270-311 — add a "Status: RETIRED 2026-05-14 by prelude-decouple milestone" marker at the top of §C4 (b) and a one-paragraph epitaph at the bottom; preserve the original §C4 (b) text below as historical context. Upstream callouts (lines 232-236, 462-465, 477-484) stay as historical references — their "as of 2026-05-13" temporal qualifiers preserve correctness.
  • Delete: examples/prelude.ail.json (the milestone's primary artefact retirement).

Task 1: Re-point the in-mod test in workspace.rs from include_str!("prelude.ail.json") to ailang_surface::parse_prelude()

Files:

  • Modify: crates/ailang-core/src/workspace.rs — the in-mod test that inlined serde_json::from_str(include_str!("prelude.ail.json")) as a pd.2 replacement for the deleted load_prelude() call. Per recon, the reference is at lines 2654 / 2656 (inside #[cfg(test)] mod tests).

The dev-dep edge ailang-core → ailang-surface (declared in crates/ailang-core/Cargo.toml's [dev-dependencies]) is reachable from in-mod tests when surface is consumed via fully-qualified path in the test body, but the lib-test-target compilation cycle blocks top-level use ailang_surface::*; lines. Use the fully-qualified ailang_surface::parse_prelude() at the call site to avoid the import issue (per pd.2's experience).

  • Step 1: Read the existing in-mod test to confirm shape

Open crates/ailang-core/src/workspace.rs, navigate to lines 2640-2670. Identify the test fn that contains the include_str!("../../../examples/prelude.ail.json") and serde_json::from_str pair. Per the pd.2 journal this is build_workspace_accepts_assembled_modules_and_runs_validation (and possibly one sibling using the same pattern; check for both occurrences at lines 2654 and 2656).

If the line numbers shifted post-pd.2 (very possible given pd.2's deletions), use:

git grep -n 'include_str!.*prelude\.ail\.json' crates/ailang-core/src/workspace.rs
  • Step 2: Verify ailang_surface is reachable from in-mod tests

Read crates/ailang-core/Cargo.toml [dev-dependencies]. Confirm ailang-surface = { path = "../ailang-surface" } is present (per brainstorm-recon: yes).

This is sanity-checking; if absent, STOP — pd.2 was wrong about the edge existing and the migration target needs revisiting.

  • Step 3: Replace the inlined serde_json::from_str with ailang_surface::parse_prelude()

In each affected test fn body, replace the pattern:

Old (per pd.2 journal):

let prelude: Module = serde_json::from_str(include_str!(
    "../../../examples/prelude.ail.json"
)).expect("embedded prelude must parse");

New:

let prelude = ailang_surface::parse_prelude();

The Module type annotation can drop because parse_prelude returns Module directly. The import line for Module (if newly orphan) either stays or is removed depending on other test bodies in the same mod.

  • Step 4: Run the affected tests to verify they still pass

Run: cargo test -p ailang-core build_workspace_accepts_assembled_modules_and_runs_validation

(And the second test name if recon surfaces one — Step 1 will have named it.)

Expected: PASS. The semantic content is unchanged (same prelude Module, just constructed via the canonical post-pd.2 path).

  • Step 5: Run the full core test suite

Run: cargo test -p ailang-core Expected: all green.


Task 2: Delete the cross-form-identity preflight + supporting bytes

Files:

  • Modify: crates/ailang-surface/tests/prelude_module_hash_pin.rs — delete the prelude_ail_and_json_parse_to_identical_module test fn (lines 22-43 per recon), the PRELUDE_JSON const (line 20), the PRELUDE_AIL orphan import (line 18 — keep parse_prelude only), the const _: &str = PRELUDE_AIL; shape-pin (line 60), and trim the module doc-comment (lines 1-15) to describe only the surviving long-term hash anchor.

  • Step 1: Delete the cross-form-identity test fn + its supporting bytes

Edit crates/ailang-surface/tests/prelude_module_hash_pin.rs. Delete:

  • Lines 1-15: the module doc-comment block (will be replaced in Step 2).
  • Line 18: the import use ailang_surface::{parse_prelude, PRELUDE_AIL}; — replace with use ailang_surface::parse_prelude; (PRELUDE_AIL no longer consumed).
  • Line 20: const PRELUDE_JSON: &str = include_str!(...);
  • Lines 22-43: the entire #[test] fn prelude_ail_and_json_parse_to_identical_module body.
  • Lines 59-60: the shape-pin comment + const _: &str = PRELUDE_AIL;.

The surviving content is the prelude_parse_yields_canonical_hash test fn (post-edit lines will be ~3-15).

  • Step 2: Replace the module doc-comment with the surviving-only version

At the top of the file, insert:

//! pd.2/pd.3 (`prelude-decouple` milestone): pin the canonical
//! identity of the parsed prelude as a literal hex hash. Updates
//! intentionally on prelude-content changes (record the why in the
//! per-iter journal); accidental drift trips immediately.
//!
//! pd.2 also carried a `prelude_ail_and_json_parse_to_identical_module`
//! cross-form-identity preflight that compared `parse(prelude.ail)`
//! against `serde_json::from_str(prelude.ail.json)`. The preflight
//! discharged its purpose at pd.2 close (PASSED at hash
//! `3abe0d3fa3c11c99`) and was retired in pd.3 alongside the
//! `prelude.ail.json` file.
  • Step 3: Run the surviving test

Run: cargo test -p ailang-surface --test prelude_module_hash_pin Expected: prelude_parse_yields_canonical_hash PASSES (still pinning hash 3abe0d3fa3c11c99); the deleted prelude_ail_and_json_parse_to_identical_module no longer appears in the suite.

If the hash assertion fails, the prelude content has changed since pd.2 close — investigate. With no source-edit between pd.2 commit and pd.3 task 2, the hash MUST be unchanged.

  • Step 4: Verify orphan imports are clean

Run: cargo build -p ailang-surface --tests 2>&1 | grep -E '(warning|error)' | head -10 Expected: no unused import warnings on the edited file. If Module or any other type was the only consumer of an import line that now has no consumer, drop the line.


Task 3: Delete the defensive include in crates/ail/src/main.rs + its lockstep skip-branch

Files:

  • Modify: crates/ail/src/main.rs — two adjacent code blocks inside the Cmd::MigrateBareCrossModuleRefs arm: the defensive include + insert at lines 472-484, and the skip-branch at lines 500-505.

  • Step 1: Delete the defensive include block

Edit crates/ail/src/main.rs. Delete lines 472-484 (verify with git grep -n 'Include the embedded prelude' crates/ail/src/main.rs):

            // Include the embedded prelude (so the implicit fallback
            // matches the loader's behaviour).
            let prelude_json: &str = include_str!(
                "../../../examples/prelude.ail.json"
            );
            let prelude: Module = serde_json::from_str(prelude_json)
                .expect("embedded prelude must parse");
            if !modules.contains_key("prelude") {
                modules.insert(
                    "prelude".to_string(),
                    (PathBuf::new(), prelude),
                );
            }

Block-locals prelude_json and prelude are never read after their binding scope ends; deletion is safe.

  • Step 2: Delete the lockstep skip-branch

Same file, locate the loop body. Delete the comment + guard at lines 500-505:

            // Pass 2: rewrite each module's bare cross-module refs.
            // Skip the synthetic prelude entry (no path to write to).
            let mut rewritten = 0usize;
            for (mod_name, (path, m)) in modules.iter_mut() {
                if mod_name == "prelude" && path.as_os_str().is_empty() {
                    continue;
                }

becomes:

            // Pass 2: rewrite each module's bare cross-module refs.
            let mut rewritten = 0usize;
            for (mod_name, (path, m)) in modules.iter_mut() {

The mod_name binding is no longer read inside the loop after the guard's removal (verify by reading the next ~15 lines). If the unused binding generates a warning, change to for (_, (path, m)) in modules.iter_mut() {.

  • Step 3: Verify the subcommand still compiles

Run: cargo check -p ail 2>&1 | head -20 Expected: clean. No new warnings on main.rs.

  • Step 4: Spot-test the migrate subcommand (optional sanity check)

Run: cargo run -p ail -- migrate-bare-cross-module-refs --help 2>&1 | head -10 Expected: prints help text successfully (the binary builds and the subcommand enumerates).

The subcommand's substantive behaviour (rewriting bare cross-module refs across a directory of .ail.json files) hasn't been exercised since the canonical-type-names milestone; no in-tree fixtures are known to need rewriting today, so a deeper test is out of scope.


Task 4: Update carve_out_inventory.rs — drop §C4 (b)

Files:

  • Modify: crates/ailang-core/tests/carve_out_inventory.rs — three edits: header sentence count (line 2), comment block (lines 6-11), EXPECTED list (line 24-25 area).

  • Step 1: Update the header sentence

Edit crates/ailang-core/tests/carve_out_inventory.rs. Change line 2 from:

//! spec. Asserts that exactly the eight named carve-out files exist

to:

//! spec. Asserts that exactly the seven named carve-out files exist
  • Step 2: Rewrite the carve-out comment block (lines 6-11)

Old:

//! Eight carve-outs at iter form-a.1 close:
//! - §C4 (a) subject-matter: 7 fixtures (canonical-form rejection / unbound / class-def rejection).
//! - §C4 (b) compile-time-embed: 1 fixture (prelude.ail.json).
//!
//! When the prelude-embed-refactor milestone retires §C4 (b), this
//! test's pass-count drops to seven and `EXPECTED` is re-published.

New:

//! Seven carve-outs post prelude-decouple (2026-05-14):
//! - §C4 (a) subject-matter: 7 fixtures (canonical-form rejection / unbound / class-def rejection).
//! - §C4 (b) compile-time-embed: retired 2026-05-14 by milestone
//!   prelude-decouple; the prelude is now embedded as `prelude.ail`
//!   in `ailang-surface` and parsed at compile time via
//!   `ailang_surface::parse_prelude`.
  • Step 3: Drop the "prelude.ail.json" entry from EXPECTED

Edit lines around 24-25. Delete:

    // §C4 (b) — compile-time-embed
    "prelude.ail.json",

If the comment "// §C4 (b) — compile-time-embed" is on its own line above the entry, delete the comment too (it has no other content beneath it after this edit).

  • Step 4: Run the test — should now PASS against the post-pd.3 corpus

Run: cargo test -p ailang-core --test carve_out_inventory Expected: PASS — but note this test STILL READS examples/prelude.ail.json because the file hasn't been deleted yet (Task 7). The EXPECTED list no longer contains it, so the assertion matches_carve_outs will FAIL saying "unexpected file: prelude.ail.json". That's expected at this task — it goes green only after Task 7 deletes the file.

If you want intermediate green between this task and Task 7, swap the task order (Task 7 before Task 4). The plan's order keeps the test edits adjacent and defers the file-system mutation to last; either order is correct.


Task 5: Update docs/specs/0025-form-a-default-authoring.md §C4 (b)

Files:

  • Modify: docs/specs/0025-form-a-default-authoring.md:270-311 — add a "Status: RETIRED" marker at top of §C4 (b) and a one-paragraph epitaph at the bottom; preserve the original text as historical context.

The spec is a historical document about the closed form-a-default-authoring milestone; the convention is to retire sections in place rather than delete them, so future readers tracing back the prelude-embed-refactor milestone's prerequisites can find the original framing.

Per the recon, upstream callouts at lines 232-236, 462-465, 477-484 stay as historical references — their "as of 2026-05-13" temporal qualifiers preserve correctness.

  • Step 1: Insert a status marker at the top of §C4 (b)

Edit docs/specs/0025-form-a-default-authoring.md. Find the **(b) Compile-time-embed carve-outs (one file as of 2026-05-13):** heading (line 270 area). Insert immediately above:

> **Status: RETIRED 2026-05-14 by milestone prelude-decouple.** The
> §C4 (b) compile-time-embed carve-out is empty post that milestone;
> the prelude is now authored exclusively as `examples/prelude.ail`
> and parsed at compile time via `ailang_surface::parse_prelude`. The
> original §C4 (b) framing below is preserved as historical context;
> it documents the doctrine inconsistency that prelude-decouple
> resolved. See `docs/specs/0028-prelude-decouple.md` and the
> per-iter journals 2026-05-14-iter-pd.{1,2,3}.md for the resolution.
  • Step 2: Verify the spec still reads coherently

Read the §C4 (b) section top-to-bottom (lines ~268-315 post-insert). Confirm the new status marker reads as a forward-pointer to the resolution and the original text reads as the historical motivation. The intent: a future reader seeing "the prelude is the only file authored as JSON" understands that statement as past-tense and finds the resolution one click away.

No other edits in this task. Upstream callouts (lines 232-236, 462-465, 477-484) stay verbatim because their qualifiers ("as of 2026-05-13", "one file as of 2026-05-13") remain truthful as historical statements.


Task 6: Create the carve-out retirement pin

Files:

  • Create: crates/ailang-surface/tests/prelude_decouple_carve_out_pin.rs — single #[test] fn asserting examples/prelude.ail.json does NOT exist on disk.

  • Step 1: Write the test

Create crates/ailang-surface/tests/prelude_decouple_carve_out_pin.rs:

//! pd.3 (`prelude-decouple` milestone): pin that
//! `examples/prelude.ail.json` does NOT exist on disk. Surfaces
//! immediately if the file is recreated by accident or a future
//! refactor reverts the embed-source swap.
//!
//! Companion to the `examples_ail_json_inventory_matches_carve_outs`
//! test in `crates/ailang-core/tests/carve_out_inventory.rs`: that
//! one asserts the carve-out list is exactly seven; this one
//! asserts the eighth (retired) file specifically does not exist.
//! Either test detects an accidental re-introduction; this one
//! pinpoints the file by name in the error message.

use std::path::Path;

#[test]
fn prelude_ail_json_does_not_exist_on_disk() {
    let path = Path::new("../../examples/prelude.ail.json");
    assert!(
        !path.exists(),
        "examples/prelude.ail.json must NOT exist after the prelude-\
         decouple milestone (closed 2026-05-14). The prelude is now \
         embedded as prelude.ail in ailang-surface. If you intentionally \
         re-introduced this file, you must also: (a) remove this pin, \
         (b) restore the §C4 (b) entry in carve_out_inventory.rs, \
         (c) revert the §C4 (b) status marker in \
         docs/specs/0025-form-a-default-authoring.md, and \
         (d) record the rationale in a per-iter journal."
    );
}
  • Step 2: Run the test — must FAIL today

Run: cargo test -p ailang-surface --test prelude_decouple_carve_out_pin Expected: FAIL because examples/prelude.ail.json still exists. The test is RED-on-purpose at this point; Task 7 turns it GREEN by deleting the file.

If the test PASSES at this step, the file is already gone from the working tree — investigate (was it deleted in an earlier task by mistake? Recon should not have permitted that).


Task 7: Delete examples/prelude.ail.json

Files:

  • Delete: examples/prelude.ail.json — the milestone's primary artefact retirement.

  • Step 1: Verify zero remaining production / test consumers

Run:

git grep -n 'include_str!.*prelude\.ail\.json' -- '*.rs'

Expected: NO matches. (Tasks 1-3 cleared all three.)

git grep -n 'prelude\.ail\.json' -- '*.rs' | grep -vE '(//|//!|/\*)'

Expected: NO matches outside comments / doc-comments / string-literals in test fixtures. The non-include_str! references should all be either documentation, comments, or path-string constructions in tests (which fail gracefully on missing files).

If any reference appears as a file-system access (read/open/etc), STOP and migrate it before proceeding.

  • Step 2: Delete the file

Run:

rm examples/prelude.ail.json

(Plain shell delete; not git rm. The file will appear as a deletion in git status. The Boss's commit step stages the deletion via git add of the path or git add -u.)

  • Step 3: Run the carve-out retirement pin — must now PASS

Run: cargo test -p ailang-surface --test prelude_decouple_carve_out_pin Expected: PASS — prelude.ail.json no longer exists.

  • Step 4: Run the carve-out inventory test — must now PASS

Run: cargo test -p ailang-core --test carve_out_inventory Expected: PASS — EXPECTED is exactly seven names, and exactly seven files exist under examples/*.ail.json.

  • Step 5: Run the full workspace test suite

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


Task 8: Bench regressions + final acceptance

Files: none directly.

  • Step 1: Run regression baselines

Run: python bench/check.py && python bench/compile_check.py && python bench/cross_lang.py Expected: all three exit 0. pd.3 changes are file-system + spec text

  • test deletion + dead-code cleanup; no codegen, no runtime, no typecheck-result deltas.

If check.py fires the same bench_list_sum.bump_s envelope-noise as in pd.1 / pd.2 / earlier audits, treat as established noise (per the prior 13-audit pattern) — pd.3 cannot be the source.

  • Step 2: Confirm milestone-level acceptance

Per the parent spec (§Acceptance criteria):

  1. examples/prelude.ail.json is removed from the working tree — verified by Task 7 Step 3.
  2. cargo build --workspace succeeds with no include_str! of prelude.ail.json — verified by Task 7 Step 1.
  3. git grep -l '"prelude"' crates/ailang-core/src/ returns zero matches — already true post-pd.2; spot-check now to confirm pd.3 didn't regress.
  4. cargo test --workspace is green — verified Task 7 Step 5 + Task 8 Step 1.
  5. Baselines green at unchanged values — verified Task 8 Step 1.
  6. docs/specs/0025-form-a-default-authoring.md §C4 (b) is updated — verified by Task 5.
  7. CLAUDE.md and docs/DESIGN.md reviewed — spot-check now.

For item 7:

git grep -n 'prelude\.ail\.json' CLAUDE.md docs/DESIGN.md

Expected: NO production-doctrine assertions; only historical references acceptable. If a CLAUDE.md sentence still asserts that core embeds the prelude, edit it to reflect the post-milestone state.

git grep -n 'authors write `\.ail`' CLAUDE.md

Expected: this sentence is consistent with the post-milestone state (authors write .ail for ALL files now, including prelude — no exception). If the wording still names a prelude exception, edit it to drop the exception.

  • Step 3: Confirm zero literal-"prelude" strings in core/src/

Run: git grep '"prelude"' crates/ailang-core/src/ Expected: NO matches (already true post-pd.2; pd.3 is purely file-system + downstream test/spec adjustments, no core/src/ source edits).

If any match appears in production code, STOP — pd.3 should not have modified core's src/ source.


Out of scope for pd.3

The following are NOT part of this milestone and MUST NOT be touched:

  • The WorkspaceLoadError::ReservedModuleName variant declaration in crates/ailang-core/src/workspace.rs — stays per spec (generic shape, no literal "prelude").
  • The crates/ail/src/main.rs:1220-1229 W::ReservedModuleName match-arm in workspace_error_to_diagnostic — variant tag preserved, no edit.
  • The ailang-check and ailang-codegen implicit-prelude resolution paths (crates/ailang-check/src/lib.rs:143 area, codegen iter 23.2.4 behaviour) — these consume the prelude module from a populated workspace, do not embed prelude bytes, and were explicitly listed as out-of-scope in the parent spec.
  • The 3 cargo doc warnings on dangling [load_workspace] references (in Workspace and Registry rustdoc) noted as known debt in the pd.2 journal — pre-existing baseline; unrelated to pd.3 scope.
  • Audit (Boss-dispatched after pd.3 closes — separate skill invocation per skills/audit/SKILL.md).