# raw-buf.3 — Type-scoped polymorphic intrinsic mechanism — 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:** Make a type-scoped polymorphic top-level `(intrinsic)`
op mint a *scope-qualified* mono symbol (`StubT_peek__Int`, not
`peek__Int`) and have the intrinsic↔intercept bijection express
one such polymorphic marker as its N per-`param-in`-element entries
— ratified standalone on the stub via a new `StubT.peek` op.

**Architecture:** Two tasks. Task 1 threads a TypeDef-scope through
the existing free-fn mono path: a new `scope: Option<String>` field
on `FreeFnCall` and `MonoTarget::FreeFn`, populated only when a call
resolved via the type-scoped `T.f` spelling, consumed at the two
mono-symbol mint sites (which already read the same `MonoTarget`, so
they stay in lockstep) and folded into the dedup key. Task 2 adds
the `StubT.peek` ratifier op, extends the bijection collector to
expand a polymorphic type-scoped intrinsic over its TypeDef's
`param-in` set (computing the *identical* scoped strings via the
same `mono_symbol_n`), registers the two `StubT_peek__*` entries,
and adds the mono-symbol + round-trip tests. No RawBuf, no
`Term::New`, no running E2E this iteration (constructing a `StubT`
needs the `Term::New` desugar that lands in raw-buf.4); ratification
is unit/integration tests + the bijection pin.

**Tech Stack:** Rust. `crates/ailang-check/src/lib.rs` (resolution +
`FreeFnCall`), `crates/ailang-check/src/mono.rs` (mint + key),
`crates/ailang-codegen/src/intercepts.rs` (collector + entries +
emit), `crates/ailang-kernel/src/kernel_stub/source.ail` (the
ratifier op), `crates/ailang-core/tests/design_schema_drift.rs`
(round-trip), a new mono-symbol test.

---

**Files this plan creates or modifies:**

- Modify: `crates/ailang-check/src/lib.rs:2741` — add `scope: Option<String>` to `FreeFnCall`.
- Modify: `crates/ailang-check/src/lib.rs:3535` (+ every other `free_fn_owner = Some(..)` site) — carry the scope through the resolution tuple; populate `Some(prefix)` only on the type-scoped dotted branch.
- Modify: `crates/ailang-check/src/lib.rs:3602` — set `scope` on the `FreeFnCall` push.
- Modify: `crates/ailang-check/src/mono.rs:577` — add `scope: Option<String>` to `MonoTarget::FreeFn`.
- Modify: `crates/ailang-check/src/mono.rs:600-607` — fold `scope` into `mono_target_key`.
- Modify: `crates/ailang-check/src/mono.rs:857` — set `scope: fc.scope.clone()` at the `MonoTarget::FreeFn` construction.
- Modify: `crates/ailang-check/src/mono.rs:521` (nearby) — add a `scoped_base` helper.
- Modify: `crates/ailang-check/src/mono.rs:1080` + `:1176` — mint via `scoped_base(scope, name)` at both sites.
- Modify: `crates/ailang-kernel/src/kernel_stub/source.ail` — append the `peek` op.
- Modify: `crates/ailang-codegen/src/intercepts.rs:480-509` (`workspace_intrinsic_markers`) — type-scoped-poly expansion arm.
- Modify: `crates/ailang-codegen/src/intercepts.rs:35-174` (`INTERCEPTS`) + after `:459` (emit fns) — `StubT_peek__Int`, `StubT_peek__Float`.
- Modify: `crates/ailang-core/tests/design_schema_drift.rs:744-766` (`kernel_stub_module_round_trips`) — cover `peek`.
- Test: `crates/ail/tests/mono_scoped_symbol.rs` — new; asserts a type-scoped `StubT.peek` call monomorphises to `StubT_peek__Int` / `StubT_peek__Float`.

---

### Task 1: Thread the TypeDef scope into the free-fn mono symbol

