Files
AILang/docs/plans/0105-raw-buf.2-kernel-manifest.md
Brummel 647121cb8f plan: raw-buf.2-kernel-manifest — 7-task crate-rename + RawBuf submodule + checker-side surface (refs #7)
Decomposes raw-buf.2 (parent spec docs/specs/0054-raw-buf.md
§ Architecture point 2, § Components row 2, § Testing strategy
"raw-buf.2") into 7 atomic tasks:

  Task 1 — crate rename + reshape: crates/ailang-kernel-stub/ →
    crates/ailang-kernel/, restructured as a family-crate with
    src/kernel_stub/{mod.rs,source.ail}; 13 steps covering 8
    compile-checked rename sites plus design/INDEX.md + CLAUDE.md
    path updates.
  Task 2 — add raw_buf submodule: src/raw_buf/{mod.rs,source.ail}
    with the spec's Form-A source (4 fns + 1 TypeDef with
    param-in (a Int Float Bool)); RAW_BUF_AIL re-export.
  Task 3 — wire raw_buf into ailang-surface: parse_raw_buf fn,
    re-export at lib.rs:39, workspace injection alongside the
    existing kernel_stub block, workspace_pin count bump 4→5.
  Task 4 — round-trip test raw_buf_module_round_trips, verbatim
    structural clone of kernel_stub_module_round_trips.
  Task 5 — E2E raw_buf_check_e2e: (con RawBuf (con Int)) consumer
    checks clean.
  Task 6 — E2E raw_buf_param_in_reject_e2e: (con RawBuf (con Str))
    consumer rejected at check with param-not-in-restricted-set.
  Task 7 — E2E raw_buf_build_intercept_missing_e2e: (new RawBuf …)
    consumer accepted at check, rejected at build with the
    existing Term::New deferral message.

Three substantive plan-time decisions resolving plan-recon's
open questions:

