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
22 KiB
Cell as the hot-path value carrier — Implementation Plan
Parent spec:
docs/specs/0047-cell-hot-path-carrier.mdFor agentic workers: REQUIRED SUB-SKILL: use the
implementskill 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,410—useaddsCell;Node::evaltrait sig →Option<&[Cell]>;Bare/Probeeval sigs. - Modify:
crates/aura-core/src/any.rs:7,62-92— adduse crate::cell::Cell;; addpush_cell(Cell); add apush_cellround-trip test in thetestsmod. - 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,60—useaddsCell(the return type names it); eval sig →Option<&[Cell]>(body still returnsNone). - 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).
- fixtures
- Modify:
crates/aura-engine/src/blueprint.rs:816(testuseaddsCell) + fixturesJoin2/Pass1/SinkF64/SinkI64(:1057,1063,1069,1076,1082,1087,1098,1110)- ctor literals (
:1119,1127,2283).
- ctor literals (
- Modify:
crates/aura-engine/src/graph_model.rs:274-276,286(testuseaddsCell;Bareeval 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.rsparam path,any.rs::push/get,sweep.rs/mc.rs/walkforward.rsparam args +scalar_as_f64,aura-cli/src/main.rsrun_oos/scalar_as_param_f64,report.rs::f64_field,recorder.rschannel 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
Cellin 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_cellmethod
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::evaltrait 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 621out: [Cell; 1],; line 636 eval sig; line 642self.out[0] = Cell::from_f64(a[0] + b[0]);BarrierSum: line 650out: [Cell; 1],; line 665 eval sig; line 666self.out[0] = Cell::from_f64(ctx.f64_in(0)[0] + ctx.f64_in(1)[0]);MixedSum: line 675out: [Cell; 1],; line 694 eval sig; line 701self.out[0] = Cell::from_f64(a[0] + b[0] + c[0]);Ohlcv: line 711out: [Cell; 5],; line 732 eval sig; line 738self.out[i] = Cell::from_f64(w[0]);TwoField: line 749out: [Cell; 2],; line 767 eval sig; line 768self.out[0] = Cell::from_f64(0.0);; line 769self.out[1] = Cell::from_i64(0);TapForward(dual-role): line 778out: [Cell; 1],; line 794 eval sig; line 801self.out[0] = Cell::from_f64(v);. Line 779 keepstx: mpsc::Sender<(Timestamp, Vec<Scalar>)>and line 800 keepslet _ = 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)](thetx: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 1057out: [Cell; 1],; line 1063 eval sig; line 1069self.out[0] = Cell::from_f64(a[0] + b[0]);Pass1: line 1076out: [Cell; 1],; line 1082 eval sig; line 1087self.out[0] = Cell::from_f64(w[0]);SinkF64: line 1098 eval sig →Option<&[Cell]>(body returnsNone).SinkI64: line 1110 eval sig →Option<&[Cell]>(body returnsNone).
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:92—Some([Cell::from_f64(14.0)].as_slice()) -
sub.rs:83—Some([Cell::from_f64(6.0)].as_slice()) -
sma.rs:86,97,100— wrap each expected mean withCell::from_f64(...)(line 86 is inside theSome(m) => assert_eq!(got, Some([Cell::from_f64(m)].as_slice()))match arm; lines 97, 100 areSome([Cell::from_f64(7.0)]...)/Some([Cell::from_f64(9.0)]...)). -
ema.rs:131,144,147— same: line 131 match armSome([Cell::from_f64(m)]...); lines 144, 147Some([Cell::from_f64(7.0)]...)/Some([Cell::from_f64(9.0)]...). -
exposure.rs:81—Some([Cell::from_f64(want)].as_slice()) -
lincomb.rs:105,118,136,168,183— eachSome([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(),(wasSome([s]) => s.as_f64(),). Thepanic!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).