**Files:**
- Modify: `crates/ailang-check/src/lib.rs:2741, 3535(+siblings), 3602`
- Modify: `crates/ailang-check/src/mono.rs:521, 577, 600-607, 857, 1080, 1176`

- [ ] **Step 1: Add the `scope` field to `FreeFnCall`.**

Edit `crates/ailang-check/src/lib.rs`. In the `FreeFnCall` struct (the `metas` field is last, at ~2755), append:

```rust
    /// The TypeDef scope the call resolved through, when it was
    /// spelled type-scoped (`T.f`, prep.1) — `Some("StubT")` for
    /// `StubT.peek`, `Some("RawBuf")` for `RawBuf.get`. `None` for a
    /// bare / dot-qualified / local resolution. raw-buf.3: drives the
    /// scope-qualified mono symbol `T_f__<elem>` so two types' ops
    /// of the same name never collide.
    pub scope: Option<String>,
```

- [ ] **Step 2: Carry the scope through the resolution tuple `free_fn_owner`.**

The local `free_fn_owner` is `Option<(String, String)>` `(owner_module, unqualified_name)`, set in several resolution branches and destructured at `lib.rs:3549`. Change its element type to `(String, String, Option<String>)` `(owner_module, unqualified_name, scope)`. The compiler flags every `free_fn_owner = Some((..))` site (and the destructure at 3549). Update each:

- The **type-scoped dotted branch** at `lib.rs:3535`, currently:
  ```rust
                (qualified, Some((target_module, suffix.to_string())))
  ```
  becomes:
  ```rust
                let scope = if type_home.is_some() {
                    Some(prefix.to_string())
                } else {
                    None
                };
                (qualified, Some((target_module, suffix.to_string(), scope)))
  ```
  (`type_home` and `prefix` are both live here — `type_home.is_some()` is exactly "resolved via the TypeDef-first ladder"; the legacy module-as-receiver sub-case has `type_home == None` → `scope = None`.)

- **Every other** `free_fn_owner = Some((m, n))` assignment (the local / same-module / implicit-import branches the compiler flags): add `, None` as the third element. These are not type-scoped, so `scope = None` — existing bare/dot-qualified symbols stay byte-identical.

- [ ] **Step 3: Destructure and thread the scope to the `FreeFnCall` push.**

At `lib.rs:3549`, the `if let (Type::Forall { .. }, Some((owner, unqualified_name))) = (&raw, &free_fn_owner)` destructure gains the third binding:
```rust
            if let (Type::Forall { vars, constraints, body }, Some((owner, unqualified_name, scope))) =
                (&raw, &free_fn_owner)
```
At the `FreeFnCall` push (`lib.rs:3602`), add the field:
```rust
                free_fn_calls.push(FreeFnCall {
                    name: unqualified_name.clone(),
                    owner_module: owner.clone(),
                    forall_vars: vars.clone(),
                    metas: metas.clone(),
                    scope: scope.clone(),
                });
```

- [ ] **Step 4: Add the `scope` field to `MonoTarget::FreeFn` + the `scoped_base` helper.**

Edit `crates/ailang-check/src/mono.rs`. In `MonoTarget::FreeFn` (the `defining_module` field is last, ~583), append:
```rust
        /// The TypeDef scope (`Some("StubT")`) when the call resolved
        /// type-scoped; `None` for a bare free fn. Part of the dedup
        /// key and the minted symbol base.
        scope: Option<String>,
```
Add the helper next to `mono_symbol_n` (after its definition, ~528):
```rust
/// raw-buf.3: the symbol base for a free-fn mono target. A
/// type-scoped op (`scope = Some("StubT")`, name `"peek"`) bases on
/// `"StubT_peek"`, so `mono_symbol_n` mints `StubT_peek__Int`; a
/// bare free fn (`scope = None`) keeps its bare base.
pub fn scoped_base(scope: &Option<String>, name: &str) -> String {
    match scope {
        Some(t) => format!("{t}_{name}"),
        None => name.to_string(),
    }
}
```

