Files
Aura/docs/plans/0047-cell-hot-path-carrier.md
T
Brummel 1a33b4f252 plan: 0047 cell hot-path carrier
Four tasks for the behaviour-preserving carrier swap: (1) add the
branch-free AnyColumn::push_cell + round-trip test (additive); (2) the
atomic lib-code flip (Node::eval -> Option<&[Cell]>, 8 aura-std nodes,
recorder, harness forward loop) gated by a lib-only build; (3) the
cfg(test) migration (fixtures + eval-output assertions per the spec's
three-role rule) gated by the full four-gate run; (4) the C7/C8 ledger
realization note.

refs #74
2026-06-16 12:58:21 +02:00

22 KiB

Cell as the hot-path value carrier — Implementation Plan

Parent spec: docs/specs/0047-cell-hot-path-carrier.md

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

Goal: Narrow the hot-path value carrier from Scalar to the tag-free Cell (Node::eval -> Option<&[Cell]>, every node out-buffer, the inter-node forward), keeping Scalar on the self-describing dynamic boundaries (params, get, source ingestion). Behaviour-preserving (C1).

Architecture: The Node::eval trait return type is the single atomic compile cut: flipping it breaks all 21 impl Node bodies at once, so the lib-code flip (trait + 8 aura-std nodes + recorder + harness forward loop) lands as one task gated by a lib-only build; the #[cfg(test)] fixtures and the eval-output assertions (which only compile under --all-targets) migrate in a second task gated by the full four-gate run. A new branch-free AnyColumn::push_cell (added first, additively) is the infallible inter-node push; the fallible push(Scalar) and get -> Option<Scalar> stay for ingestion / type-erased reads.

Tech Stack: aura-core (node.rs, any.rs), aura-std (8 nodes), aura-engine (harness.rs forward loop + fixtures, blueprint.rs/graph_model.rs fixtures), the design ledger.


Files this plan creates or modifies

  • Modify: crates/aura-core/src/node.rs:13,264,286,410use adds Cell; Node::eval trait sig → Option<&[Cell]>; Bare/Probe eval sigs.
  • Modify: crates/aura-core/src/any.rs:7,62-92 — add use crate::cell::Cell;; add push_cell(Cell); add a push_cell round-trip test in the tests mod.
  • Modify: crates/aura-std/src/add.rs:5,20,26,58,64,92
  • Modify: crates/aura-std/src/sub.rs:6,11,17,49,55,83
  • Modify: crates/aura-std/src/sma.rs:6-9,14,21,45,54,86,97,100
  • Modify: crates/aura-std/src/ema.rs:20-23,37,52,79,99,131,144,147
  • Modify: crates/aura-std/src/exposure.rs:6-9,15,22,46,51,81
  • Modify: crates/aura-std/src/lincomb.rs:9-12,30,40,68,77,105,118,136,168,183
  • Modify: crates/aura-std/src/sim_broker.rs:7,41,54,82,94,124
  • Modify: crates/aura-std/src/recorder.rs:10,60use adds Cell (the return type names it); eval sig → Option<&[Cell]> (body still returns None).
  • Modify: crates/aura-engine/src/harness.rs:10,24,373,416,426-428 (forward path)
    • fixtures AsOfSum/BarrierSum/MixedSum/Ohlcv/TwoField/TapForward (out-buffers, eval sigs, assignments) + their ctor literals (:898,944,993,1044,1156,1209,1255,1380,1307,1614,1475,1781).
  • Modify: crates/aura-engine/src/blueprint.rs:816 (test use adds Cell) + fixtures Join2/Pass1/SinkF64/SinkI64 (:1057,1063,1069,1076,1082,1087,1098,1110)
    • ctor literals (:1119,1127,2283).
  • Modify: crates/aura-engine/src/graph_model.rs:274-276,286 (test use adds Cell; Bare eval sig).
  • Modify: docs/design/INDEX.md (C7 realization note :195-210; C8 prose :221,242).
  • KEEP (no change — param plane / type-erased read / ingestion / channel rows): node.rs param path, any.rs::push/get, sweep.rs/mc.rs/walkforward.rs param args + scalar_as_f64, aura-cli/src/main.rs run_oos/scalar_as_param_f64, report.rs::f64_field, recorder.rs channel row, graph_model.rs::scalar_str, blueprint.rs::compile_with_params, test_fixtures.rs, builder.rs, aura-ingest, aura-registry, ctx.rs, column.rs, error.rs, scalar.rs, cell.rs.

