diff --git a/docs/plans/0047-cell-hot-path-carrier.md b/docs/plans/0047-cell-hot-path-carrier.md new file mode 100644 index 0000000..d46837b --- /dev/null +++ b/docs/plans/0047-cell-hot-path-carrier.md @@ -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` 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`, 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` 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 = 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)` +(line 405) stays `Vec` (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)>` 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` 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).