- [ ] **Step 5: Fold `scope` into `mono_target_key`.**

Edit `mono_target_key` (`mono.rs:600-607`). The `MonoTarget::FreeFn` arm currently keys on `("free", name.clone(), joined)`. Include the scope so a scoped op and a bare op of the same name never dedup-collide:
```rust
        MonoTarget::FreeFn { name, type_args, scope, .. } => {
            let joined = type_args
                .iter()
                .map(ailang_core::canonical::type_hash)
                .collect::<Vec<_>>()
                .join("|");
            ("free".into(), scoped_base(scope, name), joined)
        }
```

- [ ] **Step 6: Set `scope` at the `MonoTarget::FreeFn` construction.**

At `mono.rs:857` (`collect_mono_targets`'s `out.push(MonoTarget::FreeFn { .. })`), add `scope: fc.scope.clone(),`.

- [ ] **Step 7: Mint the scope-qualified symbol at both mint sites.**

- `mono.rs:1080` (`synthesise_mono_fn_for_free_fn`): the symbol is `mono_symbol_n(name, type_args.as_slice())`. This fn must receive the scope. Add a `scope: &Option<String>` parameter to `synthesise_mono_fn_for_free_fn`, pass `&target.scope` (or the destructured `scope`) from its caller (the `MonoTarget::FreeFn` match where it is invoked), and change the mint to:
  ```rust
        name: mono_symbol_n(&scoped_base(scope, name), type_args.as_slice()),
  ```
- `mono.rs:1176` (`rewrite_mono_calls`): the `MonoTarget::FreeFn { name: fn_name, type_args, defining_module }` arm — bind `scope` too and change:
  ```rust
                        MonoTarget::FreeFn { name: fn_name, type_args, defining_module, scope } => {
                            (mono_symbol_n(&scoped_base(scope, fn_name), type_args.as_slice()), defining_module.as_str())
                        }
  ```
  Both sites read the same `MonoTarget`, so the minted def name and the rewritten call name stay byte-identical.

- [ ] **Step 8: Build the workspace.**

Run: `cargo build --workspace 2>&1 | tail -8`
Expected: build succeeds; the compiler-flagged `free_fn_owner` / `MonoTarget::FreeFn` / `FreeFnCall` sites are all threaded (a missed site is a hard compile error here).

- [ ] **Step 9: Full suite — no regression (existing symbols unchanged).**

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`. No type-scoped polymorphic intrinsic exists yet (peek arrives in Task 2), and all existing free-fn calls resolve with `scope = None` → bare base → byte-identical symbols. `mono_hash_stability` (six prelude `eq__*`/`compare__*` hashes) and the bijection test stay green, confirming no existing symbol moved. (If any existing test exercised a `Type.fn`-spelled poly-free-fn call and pinned its bare symbol, it would flip here — investigate and report, do not adjust the assertion.)

---

### Task 2: StubT.peek ratifier + bijection expansion + entries + tests

**Files:**
- Modify: `crates/ailang-kernel/src/kernel_stub/source.ail`
- Modify: `crates/ailang-codegen/src/intercepts.rs` (collector + `INTERCEPTS` + emit fns)
- Modify: `crates/ailang-core/tests/design_schema_drift.rs:744-766`
- Test: `crates/ail/tests/mono_scoped_symbol.rs` (new)

- [ ] **Step 1: Append the `peek` op to the stub source.**

Edit `crates/ailang-kernel/src/kernel_stub/source.ail`. The file currently ends with the `answer` fn whose final line is `    (intrinsic)))` (the two closing parens close `answer` and the module). Insert the `peek` fn between `answer` and the module close: change `answer`'s last line from `    (intrinsic)))` to `    (intrinsic))` (close only `answer`), then append `peek` and re-close the module. The fn body (verified to parse + check clean in a kernel module this session):

```text
  (fn peek
    (doc "Ratifies the type-scoped polymorphic intrinsic mechanism: StubT.peek is polymorphic over a (param-in {Int,Float}) and type-scoped to StubT. Codegen intercepts StubT_peek__Int / StubT_peek__Float emit a load of the Stub payload. Retires with the stub in raw-buf.5.")
    (type (forall (vars a) (fn-type (params (borrow (con StubT a))) (ret a))))
    (params s)
    (intrinsic)))