Task 1: Add AnyColumn::push_cell + round-trip test (additive)

Files:

  • Modify: crates/aura-core/src/any.rs

This task is additive: push_cell is new pub code, the test exercises it, and nothing existing changes — the workspace stays green throughout.

  • Step 1: Import Cell in any.rs

In crates/aura-core/src/any.rs, after the existing import line use crate::error::KindMismatch; (line ~6), and the line use crate::scalar::{Scalar, ScalarKind, Timestamp}; (line 7), add:

use crate::cell::Cell;
  • Step 2: Add the push_cell method

In impl AnyColumn, immediately after the existing push method (ends ~line 82), add:

    /// Branch-free, infallible hot-path push of a bare [`Cell`] into this column's
    /// concrete `Column<T>`, reinterpreting the cell by *this* column's kind. The
    /// inter-node forward uses this: the edge's `from_field`→slot kind match is
    /// verified once at bootstrap (C7), so no per-value kind check is needed here
    /// — unlike [`push`](Self::push), which keeps the kind guard for the
    /// ingestion boundary.
    pub fn push_cell(&mut self, c: Cell) {
        match self {
            AnyColumn::I64(col) => col.push(c.i64()),
            AnyColumn::F64(col) => col.push(c.f64()),
            AnyColumn::Bool(col) => col.push(c.bool()),
            AnyColumn::Ts(col) => col.push(c.ts()),
        }
    }
  • Step 3: Add the round-trip test

In the #[cfg(test)] mod tests block (after same_kind_push_round_trips, ~line 171), add:

    #[test]
    fn push_cell_round_trips_by_column_kind() {
        // push_cell reinterprets the bare cell by the column's own kind; reading
        // back through `get` reconstructs the self-describing Scalar.
        let mut f = AnyColumn::with_capacity(ScalarKind::F64, 2);
        f.push_cell(Cell::from_f64(1.5));
        assert_eq!(f.get(0), Some(Scalar::f64(1.5)));
        assert_eq!(f.run_count(), 1);

        let mut i = AnyColumn::with_capacity(ScalarKind::I64, 2);
        i.push_cell(Cell::from_i64(42));
        assert_eq!(i.get(0), Some(Scalar::i64(42)));

        let mut b = AnyColumn::with_capacity(ScalarKind::Bool, 2);
        b.push_cell(Cell::from_bool(true));
        assert_eq!(b.get(0), Some(Scalar::bool(true)));

        let mut t = AnyColumn::with_capacity(ScalarKind::Timestamp, 2);
        t.push_cell(Cell::from_ts(Timestamp(7)));
        assert_eq!(t.get(0), Some(Scalar::ts(Timestamp(7))));
    }
  • Step 4: Verify it compiles and passes

Run: cargo test -p aura-core push_cell_round_trips_by_column_kind Expected: PASS (1 passed). The rest of the workspace is untouched and still green.


Task 2: Atomic lib-code carrier flip

Files:

  • Modify: crates/aura-core/src/node.rs
  • Modify: crates/aura-std/src/{add,sub,sma,ema,exposure,lincomb,sim_broker,recorder}.rs
  • Modify: crates/aura-engine/src/harness.rs

The trait-sig flip breaks every impl Node simultaneously; all lib-code impls + the harness forward loop must change together. Fixtures (#[cfg(test)]) and eval-output assertions are deferred to Task 3, so the gate here is a lib-only build (cargo build --workspace, NOT --all-targets) — the cfg(test) code does not compile until Task 3.

  • Step 1: Flip the Node::eval trait signature (node.rs)

In crates/aura-core/src/node.rs:

Line 13 — add Cell to the import:

use crate::{Cell, Ctx, Scalar, ScalarKind};

Line 264 — the trait method:

    fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]>;