1. The intercept-not-registered diagnostic is NOT introduced.
   The spec hedged ("intercept-not-registered: new__RawBuf__Int
   (or the existing Term::New-deferral diagnostic)") between
   shipping a fresh diagnostic and reusing the existing deferral
   at crates/ailang-codegen/src/lib.rs:2075-2078. Plan picks
   reuse: the deferral already names "milestone raw-buf" and is
   self-describing; a new diagnostic would exist for ~1 iter
   (raw-buf.2 → raw-buf.3) before going away — disposable infra.
   Task 7's test asserts on the substring "Term::New requires
   the type's" which is stable in the existing message.

2. In-tree references to crates/ailang-kernel-stub/ AFTER the
   rename — split-scope between raw-buf.2 and raw-buf.3.
   The path changes in raw-buf.2 (the crate moves now), so
   path-references in design/INDEX.md:112 and the CLAUDE.md
   Code-layout table get updated in Task 1 Steps 10-11. The
   retire-language (stub as "ratifying fixture for kernel-
   extension-mechanics" → "retired by raw-buf") stays for
   raw-buf.3 close, per spec § Components row 3.

3. parse_raw_buf gets a mirror re-export at ailang-surface/
   src/lib.rs:39 alongside parse_kernel_stub. Recon's natural
   reading; load-bearing because the new round-trip test calls
   ailang_surface::parse_raw_buf() directly.

Plan honours the project's compile-gate discipline. Task 1's
13 steps thread all 8 rename sites inside the same task —
no deferred caller across the build gate. Task 3 bundles the
workspace_pin count bump with the loader injection that drives
it; the assertion goes 4→5 in the same task that makes it true.

Test-count trajectory: 667 (post-raw-buf.1 baseline) → 667
(Tasks 1, 2, 3 — refactor + wiring, no new test) → 668 (Task 4
round-trip) → 669 (Task 5 check E2E) → 670 (Task 6 reject E2E)
→ 671 (Task 7 deferral E2E).

Handoff target: skills/implement on docs/plans/0105-raw-buf.2-kernel-manifest.md
2026-05-29 11:17:30 +02:00

36 KiB

raw-buf.2 — RawBuf kernel-tier manifest + checker side — Implementation Plan

Parent spec: docs/specs/0054-raw-buf.md

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

Goal: Rename crates/ailang-kernel-stub/crates/ailang-kernel/, reshape as a family-crate (one Cargo crate hosting all kernel-tier modules as sibling submodules under src/), add the new raw_buf submodule with the spec's Form-A source, wire parse_raw_buf + workspace injection through ailang-surface, and ratify the checker-visible surface via four new tests (round-trip + 3 E2E).

Architecture: Three concerns layered into seven tasks. Task 1 does the wholesale crate-rename + reshape (kernel_stub re-housed under src/kernel_stub/, public-symbol surface preserved via pub use kernel_stub::SOURCE as STUB_AIL;). Task 2 adds the raw_buf submodule + RAW_BUF_AIL re-export. Task 3 wires raw_buf into ailang-surface (parse fn + re-export + workspace injection + workspace_pin bump). Tasks 4-7 land the four tests. The codegen path stays unchanged — the iter raw-buf.2 acceptance is the existing Term::New-deferral diagnostic firing precisely on (new RawBuf …) consumer code (a new intercept-not-registered diagnostic was hedged in the spec but rejected at plan time as disposable infra; the existing deferral already names "milestone raw-buf" and is self-describing).

Tech Stack: Rust 1.x (stable), crates/ailang-kernel/ (new family-crate), crates/ailang-surface/ (parse + workspace injection), crates/ailang-core/tests/ (round-trip + workspace_pin), crates/ail/tests/ (three E2E), examples/ (three fixtures).


Files this plan creates or modifies:

  • Create: crates/ailang-kernel/Cargo.toml — renamed manifest, package name ailang-kernel.
  • Create: crates/ailang-kernel/src/lib.rs — family-crate hub with mod declarations and re-exports.
  • Create: crates/ailang-kernel/src/kernel_stub/mod.rspub const SOURCE: &str = include_str!("source.ail"); + the rustdoc lifted from the old STUB_AIL rustdoc.
  • Create: crates/ailang-kernel/src/kernel_stub/source.ail — the Form-A text currently embedded in crates/ailang-kernel-stub/src/lib.rs:27-37 (the r#"…"# body), now an .ail file.
  • Create: crates/ailang-kernel/src/raw_buf/mod.rs — same shape as kernel_stub/mod.rs.
  • Create: crates/ailang-kernel/src/raw_buf/source.ail — the new Form-A text per spec § Concrete code shapes "Primary — the raw-buf kernel-tier module" (lines 158-201 of the spec).
  • Delete: crates/ailang-kernel-stub/Cargo.toml — replaced by the renamed file at crates/ailang-kernel/Cargo.toml.
  • Delete: crates/ailang-kernel-stub/src/lib.rs — content fans out into the new lib.rs (re-exports) + kernel_stub/{mod.rs, source.ail}.
  • Delete: crates/ailang-kernel-stub/ (whole folder, after contents are evacuated by the above).
  • Modify: Cargo.toml:7 — workspace member rename.
  • Modify: Cargo.toml:37 — workspace dep alias rename.
  • Modify: crates/ailang-surface/Cargo.toml:9 — dep name rename.
  • Modify: crates/ailang-surface/src/loader.rs:45-57 — rename references ailang_kernel_stub::STUB_AILailang_kernel::STUB_AIL (rustdoc L48, call L55, expect-message L56) AND add parse_raw_buf fn after parse_kernel_stub.
  • Modify: crates/ailang-surface/src/loader.rs:123-138 — extend the workspace injection in load_workspace to also inject raw_buf.
  • Modify: crates/ailang-surface/src/lib.rs:39 — extend the re-export to also include parse_raw_buf.
  • Modify: crates/ailang-core/tests/workspace_pin.rs:52-58 — bump count 4 → 5, extend the auto-injects comment, add contains_key("raw_buf") assertion.
  • Modify: design/INDEX.md:112 — update the path-ref in the kernel-extensions Models row (ailang-kernel-stubailang-kernel/src/kernel_stub). Retire-language stays for raw-buf.3.
  • Modify: CLAUDE.md — update the "Code layout" table row for the renamed crate (path + one-line role description; the role transitions from "carries STUB_AIL only" to "family-crate hosting all kernel-tier modules").
  • Test: crates/ailang-core/tests/design_schema_drift.rs:765+ — add raw_buf_module_round_trips after the existing kernel_stub_module_round_trips test.
  • Test: examples/raw_buf_consumer_int.ail — fixture for the check-passes E2E.
  • Test: examples/raw_buf_consumer_str_reject.ail — fixture for the param-in reject E2E.
  • Test: examples/raw_buf_consumer_build_defer.ail — fixture for the Term::New-deferral E2E.
  • Test: crates/ail/tests/raw_buf_check_e2e.rsraw_buf_check_e2e_consumer_passesail check exit 0 on the int-element consumer.
  • Test: crates/ail/tests/raw_buf_param_in_reject_e2e.rsraw_buf_param_in_reject_consumer_fails_at_checkail check exits non-zero with param-not-in-restricted-set substring.
  • Test: crates/ail/tests/raw_buf_build_intercept_missing_e2e.rsraw_buf_build_term_new_defers_to_raw_buf_3ail build exits non-zero with Term::New requires the type's substring.

Task 1: Crate rename + family-crate reshape — kernel_stub re-housed, public-symbol surface preserved

Files:

  • Move: crates/ailang-kernel-stub/crates/ailang-kernel/ (and reshape src/ inside)

  • Modify: Cargo.toml:7, Cargo.toml:37

  • Modify: crates/ailang-surface/Cargo.toml:9

  • Modify: crates/ailang-surface/src/loader.rs:45-57

  • Modify: design/INDEX.md:112

  • Modify: CLAUDE.md (Code-layout table row)

  • Step 1: Move the crate folder.

Run: git mv crates/ailang-kernel-stub crates/ailang-kernel Expected: folder rename succeeds; git status shows the renames.

  • Step 2: Rewrite crates/ailang-kernel/Cargo.toml package name.

Edit crates/ailang-kernel/Cargo.toml. Change the name = "ailang-kernel-stub" line to name = "ailang-kernel". Leave all other lines (edition, version, dependencies — currently empty [dependencies]) unchanged. The package description (if any) may stay verbatim or get an inline updated phrase: "Family-crate hosting the embedded Form-A sources of every kernel-tier module."

  • Step 3: Carve out kernel_stub/source.ail from the current lib.rs body.

Create the new file crates/ailang-kernel/src/kernel_stub/source.ail with the exact Form-A text currently embedded between the r#" and "# markers in crates/ailang-kernel/src/lib.rs at lines 27-37 (the STUB_AIL const body). Verbatim — preserve every newline, indent, and trailing newline. The expected content is:

(module kernel_stub
  (kernel)
  (data StubT (vars a)
    (ctor Stub a)
    (param-in (a Int Float)))
  (fn new
    (doc "Construct StubT<Int> from an Int. Exercises Term::New end-to-end.")
    (type (fn-type (params (con Int)) (ret (con StubT (con Int)))))
    (params x)
    (body (term-ctor StubT Stub x))))

(Verify against the source bytes before writing — the implementer must git show HEAD:crates/ailang-kernel-stub/src/lib.rs if the current lib.rs body diverges; the source is authoritative.)

  • Step 4: Create crates/ailang-kernel/src/kernel_stub/mod.rs.

Write the following exact content:

//! Kernel-stub submodule — minimal ratifying fixture for the
//! kernel-extension-mechanics milestone (prep.3). The Form-A
//! text lives in the sibling `source.ail`.
//!
//! No domain content. The stub may be retired or repositioned
//! when a real second kernel-tier consumer (the `raw-buf`
//! milestone) lands; until then it stays as the
//! ratifying-fixture anchor.

/// Source-of-truth Form-A text for the stub kernel module. Parsed
/// by `ailang_surface::parse_kernel_stub` (and round-trip-pinned by
/// `kernel_stub_module_round_trips` in `design_schema_drift.rs`).
///
/// Ships a `(fn new ...)` def so consumers can spell `(new StubT 42)`,
/// the worked-consumer example from prep.3 of the spec — without it,
/// the stub's `Term::New` end-to-end ratification story is broken
/// (per fieldtest F4, 2026-05-28).
pub const SOURCE: &str = include_str!("source.ail");
  • Step 5: Rewrite crates/ailang-kernel/src/lib.rs as the family-crate hub.

Replace the entire contents of crates/ailang-kernel/src/lib.rs with:

//! Kernel-tier modules — family-crate hosting the Form-A source
//! of every kernel-tier `.ail` module the AILang workspace
//! auto-injects. One Cargo crate, one Rust submodule per
//! kernel-tier module, one `pub const SOURCE` per submodule.
//!
//! The acyclic dependency chain is preserved:
//! `ailang-surface → ailang-kernel → ailang-core`. Per-module
//! parse hops live in `ailang-surface` (`parse_kernel_stub`,
//! `parse_raw_buf`, …) so this crate carries zero parser code.

mod kernel_stub;

pub use kernel_stub::SOURCE as STUB_AIL;

(Task 2 will add a mod raw_buf; line and a matching re-export — leave them out of this task; the kernel_stub submodule alone must build clean here so the rename can land as a strict refactor.)

  • Step 6: Update Cargo.toml:7 workspace member.

Edit the workspace Cargo.toml. Find line 7 (the member entry currently reading "crates/ailang-kernel-stub",) and change it to "crates/ailang-kernel",. Preserve the trailing comma and surrounding whitespace.

  • Step 7: Update Cargo.toml:37 workspace dep alias.

Edit the workspace Cargo.toml. Find line 37 (the dep alias currently reading ailang-kernel-stub = { path = "crates/ailang-kernel-stub" }) and change it to ailang-kernel = { path = "crates/ailang-kernel" }.

  • Step 8: Update crates/ailang-surface/Cargo.toml:9 dep name.

Edit crates/ailang-surface/Cargo.toml. Find line 9 (ailang-kernel-stub.workspace = true) and change to ailang-kernel.workspace = true.

  • Step 9: Update crates/ailang-surface/src/loader.rs use-paths + rustdoc + expect-message.

Three edits in this file:

(a) Line 48 (rustdoc): change Source-of-truth: \ailang_kernel_stub::STUB_AIL`.toSource-of-truth: `ailang_kernel::STUB_AIL`.`

(b) Line 55 (call): change crate::parse(ailang_kernel_stub::STUB_AIL) to crate::parse(ailang_kernel::STUB_AIL).

(c) Line 56 (expect-message): change "ailang_kernel_stub::STUB_AIL must parse as a Module" to "ailang_kernel::STUB_AIL must parse as a Module".

  • Step 10: Update design/INDEX.md:112.

Edit design/INDEX.md. The Models-section row for kernel-extensions (line 112) currently contains the literal ailang-kernel-stub referring to the crate path. Change every ailang-kernel-stub occurrence in that row to ailang-kernel/src/kernel_stub (the new submodule path). The retire-language ("planned for retirement when raw-buf lands a real second consumer") stays — raw-buf.3 close updates that sentence.

  • Step 11: Update CLAUDE.md "Code layout" table row for the renamed crate.

Edit CLAUDE.md. Locate the "Code layout" table; find the row that currently reads crates/ailang-kernel-stub/ in the path column. Change the path to crates/ailang-kernel/. Update the role-description column to:

Family-crate hosting the Form-A source of every kernel-tier .ail module. Sub-modules under src/ carry one pub const SOURCE each (kernel_stub, raw_buf from raw-buf.2 onwards). Parse hops live in ailang-surface; this crate is the zero-dependency leaf that lets ailang-surface → ailang-kernel → ailang-core stay acyclic.

The "Zero-dependency leaf … kernel_stub ratifying-fixture" language is retired now that the crate hosts more than one submodule. The raw-buf milestone retire-language stays in raw-buf.3 close.

  • Step 12: Build the workspace.

Run: cargo build --workspace 2>&1 | tail -8 Expected: build succeeds; zero errors, zero warnings about ailang-kernel-stub (the crate is fully renamed; no dangling refs).

  • Step 13: Run the full workspace test suite to confirm zero behavioural change.

Run: cargo test --workspace 2>&1 | grep -E "^test result:" | awk -F"[: ;]+" '{p+=$4; f+=$6; i+=$8} END{print "passed:",p," failed:",f," ignored:",i}' Expected: passed: 667 failed: 0 ignored: 2 (same as the post-raw-buf.1 baseline; the rename is pure refactor, no test count change yet).


Task 2: Add raw_buf submodule + RAW_BUF_AIL re-export

Files:

  • Create: crates/ailang-kernel/src/raw_buf/mod.rs

  • Create: crates/ailang-kernel/src/raw_buf/source.ail

  • Modify: crates/ailang-kernel/src/lib.rs (add mod raw_buf; + re-export)

  • Step 1: Create crates/ailang-kernel/src/raw_buf/source.ail.

Write the Form-A source verbatim per spec § "Primary — the raw-buf kernel-tier module" (spec lines 158-201). The content is:

(module raw_buf
  (kernel)
  (data RawBuf (vars a)
    (ctor B (storage-tag))
    (param-in (a Int Float Bool)))

  (fn new
    (doc "Allocate uninitialised RawBuf of capacity n. Codegen
          intercept new__RawBuf__<T> emits @ailang_rc_alloc + the
          size header. Element bytes are uninitialised — caller
          must (set …) before (get …).")
    (type (fn-type (params (con Int)) (ret (own (RawBuf a)))))
    (params n)
    (body (term-ctor RawBuf B n)))

  (fn get
    (doc "Indexed read. UB if i >= (size b); caller checks
          bounds. Codegen intercept RawBuf_get__<T> emits
          getelementptr + load.")
    (type (fn-type
            (params (borrow (RawBuf a)) (con Int))
            (ret (con a))))
    (params b i)
    (body (term-ctor RawBuf B 0)))

  (fn set
    (doc "Indexed write. Linear: own in, own out. Under
          uniqueness, in-place; under shared, copy-on-write
          (Issue #22 territory). Codegen intercept
          RawBuf_set__<T> emits getelementptr + store.")
    (type (fn-type
            (params (own (RawBuf a)) (con Int) (con a))
            (ret (own (RawBuf a)))))
    (params b i v)
    (body b))

  (fn size
    (doc "Element count. Codegen intercept RawBuf_size__<T>
          emits a single i64 load from the slab header.")
    (type (fn-type (params (borrow (RawBuf a))) (ret (con Int))))
    (params b)
    (body 0)))

End the file with a single newline (no trailing blank lines). The placeholder bodies ((term-ctor RawBuf B n), (term-ctor RawBuf B 0), b, 0) match the existing convention from examples/prelude.ail — codegen never executes them (the intercept registry will override in raw-buf.3); they exist solely to satisfy the round-trip invariant.

  • Step 2: Create crates/ailang-kernel/src/raw_buf/mod.rs.

Write:

//! Raw-buf submodule — embeds the Form-A source of the `raw_buf`
//! kernel-tier module. Parsed by `ailang_surface::parse_raw_buf`
//! (and round-trip-pinned by `raw_buf_module_round_trips` in
//! `design_schema_drift.rs`).

/// Source-of-truth Form-A text for the `raw_buf` kernel module.
///
/// Declares `(data RawBuf (vars a) (ctor B …) (param-in (a Int Float Bool)))`
/// plus `new`, `get`, `set`, `size` fns with placeholder bodies.
/// The codegen intercept entries that replace the placeholder
/// bodies at lowering time ship in raw-buf.3.
pub const SOURCE: &str = include_str!("source.ail");
  • Step 3: Extend crates/ailang-kernel/src/lib.rs to register the new submodule + re-export its SOURCE.

Edit crates/ailang-kernel/src/lib.rs. After the existing mod kernel_stub; line, add mod raw_buf;. After the existing pub use kernel_stub::SOURCE as STUB_AIL; line, add pub use raw_buf::SOURCE as RAW_BUF_AIL;. The final file content is:

//! Kernel-tier modules — family-crate hosting the Form-A source
//! of every kernel-tier `.ail` module the AILang workspace
//! auto-injects. One Cargo crate, one Rust submodule per
//! kernel-tier module, one `pub const SOURCE` per submodule.
//!
//! The acyclic dependency chain is preserved:
//! `ailang-surface → ailang-kernel → ailang-core`. Per-module
//! parse hops live in `ailang-surface` (`parse_kernel_stub`,
//! `parse_raw_buf`, …) so this crate carries zero parser code.

mod kernel_stub;
mod raw_buf;

pub use kernel_stub::SOURCE as STUB_AIL;
pub use raw_buf::SOURCE as RAW_BUF_AIL;
  • Step 4: Build the workspace.

Run: cargo build --workspace 2>&1 | tail -8 Expected: build succeeds. A dead-code warning on RAW_BUF_AIL may fire (the re-export is consumed only after Task 3 wires it through ailang-surface); accept it.

  • Step 5: Full workspace test run — no test count change yet.

Run: cargo test --workspace 2>&1 | grep -E "^test result:" | awk -F"[: ;]+" '{p+=$4; f+=$6; i+=$8} END{print "passed:",p," failed:",f," ignored:",i}' Expected: passed: 667 failed: 0 ignored: 2. Same as Task 1 (the raw_buf submodule exists but no test consumes it).


Task 3: Wire raw_buf into ailang-surface — parse fn + re-export + workspace injection + workspace_pin bump

Files:

  • Modify: crates/ailang-surface/src/loader.rs (add parse_raw_buf after parse_kernel_stub; extend load_workspace injection)

  • Modify: crates/ailang-surface/src/lib.rs:39 (extend re-export)

  • Modify: crates/ailang-core/tests/workspace_pin.rs:52-58 (count bump + comment + new assertion)

  • Step 1: Add parse_raw_buf in crates/ailang-surface/src/loader.rs.

Insert the following block immediately after the existing parse_kernel_stub fn (which currently ends at line 57). Place it before the fn is_ail_source(path: &Path) -> bool { line at L59:

/// raw-buf.2 (raw-buf milestone): parse the embedded raw_buf
/// kernel-tier module bytes into a `Module`. Mirror of
/// [`parse_kernel_stub`].
///
/// Source-of-truth: `ailang_kernel::RAW_BUF_AIL`. Declares the
/// `RawBuf` TypeDef with `param-in (a Int Float Bool)` plus the
/// four operation signatures (`new`, `get`, `set`, `size`) with
/// placeholder bodies that codegen replaces via the intercept
/// registry (raw-buf.3).
///
/// Panics on parse failure — the source is build-time-validated
/// by every drift test run.
pub fn parse_raw_buf() -> Module {
    crate::parse(ailang_kernel::RAW_BUF_AIL)
        .expect("ailang_kernel::RAW_BUF_AIL must parse as a Module")
}
  • Step 2: Extend the workspace injection in load_workspace (loader.rs).

The current injection block at lines 123-132 reserves the name kernel_stub and inserts parse_kernel_stub() into the modules map. Insert an analogous block for raw_buf immediately AFTER the kernel_stub block (after line 132, before the existing // Derive the implicit-imports list … comment at L134):

    // parse_raw_buf() injects the raw_buf kernel-tier base
    // extension — declares the `RawBuf` TypeDef with the
    // `param-in (a Int Float Bool)` element-type restriction and
    // the four operation signatures. Carries `(kernel)`, so the
    // kernel-flag filter below picks it up automatically. Per
    // raw-buf milestone (Gitea #7); codegen intercepts ship in
    // raw-buf.3.
    if modules.contains_key("raw_buf") {
        return Err(WorkspaceLoadError::ReservedModuleName {
            name: "raw_buf".to_string(),
        });
    }
    modules.insert("raw_buf".to_string(), parse_raw_buf());
  • Step 3: Extend the re-export in crates/ailang-surface/src/lib.rs:39.

Edit crates/ailang-surface/src/lib.rs. Locate line 39 — the pub use loader::{load_module, load_workspace, parse_kernel_stub, parse_prelude, PRELUDE_AIL}; block. Insert parse_raw_buf into the list (alphabetically between parse_prelude and PRELUDE_AIL, or anywhere in the brace-list — the order does not matter functionally). Result:

pub use loader::{load_module, load_workspace, parse_kernel_stub, parse_prelude, parse_raw_buf, PRELUDE_AIL};
  • Step 4: Bump workspace_pin.rs:52-58 — count, comment, and the new contains_key assertion.

Edit crates/ailang-core/tests/workspace_pin.rs. Lines 52-58 currently read:

    // the loader auto-injects two built-in kernel-tier modules
    // (`prelude` and `kernel_stub` — see prep.3 of the
    // kernel-extension-mechanics milestone), so the count is the
    // user's two modules plus those two.
    assert_eq!(ws.modules.len(), 4);
    assert!(ws.modules.contains_key("prelude"));
    assert!(ws.modules.contains_key("kernel_stub"));

Replace with:

    // the loader auto-injects three built-in kernel-tier modules
    // (`prelude`, `kernel_stub`, and `raw_buf` — the last added
    // in raw-buf.2 of the raw-buf milestone), so the count is
    // the user's two modules plus those three. raw-buf.3 will
    // retire `kernel_stub`, dropping the count back to 4.
    assert_eq!(ws.modules.len(), 5);
    assert!(ws.modules.contains_key("prelude"));
    assert!(ws.modules.contains_key("kernel_stub"));
    assert!(ws.modules.contains_key("raw_buf"));
  • Step 5: Build the workspace.

Run: cargo build --workspace 2>&1 | tail -8 Expected: build succeeds; the RAW_BUF_AIL dead-code warning (from Task 2) is gone (the re-export is now consumed by parse_raw_buf).

  • Step 6: Run the workspace_pin test specifically to confirm the count assertion.

Run: cargo test -p ailang-core --test workspace_pin loads_example_workspace_happy_path Expected: PASS — ws.modules.len() is now 5, all three contains_key assertions hold.

  • Step 7: Full workspace test run.

Run: cargo test --workspace 2>&1 | grep -E "^test result:" | awk -F"[: ;]+" '{p+=$4; f+=$6; i+=$8} END{print "passed:",p," failed:",f," ignored:",i}' Expected: passed: 667 failed: 0 ignored: 2. No test count change yet (the bump is a 4→5 edit on an existing assertion, not a new test).


Task 4: Round-trip test raw_buf_module_round_trips

Files:

  • Modify: crates/ailang-core/tests/design_schema_drift.rs:765+ (append after the existing kernel_stub round-trip test which ends at L764)

  • Step 1: Append raw_buf_module_round_trips after L764.

Edit crates/ailang-core/tests/design_schema_drift.rs. After the closing } of kernel_stub_module_round_trips at line 764, insert the following test:


/// raw-buf.2 (raw-buf milestone): pin the JSON byte-shape of the
/// raw_buf kernel-tier base-extension module. Mirror of
/// `kernel_stub_module_round_trips`. Catches regressions in
/// `Module.kernel`, `TypeDef.param-in` (with three elements:
/// Int / Float / Bool), and the ctor + four-fn manifest shape
/// the spec lists in § Concrete code shapes lines 158-201.
#[test]
fn raw_buf_module_round_trips() {
    let m = ailang_surface::parse_raw_buf();
    assert!(m.kernel, "raw_buf module is kernel-tier");
    assert_eq!(m.name, "raw_buf");

    let json = serde_json::to_value(&m).expect("serialise raw_buf module");
    assert_eq!(json["kernel"], true);
    assert_eq!(json["name"], "raw_buf");

    let recovered: Module =
        serde_json::from_value(json).expect("raw_buf module round-trips through serde");
    assert!(recovered.kernel);
    assert_eq!(recovered.name, "raw_buf");

    // Locate the RawBuf TypeDef and confirm param_in is intact
    // for all three element types Int / Float / Bool (spec §
    // Concrete code shapes line 163: `(param-in (a Int Float Bool))`).
    let td = recovered.defs.iter().find_map(|d| match d {
        Def::Type(t) if t.name == "RawBuf" => Some(t),
        _ => None,
    }).expect("RawBuf TypeDef present in raw_buf module");
    let allowed = td.param_in.get("a").expect("RawBuf.a restricted");
    assert!(allowed.contains("Int"));
    assert!(allowed.contains("Float"));
    assert!(allowed.contains("Bool"));
}
  • Step 2: Run the new round-trip test.

Run: cargo test -p ailang-core --test design_schema_drift raw_buf_module_round_trips Expected: PASS — the parsed raw_buf module serialises to JSON, round-trips through serde, and the param_in for a contains all three element types.

  • Step 3: Full workspace test run — count goes up by one.

Run: cargo test --workspace 2>&1 | grep -E "^test result:" | awk -F"[: ;]+" '{p+=$4; f+=$6; i+=$8} END{print "passed:",p," failed:",f," ignored:",i}' Expected: passed: 668 failed: 0 ignored: 2 (+1 from the new round-trip).


Task 5: E2E raw_buf_check_e2e — consumer with (con RawBuf (con Int)) checks clean

Files:

  • Create: examples/raw_buf_consumer_int.ail

  • Create: crates/ail/tests/raw_buf_check_e2e.rs

  • Step 1: Create examples/raw_buf_consumer_int.ail.

Write:

(module raw_buf_consumer_int
  (fn read_first
    (doc "Consumer demonstrating (con RawBuf (con Int)) in a type slot. Kernel-tier auto-import (prep.3) makes RawBuf visible without an explicit (import raw_buf). The body calls RawBuf.get via the type-scoped namespacing landed in prep.1.")
    (type (fn-type (params (borrow (RawBuf (con Int)))) (ret (con Int))))
    (params buf)
    (body (app RawBuf.get buf 0))))
  • Step 2: Create crates/ail/tests/raw_buf_check_e2e.rs.

Write:

//! E2E: a consumer fixture that mentions `(con RawBuf (con Int))`
//! in a type signature and calls `RawBuf.get` must type-check
//! clean. raw-buf.2 ratification — the kernel-tier auto-import
//! path (prep.3) makes `RawBuf` visible without an explicit
//! `(import raw_buf)`, and type-scoped namespacing (prep.1)
//! resolves `RawBuf.get` to the kernel-tier fn.

use std::path::PathBuf;
use std::process::Command;

#[test]
fn raw_buf_check_e2e_consumer_passes() {
    let manifest_dir = env!("CARGO_MANIFEST_DIR");
    let repo_root = PathBuf::from(manifest_dir).join("../..");
    let fixture = repo_root.join("examples/raw_buf_consumer_int.ail");

    let check = Command::new(env!("CARGO_BIN_EXE_ail"))
        .arg("check")
        .arg(&fixture)
        .output()
        .expect("ail check failed to spawn");
    assert!(
        check.status.success(),
        "ail check failed: stdout={} stderr={}",
        String::from_utf8_lossy(&check.stdout),
        String::from_utf8_lossy(&check.stderr)
    );
}
  • Step 3: Run the new test.

Run: cargo test -p ail --test raw_buf_check_e2e raw_buf_check_e2e_consumer_passes Expected: PASS — ail check exits 0 on the consumer fixture.

  • Step 4: Full workspace test run — count goes up by one.

Run: cargo test --workspace 2>&1 | grep -E "^test result:" | awk -F"[: ;]+" '{p+=$4; f+=$6; i+=$8} END{print "passed:",p," failed:",f," ignored:",i}' Expected: passed: 669 failed: 0 ignored: 2 (+1 from this E2E).


Task 6: E2E raw_buf_param_in_reject_e2e — consumer with (con RawBuf (con Str)) rejected at check

Files:

  • Create: examples/raw_buf_consumer_str_reject.ail

  • Create: crates/ail/tests/raw_buf_param_in_reject_e2e.rs

  • Step 1: Create examples/raw_buf_consumer_str_reject.ail.

Write:

(module raw_buf_consumer_str_reject
  (fn cant_str_raw
    (doc "Must-fail fixture. RawBuf's TypeDef ships `(param-in (a Int Float Bool))` — instantiating with Str must be rejected by the checker with the prep.3 `param-not-in-restricted-set` diagnostic. Mirrors fieldtest F8 on the prep.3 stub.")
    (type (fn-type (params (borrow (RawBuf (con Str)))) (ret (con Str))))
    (params buf)
    (body (app RawBuf.get buf 0))))
  • Step 2: Create crates/ail/tests/raw_buf_param_in_reject_e2e.rs.

Write:

//! E2E: a consumer fixture that instantiates `(con RawBuf (con Str))`
//! in any type slot must be rejected by `ail check`, because
//! RawBuf's TypeDef declares `(param-in (a Int Float Bool))` —
//! Str is outside the restricted set. raw-buf.2 ratification of
//! the param-in mechanism on a real-payload extension (the
//! prep.3 stub fixture exercised it only on the StubT type).

use std::path::PathBuf;
use std::process::Command;

#[test]
fn raw_buf_param_in_reject_consumer_fails_at_check() {
    let manifest_dir = env!("CARGO_MANIFEST_DIR");
    let repo_root = PathBuf::from(manifest_dir).join("../..");
    let fixture = repo_root.join("examples/raw_buf_consumer_str_reject.ail");

    let check = Command::new(env!("CARGO_BIN_EXE_ail"))
        .arg("check")
        .arg(&fixture)
        .output()
        .expect("ail check failed to spawn");
    assert!(
        !check.status.success(),
        "ail check unexpectedly succeeded: stdout={} stderr={}",
        String::from_utf8_lossy(&check.stdout),
        String::from_utf8_lossy(&check.stderr)
    );
    let stderr = String::from_utf8_lossy(&check.stderr);
    assert!(
        stderr.contains("param-not-in-restricted-set"),
        "expected `param-not-in-restricted-set` substring in stderr; got: {stderr}"
    );
}
  • Step 3: Run the new test.

Run: cargo test -p ail --test raw_buf_param_in_reject_e2e raw_buf_param_in_reject_consumer_fails_at_check Expected: PASS — ail check exits non-zero with the param-not-in-restricted-set diagnostic code in stderr.

(If the diagnostic format wraps the code (e.g. [param-not-in-restricted-set] with brackets, or error[param-not-in-restricted-set]:), the substring assertion still hits — the test asserts on the bare code string. If the actual diagnostic uses a different code spelling (e.g. underscore-separated), update the assertion substring to match the reality; the prep.3 fieldtest F8 verified the code as written above.)

  • Step 4: Full workspace test run — count goes up by one.

Run: cargo test --workspace 2>&1 | grep -E "^test result:" | awk -F"[: ;]+" '{p+=$4; f+=$6; i+=$8} END{print "passed:",p," failed:",f," ignored:",i}' Expected: passed: 670 failed: 0 ignored: 2.


Task 7: E2E raw_buf_build_intercept_missing_e2e — Term::New deferral at build

Files:

  • Create: examples/raw_buf_consumer_build_defer.ail

  • Create: crates/ail/tests/raw_buf_build_intercept_missing_e2e.rs

  • Step 1: Create examples/raw_buf_consumer_build_defer.ail.

Write:

(module raw_buf_consumer_build_defer
  (fn main
    (doc "Term::New invocation: the checker accepts (new RawBuf …) because raw_buf ships (fn new …), but the codegen defers Term::New until raw-buf.3 ships the desugar pass. `ail check` clean; `ail build` fails with the existing deferral message naming `milestone raw-buf`. raw-buf.3 inverts this test to PASS once the desugar lands.")
    (type (fn-type (params) (ret (con Int))))
    (params)
    (body
      (let buf (new RawBuf (con Int) 3)
        (app RawBuf.get buf 0)))))
  • Step 2: Create crates/ail/tests/raw_buf_build_intercept_missing_e2e.rs.

Write:

//! E2E: a consumer fixture that invokes `(new RawBuf (con Int) 3)`
//! type-checks clean (RawBuf has a `(fn new …)` def, satisfying
//! prep.2's checker) but fails at `ail build` because the
//! codegen Term::New arm still emits the deferral diagnostic
//! that prep.2 introduced and raw-buf.3 will retire. This test
//! pins the manifest/codegen separation that is raw-buf.2's
//! load-bearing acceptance: the checker accepts but the binary
//! does not yet build. raw-buf.3 inverts this test to PASS
//! (delete + replace with an end-to-end build-and-run variant).

use std::path::PathBuf;
use std::process::Command;

#[test]
fn raw_buf_build_term_new_defers_to_raw_buf_3() {
    let manifest_dir = env!("CARGO_MANIFEST_DIR");
    let repo_root = PathBuf::from(manifest_dir).join("../..");
    let fixture = repo_root.join("examples/raw_buf_consumer_build_defer.ail");
    let bin = repo_root.join("target/debug/raw_buf_consumer_build_defer_test_bin");

    let build = Command::new(env!("CARGO_BIN_EXE_ail"))
        .arg("build")
        .arg(&fixture)
        .arg("-o")
        .arg(&bin)
        .output()
        .expect("ail build failed to spawn");
    assert!(
        !build.status.success(),
        "ail build unexpectedly succeeded: stdout={} stderr={}",
        String::from_utf8_lossy(&build.stdout),
        String::from_utf8_lossy(&build.stderr)
    );
    let stderr = String::from_utf8_lossy(&build.stderr);
    assert!(
        stderr.contains("Term::New requires the type's"),
        "expected Term::New deferral message; got: {stderr}"
    );
}
  • Step 3: Run the new test.

Run: cargo test -p ail --test raw_buf_build_intercept_missing_e2e raw_buf_build_term_new_defers_to_raw_buf_3 Expected: PASS — ail check accepts the fixture, then ail build errors with the Term::New deferral substring.

  • Step 4: Final full workspace test run.

Run: cargo test --workspace 2>&1 | grep -E "^test result:" | awk -F"[: ;]+" '{p+=$4; f+=$6; i+=$8} END{print "passed:",p," failed:",f," ignored:",i}' Expected: passed: 671 failed: 0 ignored: 2 — baseline 667 + 4 new tests (round-trip in Task 4 + three E2E in Tasks 5/6/7).


Self-review

  1. Spec coverage.

    • § Architecture point 2 ("Rename crates/ailang-kernel-stub/ to crates/ailang-kernel/ and re-shape it as a family-crate"): Task 1.
    • § Architecture point 2 ("the renamed crate carries src/kernel_stub/{mod.rs, source.ail} and gains src/raw_buf/{mod.rs, source.ail}"): Tasks 1 + 2.
    • § Architecture point 2 ("src/lib.rs re-exports each submodule's SOURCE const under a stable name"): Tasks 1 + 2.
    • § Architecture point 2 ("parse_kernel_stub joined by parse_raw_buf in ailang-surface"): Task 3.
    • § Architecture point 2 ("Checker accepts a consumer-fixture (con RawBuf (con Int)) in a type slot; ail check passes"): Task 5.
    • § Architecture point 2 ("ail build fails precisely with intercept-not-registered: new__RawBuf__Int (or the existing Term::New-deferral diagnostic)"): Task 7 (resolves the spec's "or" hedge by picking the Term::New-deferral path — no new diagnostic code; the existing message ships).
    • § Components row 2: covered by Tasks 1 + 2 + 3.
    • § Testing strategy raw-buf.2 bullet 1 (raw_buf_check_e2e): Task 5.
    • § Testing strategy raw-buf.2 bullet 2 (raw_buf_param_in_reject_e2e): Task 6.
    • § Testing strategy raw-buf.2 bullet 3 (raw_buf_build_intercept_missing_e2e): Task 7.
    • § Testing strategy raw-buf.2 bullet 4 (round-trip): Task 4.
    • § Testing strategy raw-buf.2 bullet 5 (workspace_pin 4→5): Task 3.
  2. Placeholder scan. No "TBD", no "TODO", no "implement later", no "similar to Task". The Task 6 Step 3 parenthetical ("If the diagnostic format wraps the code …") is a real fallback the implementer may need; it names the concrete adjustment, not a placeholder.

  3. Type consistency. Symbols used across tasks: STUB_AIL, RAW_BUF_AIL, SOURCE (per submodule), kernel_stub, raw_buf, parse_kernel_stub, parse_raw_buf, kernel_stub_module_round_trips, raw_buf_module_round_trips, ailang-kernel-stub (legacy), ailang-kernel (new). All consistent.

  4. Step granularity. Task 1 has 13 steps; each is one file edit or one command. Task 2: 5 steps. Task 3: 7 steps. Tasks 4-7: 3-4 steps each. All steps 2-5 min. Task 1 is the largest, but it must be atomic (the crate rename can't be half-done).

  5. No commit steps. None.

  6. Pin/replacement substring contiguity. Three substrings are asserted by tests against expected file content / stderr:

    • param-not-in-restricted-set — asserted by Task 6's test against ail check stderr. Not generated by any file content this plan ships; sourced from the existing checker diagnostic. No contiguity risk within plan-shipped text.
    • Term::New requires the type's — asserted by Task 7's test against ail build stderr. Sourced from the existing codegen deferral at crates/ailang-codegen/src/lib.rs:2075-2078. No contiguity risk.
    • The workspace_pin.rs assert_eq!(ws.modules.len(), 5); substring — the bumped value. The replacement-body text in Task 3 Step 4 contains this assertion verbatim and contiguously (no soft-wrap split).
  7. Compile-gate vs. deferred-caller ordering.

    • Task 1 changes the dep name ailang-kernel-stubailang-kernel. The compile-driven set is 8 sites across 6 files; ALL are threaded inside Task 1. No deferred caller. Task 1 ends with cargo build --workspace + cargo test --workspace green.
    • Task 3 changes the workspace count (injection adds raw_buf — workspace_pin count assertion goes from 4 to 5). The bump is bundled into Task 3 Step 4 — no deferred fix across the compile gate.
    • Task 5 / 6 / 7 introduce new tests; no signature changes elsewhere.
  8. Verification-command filter strings must resolve. Every cargo test … <filter> invocation uses a fn name THIS PLAN ships (Tasks 4-7) or an existing fn confirmed by recon (loads_example_workspace_happy_path in Task 3 Step 6). Zero filter-zero-match risk: the new tests are written in the same task that runs them, and the loads_example_workspace_happy_path test exists at crates/ailang-core/tests/workspace_pin.rs:46 per recon. Unfiltered cargo test --workspace at the end of each task uses a counter-based assertion (the awk pipeline), so "zero tests ran" cannot masquerade as success.