```

The final file ends with `    (intrinsic)))` (closing `peek` + the module). Verify with `git diff` that only the `answer` close-paren count changed and `peek` was added — `new`/`answer` bodies are otherwise byte-untouched (a perturbation there would move the `.ll` snapshots).

- [ ] **Step 2: Extend the bijection collector with a type-scoped-poly arm.**

Edit `crates/ailang-codegen/src/intercepts.rs`, `workspace_intrinsic_markers()` (~480). The current `Def::Fn(f) if matches!(f.body, Term::Intrinsic)` arm (~490) inserts the bare `f.name`. Insert a MORE-SPECIFIC arm *before* it that handles a polymorphic (`Type::Forall`) intrinsic fn type-scoped to a same-module TypeDef:

```rust
                    // raw-buf.3: a polymorphic top-level (intrinsic) op
                    // type-scoped to a same-module TypeDef T (its
                    // signature mentions `(con T …)`) expands to one
                    // marker per element type in T's `param-in` set —
                    // the same `T_f__<elem>` strings the mono pass mints
                    // (mono::scoped_base + mono_symbol_n). One marker → N
                    // entries.
                    Def::Fn(f)
                        if matches!(f.body, Term::Intrinsic)
                            && matches!(f.ty, Type::Forall { .. }) =>
                    {
                        match scope_typedef_and_elems(f, &module) {
                            Some((tdef, elems)) => {
                                for elem in elems {
                                    markers.insert(mono_symbol_n(
                                        &format!("{tdef}_{}", f.name),
                                        std::slice::from_ref(&elem),
                                    ));
                                }
                            }
                            // a Forall intrinsic not scoped to a
                            // param-in TypeDef keeps the bare name (no
                            // such case ships today; future-proofing).
                            None => {
                                markers.insert(f.name.clone());
                            }
                        }
                    }
