Files
AILang/docs/plans/0109-raw-buf.3-typescoped-intrinsic-mechanism.md
T
Brummel 8508182f84 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).
2026-05-29 19:25:04 +02:00

26 KiB

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:

    /// 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:

                  (qualified, Some((target_module, suffix.to_string())))
    

    becomes:

                  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 == Nonescope = 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:

            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:

                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:

        /// 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):

/// 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:

        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:

          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:

                          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):

  (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:

                    // 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):

/// 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):

    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:

/// 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:

    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):

//! 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 textkernel_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.