Update the trait/struct doc prose that names the old carrier: in the Node doc-comment (~lines 248-255) and the C8 reference, replace the phrase returning a borrowed slice into a buffer the node owns context — specifically any Option<&[Scalar]> mention in rustdoc becomes Option<&[Cell]>. (Search the file for &[Scalar] in doc comments; the param-path doc mentions of Scalar stay.)

  • Step 2: Migrate add.rs

crates/aura-std/src/add.rs:

Line 5 — use aura_core::{Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, Scalar, ScalarKind}; Line 20 — out: [Cell; 1], Line 26 — Self { out: [Cell::from_f64(0.0)] } Line 58 — fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> { Line 64 — self.out[0] = Cell::from_f64(a[0] + b[0]);

(Scalar stays in the use — the test column writes at lines 87, 91 keep it.)

  • Step 3: Migrate sub.rs

crates/aura-std/src/sub.rs:

Line 6 — add Cell, to the import (→ use aura_core::{Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, Scalar, ScalarKind};) Line 11 — out: [Cell; 1], Line 17 — Self { out: [Cell::from_f64(0.0)] } Line 49 — fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> { Line 55 — self.out[0] = Cell::from_f64(a[0] - b[0]);

  • Step 4: Migrate sma.rs

crates/aura-std/src/sma.rs:

Lines 6-9 — add Cell, to the multi-line import:

use aura_core::{
    Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder, Scalar,
    ScalarKind,
};

Line 14 — out: [Cell; 1], Line 21 — Self { length, out: [Cell::from_f64(0.0)] } Line 45 — fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> { Line 54 — self.out[0] = Cell::from_f64(sum / self.length as f64);

(Scalar stays — column writes + bind(..., Scalar::i64(2)) keep it.)

  • Step 5: Migrate ema.rs

crates/aura-std/src/ema.rs:

Lines 20-23 — add Cell, to the multi-line import:

use aura_core::{
    Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder, Scalar,
    ScalarKind,
};

Line 37 — out: [Cell; 1], Line 52 — out: [Cell::from_f64(0.0)], Line 79 — fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> { Line 99 — self.out[0] = Cell::from_f64(self.ema);

  • Step 6: Migrate exposure.rs

crates/aura-std/src/exposure.rs:

Lines 6-9 — add Cell, to the multi-line import:

use aura_core::{
    Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder, Scalar,
    ScalarKind,
};

Line 15 — out: [Cell; 1], Line 22 — Self { scale, out: [Cell::from_f64(0.0)] } Line 46 — fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> { Line 51 — self.out[0] = Cell::from_f64((w[0] / self.scale).clamp(-1.0, 1.0));

  • Step 7: Migrate lincomb.rs

crates/aura-std/src/lincomb.rs:

Lines 9-12 — add Cell, to the multi-line import:

use aura_core::{
    Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder, Scalar,
    ScalarKind,
};

Line 30 — out: [Cell; 1], Line 40 — Self { weights, out: [Cell::from_f64(0.0)] } Line 68 — fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> { Line 77 — self.out[0] = Cell::from_f64(acc);

  • Step 8: Migrate sim_broker.rs (eval body; test helper is Task 3)

crates/aura-std/src/sim_broker.rs:

Line 7 — add Cell, to the import. Line 41 — out: [Cell; 1], Line 54 — out: [Cell::from_f64(0.0)], Line 82 — fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> { Line 94 — self.out[0] = Cell::from_f64(self.cum);

(The test helper at line 124 migrates in Task 3.)

  • Step 9: Migrate recorder.rs (eval sig only)

crates/aura-std/src/recorder.rs:

Line 10 — add Cell to the import (the return type Option<&[Cell]> names it, even though the body returns None):

use aura_core::{Cell, Ctx, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, Scalar, ScalarKind, Timestamp};

Line 60 — fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {

The body is unchanged: it builds the Vec<Scalar> channel row from get(0)? and returns None (pure sink). Scalar stays in the use.

  • Step 10: Migrate the harness forward path (harness.rs)

crates/aura-engine/src/harness.rs:

Line 24 — add Cell to the import:

use aura_core::{AnyColumn, Cell, Ctx, Firing, Node, NodeSchema, Scalar, ScalarKind, Timestamp};

Line 10 — the module-doc prose borrowed record (Option<&[Scalar]>)borrowed record (Option<&[Cell]>). Line 373 — let mut scratch: Vec<Cell> = Vec::new(); Line 416 — let result: Option<&[Cell]> = { Lines 426-428 — replace the fallible inter-node push with the infallible cell push:

                        nb.inputs[e.slot].push_cell(scratch[e.from_field]);
                        nb.slots[e.slot] = SlotState { fresh_at: cycle_id, last_ts: ts };

(i.e. drop the .push(...).expect("edge kind checked at wiring") and call push_cell. The nb.slots[...] = ... line that follows is unchanged.)

Do NOT change line 402 — the source-target forward nb.inputs[t.slot].push(value).expect("source kind checked at wiring") stays on the fallible push(Scalar) (ingestion, keep-set).

  • Step 11: Lib-only build gate

Run: cargo build --workspace Expected: Finished with 0 errors. (This builds lib + bin targets only; the #[cfg(test)] fixtures and eval-output assertions do NOT compile yet — that is Task 3. Do not run --all-targets here; it will fail by design until Task 3.)


Task 3: Migrate #[cfg(test)] fixtures + eval-output assertions

Files:

  • Modify: crates/aura-core/src/node.rs (Bare, Probe)
  • Modify: crates/aura-engine/src/harness.rs (6 fixtures + ctor literals)
  • Modify: crates/aura-engine/src/blueprint.rs (4 fixtures + ctor literals + use)
  • Modify: crates/aura-engine/src/graph_model.rs (Bare + use)
  • Modify: crates/aura-std/src/{add,sub,sma,ema,exposure,lincomb,sim_broker}.rs (assertions)

Per the spec's three-role test-migration rule: eval-output sites → Cell; column writes (inputs[i].push(Scalar::f64(x))) → stay Scalar; out-of-graph channel sends (vec![Scalar::f64(v)]) → stay Scalar.

  • Step 1: node.rs fixtures Bare + Probe

crates/aura-core/src/node.rs:

Line 288 (Bare::eval) — fn eval(&mut self, _ctx: Ctx<'_>) -> Option<&[Cell]> { Line 410 (Probe::eval) — fn eval(&mut self, _ctx: Ctx<'_>) -> Option<&[Cell]> {

Both bodies return None — unchanged. Probe's field Probe(Vec<Scalar>) (line 405) stays Vec<Scalar> (a stored param point). All Scalar::i64/f64 in the tests / zip_params_tests modules are param/bind sites — stay Scalar.

  • Step 2: harness.rs fixtures (6) + ctor literals

crates/aura-engine/src/harness.rs — for each fixture, change the out field type, the eval signature, and the Scalar::* assignment(s) to the Cell carrier:

  • AsOfSum: line 621 out: [Cell; 1],; line 636 eval sig; line 642 self.out[0] = Cell::from_f64(a[0] + b[0]);
  • BarrierSum: line 650 out: [Cell; 1],; line 665 eval sig; line 666 self.out[0] = Cell::from_f64(ctx.f64_in(0)[0] + ctx.f64_in(1)[0]);
  • MixedSum: line 675 out: [Cell; 1],; line 694 eval sig; line 701 self.out[0] = Cell::from_f64(a[0] + b[0] + c[0]);
  • Ohlcv: line 711 out: [Cell; 5],; line 732 eval sig; line 738 self.out[i] = Cell::from_f64(w[0]);
  • TwoField: line 749 out: [Cell; 2],; line 767 eval sig; line 768 self.out[0] = Cell::from_f64(0.0);; line 769 self.out[1] = Cell::from_i64(0);
  • TapForward (dual-role): line 778 out: [Cell; 1],; line 794 eval sig; line 801 self.out[0] = Cell::from_f64(v);. Line 779 keeps tx: mpsc::Sender<(Timestamp, Vec<Scalar>)> and line 800 keeps let _ = self.tx.send((ctx.now(), vec![Scalar::f64(v)])); (out-of-graph row).

Then the fixture constructor literals — change each out: init to the Cell carrier (the type must match the migrated field):

  • Lines 898, 944, 993, 1044, 1781 (AsOfSum/BarrierSum/MixedSum/BarrierSum): out: [Cell::from_f64(0.0)]
  • Lines 1156, 1209, 1255, 1380 (Ohlcv { out: [Scalar::f64(0.0); 5] }): out: [Cell::from_f64(0.0); 5]
  • Lines 1307, 1614 (TwoField { out: [Scalar::f64(0.0), Scalar::i64(0)] }): out: [Cell::from_f64(0.0), Cell::from_i64(0)]
  • Line 1475 (TapForward { out: [Scalar::f64(0.0)], tx: ... }): out: [Cell::from_f64(0.0)] (the tx: field unchanged)

Keep Scalar on every recorded-row / channel-row literal (recorder + Tap channel sends, Vec<Scalar> assertions) at lines 830-832, 877-879, 924-927, 970-971, 1025-1027, 1070-1071, 1235-1236, 1278-1279, 1358-1361, 1367-1368, 1490-1492, 1562-1563, 1598-1601, 1682-1684, 1746-1756, 1813-1821, 1873-1874, 1935-1947, 2022-2036, 2108 — these are out-of-graph rows, not eval output.

  • Step 3: blueprint.rs fixtures (4) + ctor literals + use

crates/aura-engine/src/blueprint.rs:

Line 816 — add Cell to the test-mod import:

    use aura_core::{Cell, Ctx, FieldSpec, Firing, NodeSchema, Timestamp};
  • Join2: line 1057 out: [Cell; 1],; line 1063 eval sig; line 1069 self.out[0] = Cell::from_f64(a[0] + b[0]);
  • Pass1: line 1076 out: [Cell; 1],; line 1082 eval sig; line 1087 self.out[0] = Cell::from_f64(w[0]);
  • SinkF64: line 1098 eval sig → Option<&[Cell]> (body returns None).
  • SinkI64: line 1110 eval sig → Option<&[Cell]> (body returns None).

Constructor literals:

  • Line 1119 (Pass1 { out: [Scalar::f64(0.0)] }) → out: [Cell::from_f64(0.0)]
  • Line 1127 (Join2 { out: [Scalar::f64(0.0)] }) → out: [Cell::from_f64(0.0)]
  • Line 2283 (Pass1 { out: [Scalar::f64(0.0)] }) → out: [Cell::from_f64(0.0)]

Keep Scalar on compile_with_params(&[Scalar::...]) calls (param plane): lines 1239, 1899, 2058, 2193, 2212, 2265, 2315.

  • Step 4: graph_model.rs fixture + use

crates/aura-engine/src/graph_model.rs:

Lines 274-276 — add Cell to the test-mod import:

    use aura_core::{
        Cell, FieldSpec, Node, NodeSchema, PortSpec, PrimitiveBuilder, Scalar,
    };

Line 286 — Bare::eval sig → fn eval(&mut self, _c: aura_core::Ctx<'_>) -> Option<&[Cell]> { (body returns None). scalar_str(s: aura_core::Scalar) at line 27 is untouched (renders a BoundParam.value). If clippy later flags Scalar as unused in this test module, remove it from the line-274 import — the clippy gate (Step 8) is the backstop.

  • Step 5: aura-std eval-output assertions + sim_broker helper

Migrate ONLY the eval-output assertion lines (Some([Scalar::f64(x)].as_slice())Some([Cell::from_f64(x)].as_slice())); leave every inputs[i].push(Scalar::f64(x)) column-write untouched.

  • add.rs:92Some([Cell::from_f64(14.0)].as_slice())

  • sub.rs:83Some([Cell::from_f64(6.0)].as_slice())

  • sma.rs:86,97,100 — wrap each expected mean with Cell::from_f64(...) (line 86 is inside the Some(m) => assert_eq!(got, Some([Cell::from_f64(m)].as_slice())) match arm; lines 97, 100 are Some([Cell::from_f64(7.0)]...) / Some([Cell::from_f64(9.0)]...)).

  • ema.rs:131,144,147 — same: line 131 match arm Some([Cell::from_f64(m)]...); lines 144, 147 Some([Cell::from_f64(7.0)]...) / Some([Cell::from_f64(9.0)]...).

  • exposure.rs:81Some([Cell::from_f64(want)].as_slice())

  • lincomb.rs:105,118,136,168,183 — each Some([Cell::from_f64(x)].as_slice()) (the expected weighted sums: 11.0, 12.0, 6.0, 11.0, 11.0 respectively).

  • sim_broker.rs:124 — the test helper match arm: Some([c]) => c.f64(), (was Some([s]) => s.as_f64(),). The panic! arm and the column-write seeds (lines 120, 122) are unchanged.

  • Step 6: Full-target build gate

Run: cargo build --workspace --all-targets Expected: Finished with 0 errors. (Now the test code compiles against the Cell carrier.)

  • Step 7: Test gate

Run: cargo test --workspace Expected: all tests pass, 0 failed. (Behaviour-preserving: the only changed test expectations are eval-output re-spellings; values are identical.)

  • Step 8: Clippy gate

Run: cargo clippy --workspace --all-targets -- -D warnings Expected: 0 warnings. (Catches any now-unused Scalar import left in a fully migrated file.)

  • Step 9: Doc gate

Run: cargo doc --workspace --no-deps 2>&1 Expected: builds with no warnings (the migrated rustdoc comments + intra-doc links to Cell resolve).


Task 4: Record the carrier swap in the design ledger

Files:

  • Modify: docs/design/INDEX.md

The C7 realization note promised "its own note (and touch C8's eval contract)"; this task delivers it so the ledger no longer asserts a now-false "not yet".

  • Step 1: Update the C7 realization note

In docs/design/INDEX.md, the C7 Realization (Cell carrier split, 2026-06) note (lines ~195-210) currently ends:

the `scalar_eq_is_value_not_bitwise` fixture. Behaviour-preserving. `Cell` is
**not yet** the hot-path carrier — `eval -> Option<&[Scalar]>` and the edges
still flow `Scalar`; replacing the carrier with `Cell` is a larger step that will
get its own note (and touch C8's `eval` contract).

Replace the "not yet" sentence with the realization (keep the value-PartialEq sentence before it):

the `scalar_eq_is_value_not_bitwise` fixture. Behaviour-preserving.

**Realization (`Cell` becomes the hot-path carrier, 2026-06, #74).** The carrier
swap deferred above has landed: `Node::eval` now returns `Option<&[Cell]>` and
every node out-buffer is `[Cell; N]`, so the inter-node forward carries tag-free
8-byte words. The kind lives only at the schema/column: the harness forwards each
field via the new branch-free `AnyColumn::push_cell` (infallible — the edge
kind match is verified once at bootstrap, the surviving guard
`bootstrap_rejects_*_kind_mismatch`). `Scalar` remains on the self-describing
dynamic boundaries: the param plane (`build`/`bind`/`compile_with_params`/sweep
points/`RunManifest`), `AnyColumn::get` (the type-erased read for sinks/serde),
and source ingestion (`Source::next`, the heterogeneous C3 merge). The removed
per-value runtime kind check on node output is the same authoring-bug class C8
already leaves to a `debug_assert` (output width); node-output-kind correctness
is the node's declared `FieldSpec` contract, caught by each node's own test.
Behaviour-preserving (C1).
  • Step 2: Update the C8 prose mentions of the old carrier

In docs/design/INDEX.md, the C8 guarantee (line ~221) and the C8 cycle-0005 realization (line ~242) state eval(ctx) -> Option<&[Scalar]> / eval returns Option<&[Scalar]>. Update both Option<&[Scalar]>Option<&[Cell]>, and in the 0005 realization append a brief pointer: append to that sentence (the carrier is now a tag-freeCell — see the C7 carrier note).

  • Step 3: Glossary check (no write expected)

Run: grep -n "not yet" docs/glossary.md; grep -n -A2 "^### cell" docs/glossary.md Expected: the cell entry already says cell "is what the SoA hot path reads without a per-value branch" — forward-compatible, no "not yet" claim. If the grep shows a "not yet"-style claim about cell being the carrier, fix it; otherwise no glossary change.

  • Step 4: Verify no stale "not yet the hot-path carrier" remains

Run: grep -rn "not yet.*hot-path carrier\|not yet.*carrier" docs/ Expected: no matches (the C7 note's "not yet" line is gone).

Run: cargo doc --workspace --no-deps 2>&1 Expected: no new warnings (sanity re-check after the ledger edit; the ledger is markdown, but this confirms nothing in code regressed).