```

Add the helper `scope_typedef_and_elems` in the same file (or a private mod):

```rust
/// raw-buf.3: for a type-scoped polymorphic intrinsic fn `f`, return
/// `(TypeDef-name, element-types)` where the TypeDef is the unique
/// same-module `Def::Type` referenced via `(con T …)` in `f`'s
/// signature and the element types are its `param-in` set expressed as
/// `Type::Con` values (so `mono_symbol_n` produces the same suffix the
/// mono pass mints). `None` if `f` references zero or several
/// same-module TypeDefs.
fn scope_typedef_and_elems(
    f: &ailang_core::ast::FnDef,
    module: &ailang_core::ast::Module,
) -> Option<(String, Vec<Type>)> {
    use ailang_core::ast::{Def, Type as T};
    let typedef_names: std::collections::BTreeSet<&str> = module
        .defs
        .iter()
        .filter_map(|d| match d {
            Def::Type(td) => Some(td.name.as_str()),
            _ => None,
        })
        .collect();
    // collect Con-heads referenced anywhere in f's type
    let mut referenced: std::collections::BTreeSet<String> = Default::default();
    collect_con_heads(&f.ty, &mut referenced);
    let scoped: Vec<&String> = referenced
        .iter()
        .filter(|n| typedef_names.contains(n.as_str()))
        .collect();
    let tdef = match scoped.as_slice() {
        [one] => (*one).clone(),
        _ => return None,
    };
    let td = module.defs.iter().find_map(|d| match d {
        Def::Type(td) if td.name == tdef => Some(td),
        _ => None,
    })?;
    // param-in is keyed by the type var; take its allowed surface
    // names and build `Type::Con` element types.
    let allowed = td.param_in.values().next()?; // single-var TypeDefs
    let elems: Vec<T> = allowed
        .iter()
        .map(|s| T::Con { name: s.clone(), args: vec![] })
        .collect();
    Some((tdef, elems))
}
```

(`collect_con_heads` walks a `Type`, pushing every `Type::Con { name, .. }` head into the set; recurse through `Forall.body`, `Fn.params`/`ret`, `Borrow`/`Own` inner, and `Con.args`. Mirror the existing `Type` traversal in this crate. The implementer must confirm the exact `Type` variant names — `Type::Con { name, args }`, the borrow/own wrappers — against `crates/ailang-core/src/ast.rs` and the `param_in` field type `BTreeMap<String, BTreeSet<String>>` against the `TypeDef` struct; build a `Type::Con` exactly as `mono::type_to_mono_suffix` expects so the suffix matches.)

- [ ] **Step 3: Register the two `StubT_peek__*` entries + emit fns.**

Append to `INTERCEPTS` (`intercepts.rs:35-174`):

```rust
    Intercept {
        name: "StubT_peek__Int",
        expected_params: &["ptr"],   // borrow (con StubT Int) lowers to ptr
        expected_ret: "i64",
        wants_alwaysinline: false,
        emit: emit_stubt_peek_int,
    },
    Intercept {
        name: "StubT_peek__Float",
        expected_params: &["ptr"],
        expected_ret: "double",
        wants_alwaysinline: false,
        emit: emit_stubt_peek_float,
    },
```

Add the emit fns after `emit_answer` (~459). Mirror `emit_answer`'s shape (read the single param SSA from the tail of `emitter.locals`, load the Stub payload field, return it). Plausible bodies:

```rust
/// raw-buf.3 ratifier emit. NOTE: never instantiated to codegen this
/// iteration (no Term::New to build a StubT; unit-test-ratified only).
/// The load offset/correctness is exercised end-to-end only by
/// raw-buf.4's RawBuf path; here the entry exists to satisfy the
/// bijection. Retires with the stub in raw-buf.5.
pub(crate) fn emit_stubt_peek_int(emitter: &mut Emitter<'_>) -> Result<()> {
    let n = emitter.locals.len();
    let s = emitter.locals[n - 1].1.clone();
    let v = emitter.fresh_ssa();
    emitter.body.push_str(&format!("  {v} = load i64, ptr {s}\n"));
    emitter.body.push_str(&format!("  ret i64 {v}\n"));
    emitter.body.push_str("}\n\n");
    emitter.block_terminated = true;
    Ok(())
}

pub(crate) fn emit_stubt_peek_float(emitter: &mut Emitter<'_>) -> Result<()> {
    let n = emitter.locals.len();
    let s = emitter.locals[n - 1].1.clone();
    let v = emitter.fresh_ssa();
    emitter.body.push_str(&format!("  {v} = load double, ptr {s}\n"));
    emitter.body.push_str(&format!("  ret double {v}\n"));
    emitter.body.push_str("}\n\n");
    emitter.block_terminated = true;
    Ok(())
}
```

(The implementer must confirm against `emit_answer` / `emit_eq_str`: the exact way to read a param SSA (`emitter.locals[n-1].1` vs another accessor), the `fresh_ssa` name, and that `expected_params`/`expected_ret` match what `llvm_type` delivers for `borrow (con StubT Int)` → `"ptr"` and `Int`→`"i64"` / `Float`→`"double"` — the `eq__Unit` entry documents a past sig-mismatch from exactly this. Since the emit is never run this iteration, signature-correctness for `check_sig` is the bar, not load-offset correctness.)

- [ ] **Step 4: Update `kernel_stub_module_round_trips` to cover `peek`.**

Edit `crates/ailang-core/tests/design_schema_drift.rs` (`kernel_stub_module_round_trips`, ~744). After the existing `StubT.param_in` assertions, add an assertion that the round-tripped module contains a `peek` `Def::Fn` whose body is `Term::Intrinsic`:

```rust
    let peek = recovered.defs.iter().find_map(|d| match d {
        Def::Fn(f) if f.name == "peek" => Some(f),
        _ => None,
    }).expect("kernel_stub ships the peek ratifier op (raw-buf.3)");
    assert!(matches!(peek.body, Term::Intrinsic), "peek is an (intrinsic) marker");
