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
This commit is contained in:
2026-06-16 12:58:21 +02:00
parent 73e1558383
commit 1a33b4f252
+532
View File
@@ -0,0 +1,532 @@
# 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,410``use` 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,60``use` 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:
```rust
use crate::cell::Cell;
```
- [ ] **Step 2: Add the `push_cell` method**
In `impl AnyColumn`, immediately after the existing `push` method (ends ~line 82),
add:
```rust
/// 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:
```rust
#[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:
```rust
use crate::{Cell, Ctx, Scalar, ScalarKind};
```
Line 264 — the trait method:
```rust
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:
```rust
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:
```rust
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:
```rust
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:
```rust
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`):
```rust
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:
```rust
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:
```rust
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:
```rust
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:
```rust
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 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:81``Some([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-free `Cell` — 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).