plan: raw-buf.3 type-scoped polymorphic intrinsic mechanism (refs #7)

Two tasks. Task 1 threads a TypeDef scope through the free-fn mono path:
a scope: Option<String> field on FreeFnCall + MonoTarget::FreeFn,
populated Some(prefix) only on the type-scoped T.f resolution branch
(lib.rs:3535, gated on type_home.is_some()), None everywhere else;
consumed at the two mono-symbol mint sites via a scoped_base helper
(both read the same MonoTarget → lockstep) and folded into
mono_target_key. Result: StubT.peek @ Int mints StubT_peek__Int, not
the colliding bare peek__Int; existing bare-resolved symbols unchanged
(669-gate is the no-regression backstop).

Task 2 adds the StubT.peek ratifier op to the stub source, extends the
bijection collector to expand a polymorphic type-scoped intrinsic over
its TypeDef's param-in set (building the same T_f__<elem> strings via
mono_symbol_n on Type::Con elems, so collector and mint agree
byte-for-byte), registers StubT_peek__Int/Float entries + emit fns,
updates kernel_stub_module_round_trips, and adds a mono-symbol
integration test (borrow-param calls, no Term::New, no codegen).

plan-recon resolved the make-or-break unknown favourably: the TypeDef
scope is discarded at lib.rs:3535 but fully available there (prefix +
type_home live) — a clean thread-through, not a resolution
re-architecture. Three orchestrator design calls folded in: peek's emit
is a plausible signature-correct stub, unit-ratified only (never
instantiated in .3, no Term::New to build a StubT; RawBuf's emits in .4
are the E2E-verified ones; peek retires in .5); the exact
llvm_type(borrow StubT Int) string is an implementer verify-first step
(eq__Unit precedent); scope-trigger is the call path (not signature,
which would perturb bare-called fns like length), scope-value is the
prefix, collector infers from signature — they agree by construction
for single-TypeDef kernel modules, bijection + 669-gate backstop.

peek Form-A verified to parse+check this session under a probe module
(ok, 32 symbols / 3 modules). Final gate 670 (669 + 1 new mono test);
.ll snapshots stay green (peek polymorphic → no instantiation without a
call site).
This commit is contained in:
2026-05-29 19:25:04 +02:00
parent d2885c7ae6
commit 8508182f84
@@ -0,0 +1,432 @@
# 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.