```

(Confirm the test's in-scope names for `Def` / `Term` / the recovered-module binding against the existing body of the test before editing.)

- [ ] **Step 5: Add the mono-symbol integration test.**

Create `crates/ail/tests/mono_scoped_symbol.rs`. Mirror the workspace-build + `monomorphise_workspace` harness used by `crates/ail/tests/mono_hash_stability.rs` (read it for the exact API: how it constructs a `Workspace` from source modules incl. the auto-injected kernel_stub, and how it runs monomorphisation). A consumer module that calls `StubT.peek` at both `Int` and `Float` (each via a `borrow`-param fn, so no `StubT` value need be constructed — no `Term::New`):

```rust
//! raw-buf.3: a type-scoped polymorphic intrinsic call `StubT.peek`
//! monomorphises to the scope-qualified symbol `StubT_peek__<elem>`
//! (not the bare `peek__<elem>`). Proves the scope threads
//! resolution → FreeFnCall → MonoTarget → mint. No codegen / no
//! Term::New: assertion is on the monomorphised def names.

// <imports + harness mirrored from mono_hash_stability.rs>

const CONSUMER: &str = "(module peek_consumer\n  (fn at_int\n    (type (fn-type (params (borrow (con StubT (con Int)))) (ret (con Int))))\n    (params s)\n    (body (app StubT.peek s)))\n  (fn at_float\n    (type (fn-type (params (borrow (con StubT (con Float)))) (ret (con Float))))\n    (params s)\n    (body (app StubT.peek s))))";

#[test]
fn stubt_peek_monomorphises_to_scope_qualified_symbols() {
    // build a workspace from CONSUMER (kernel_stub auto-injected),
    // monomorphise, collect every synthesised mono FnDef name.
    let names: Vec<String> = /* mirror mono_hash_stability harness */ ;
    assert!(names.iter().any(|n| n == "StubT_peek__Int"),
        "expected scope-qualified StubT_peek__Int; got: {names:?}");
    assert!(names.iter().any(|n| n == "StubT_peek__Float"),
        "expected scope-qualified StubT_peek__Float; got: {names:?}");
    assert!(!names.iter().any(|n| n == "peek__Int"),
        "bare peek__Int must NOT be minted (scope-qualified only)");
}
```

(The `<imports + harness>` and the `names` collection are filled by mirroring `mono_hash_stability.rs` verbatim for the workspace-build + monomorphise calls — that file is the authoritative harness; do not invent a new monomorphisation entry point. The three asserts are the load-bearing content.)

- [ ] **Step 6: Run the new + affected tests.**

Run: `cargo test -p ail --test mono_scoped_symbol stubt_peek_monomorphises_to_scope_qualified_symbols`
Expected: PASS — both scope-qualified symbols present, bare `peek__Int` absent.

Run: `cargo test -p ailang-codegen intercepts_bijection_with_intrinsic_markers`
Expected: PASS — the `peek` marker expands to exactly `StubT_peek__Int` + `StubT_peek__Float`, both matched by the new `INTERCEPTS` entries (1-marker→2-entries).

Run: `cargo test -p ailang-core --test design_schema_drift kernel_stub_module_round_trips`
Expected: PASS — the stub (now with `peek`) round-trips and `peek` is an `(intrinsic)`.

- [ ] **Step 7: Final full suite.**

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` — baseline 669 + the one new `mono_scoped_symbol` test. The `.ll` snapshot tests stay green: `peek` is polymorphic, so codegen never instantiates a `StubT_peek__*` define without a call site, and this iteration adds no codegen consumer (the mono test stops at monomorphisation).

---

## Self-review

1. **Spec coverage.**
   - § Architecture pt 3 "scope-qualified mono symbols": Task 1.
   - § Architecture pt 3 "bijection expansion over param-in": Task 2 Step 2.
   - § Concrete "StubT.peek ratifier": Task 2 Step 1 + Step 3.
   - § Concrete "scope-qualified mono symbol" (mono.rs:521/the mint sites): Task 1 Steps 4-7.
   - § Data-flow "Mechanism": Task 1 (mint) + Task 2 Step 2 (collector).
   - § Testing raw-buf.3 bullets (mono unit test, bijection green, round-trip updated, no count change beyond +1): Task 2 Steps 4-7.

2. **Placeholder scan.** No "TBD/TODO/implement later/similar to/add appropriate". The `<imports + harness>` and `names` collection in Task 2 Step 5 are an explicit "mirror file X verbatim" instruction naming the authoritative source (`mono_hash_stability.rs`), with the load-bearing asserts given in full — not a placeholder.

3. **Type consistency.** `scope: Option<String>` consistent across `FreeFnCall` / `MonoTarget::FreeFn` / `mono_target_key` / construction / mint. `scoped_base` used at both mint sites + the key. Symbol strings `StubT_peek__Int` / `StubT_peek__Float` identical across source-derived (collector), mono-minted (Task 1), and the entry names (Task 2 Step 3) — all flow through `mono_symbol_n` with a `Type::Con` element, guaranteeing byte-match.

4. **Step granularity.** Each step is one struct edit, one threaded field, one helper, one fn, or one command. Task 1 Step 2's "every other `free_fn_owner` site" is compiler-enumerated (a tuple-shape change), bounded and mechanical.

5. **No commit steps.** None.

6. **Pin/replacement substring contiguity.** The asserted substrings (`StubT_peek__Int`, `StubT_peek__Float`, `peek__Int`-absent) are produced at runtime by `mono_symbol_n`, not by a verbatim file body the plan also ships — no soft-wrap split risk. The `peek` source.ail body is byte-checked by `kernel_stub_module_round_trips` (structural), not by a substring pin.

7. **Compile-gate vs. deferred-caller ordering.** Task 1 changes three shapes (`FreeFnCall` field, `MonoTarget::FreeFn` field, `free_fn_owner` tuple) that break compilation until all sites are threaded; ALL are inside Task 1, which ends with `cargo build --workspace` (Step 8) + full test (Step 9). No caller deferred past the gate. Task 2 adds entries/fns/tests with no cross-task signature break.

8. **Verification-command filter strings.** Task 2 Steps 6 filters name tests this plan creates (`stubt_peek_monomorphises_to_scope_qualified_symbols`, in the file created Step 5) or that exist per recon (`intercepts_bijection_with_intrinsic_markers`, `kernel_stub_module_round_trips`). Task 1 Step 9 + Task 2 Step 7 use the unfiltered suite with an explicit count assertion (awk), so "0 ran" cannot pass as success.

9. **Parse-the-bytes gate.** The one surface-language body inlined is the `peek` op (Task 2 Step 1), fenced `text` — `kernel_stub` is a reserved built-in module name (`ail check` standalone → `reserved-module-name`), so the configured `ail` parser cannot validate it in isolation; the gate is N/A for it. Its Form-A was nonetheless verified this session under a non-reserved probe module (`ok (32 symbols across 3 modules)`), and it is structurally ratified by `kernel_stub_module_round_trips` (Task 2 Step 4) + the `implement` compile gate. All other inlined bodies are Rust — caught by the compile gate, not the parse gate.
