5288249b44
Four tasks for cycle 0027 (spec docs/specs/0027-name-input-ports.md): 1. Add name: String to PortSpec (drop Copy) and thread all 16 construction sites in one compile-gate task — aura-std nodes get real slot names, fixtures get fixture names, derive_signature carries Role.name; port_json untouched so the byte golden stays green and the full suite passes. 2. port_json appends the name; re-capture the model byte golden and update its two substring twins (sink_has_empty_outs witnesses a leaf PortSpec.name in the model, multi_input_composite witnesses a composite Role.name carried through). 3. Lock the per-node input-slot names (8 aura-std pins). 4. C8 realization note in the design ledger (cross-links C23). refs #50
371 lines
16 KiB
Markdown
371 lines
16 KiB
Markdown
# Name Input Ports — Implementation Plan
|
|
|
|
> **Parent spec:** `docs/specs/0027-name-input-ports.md`
|
|
>
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run
|
|
> this plan. Steps use `- [ ]` checkboxes for tracking.
|
|
|
|
**Goal:** Give `PortSpec` a non-load-bearing `name: String`, name every input
|
|
slot across the aura-std nodes and fixtures, carry `Role.name` into the derived
|
|
composite signature, and make `model_to_json` emit the name — so the graph model
|
|
labels input pins faithfully (C23-clean; identity stays positional).
|
|
|
|
**Architecture:** One contract change in `aura-core` ripples to every `PortSpec`
|
|
construction site (compiler-enforced). Task 1 adds the field and threads all 16
|
|
sites in one shot (the compile gate), leaving `port_json` untouched so the byte
|
|
golden stays green. Task 2 flips `port_json` to emit the name and re-captures the
|
|
golden + its two substring twins. Task 3 locks the per-node names. Task 4 records
|
|
the realization in the ledger.
|
|
|
|
**Tech Stack:** Rust — `crates/aura-core` (the `PortSpec` contract),
|
|
`crates/aura-std` (8 nodes), `crates/aura-engine` (`derive_signature`,
|
|
`graph_model`, fixtures), `crates/aura-cli` + `crates/aura-ingest` (fixtures),
|
|
`docs/design/INDEX.md` (ledger). Hand-rolled JSON, no serde (C14).
|
|
|
|
**Files this plan creates or modifies:**
|
|
|
|
- Modify: `crates/aura-core/src/node.rs:29-37` — `PortSpec` gains `name`, drops `Copy`, doc updated; test at `:172-179`.
|
|
- Modify: `crates/aura-std/src/sma.rs:31`, `ema.rs:63`, `sub.rs:27-28`, `add.rs:36-37`, `exposure.rs:32`, `sim_broker.rs:66-67`, `lincomb.rs:47-49`, `recorder.rs:41` — name input slots.
|
|
- Modify: `crates/aura-engine/src/blueprint.rs:76` (derive_signature carries `role.name`), `:742`, `:842`, `:1938` (fixtures).
|
|
- Modify: `crates/aura-engine/src/graph_model.rs:50-53` (port_json), `:252-253` (fixture), `:422` (byte golden), `:433` + `:443` (substring pins — the leaf + composite model-name witnesses).
|
|
- Modify: `crates/aura-engine/src/harness.rs:362`, `:380` (fixtures); `crates/aura-engine/src/report.rs:210`; `crates/aura-cli/src/main.rs:54`; `crates/aura-ingest/tests/real_bars.rs:26` (fixtures).
|
|
- Test: per-node name pins in each `aura-std` node's `#[cfg(test)]` mod.
|
|
- Modify: `docs/design/INDEX.md:296` — C8 realization note (cross-link C23).
|
|
|
|
---
|
|
|
|
## Task 1: Add `name` to `PortSpec` and thread every construction site
|
|
|
|
This is the compile-gate task: adding the field breaks all 16 construction sites
|
|
workspace-wide, so they ALL land here. `port_json` is **not** touched (Task 2), so
|
|
the model byte golden stays green and the whole suite passes at the end of this task.
|
|
|
|
**Files:**
|
|
- Modify: `crates/aura-core/src/node.rs`
|
|
- Modify: `crates/aura-std/src/{sma,ema,sub,add,exposure,sim_broker,lincomb,recorder}.rs`
|
|
- Modify: `crates/aura-engine/src/blueprint.rs`, `harness.rs`, `report.rs`
|
|
- Modify: `crates/aura-cli/src/main.rs`, `crates/aura-ingest/tests/real_bars.rs`
|
|
- Modify: `crates/aura-engine/src/graph_model.rs` (the `sub_builder` test fixture only)
|
|
|
|
- [ ] **Step 1: Change the `PortSpec` struct — add `name`, drop `Copy`, update the doc**
|
|
|
|
In `crates/aura-core/src/node.rs`, replace the doc block + struct at `:29-37`:
|
|
|
|
```rust
|
|
/// One declared input **port** of a node: its scalar kind, firing policy (C6), and
|
|
/// a non-load-bearing name. The lookback depth is NOT here — it is a build-time
|
|
/// *sizing* concern answered by `Node::lookbacks()`, not part of the static
|
|
/// signature (a node's lookback can depend on an injected param, e.g. `Sma`'s
|
|
/// window = its `length`). The `name` is a **non-load-bearing** debug symbol (C23):
|
|
/// identity is the positional slot, the name is for tracing / graph rendering
|
|
/// (#13), never read by bootstrap or the run loop (which wire by index). A
|
|
/// `String`, not `&'static str` like `FieldSpec.name`: a variadic node generates
|
|
/// per-slot names (`term[0]`), exactly as `ParamSpec.name` does (`weights[0]`).
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct PortSpec {
|
|
pub kind: ScalarKind,
|
|
pub firing: Firing,
|
|
pub name: String,
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Update the `port_spec_carries_firing` test to carry + assert the name**
|
|
|
|
In `crates/aura-core/src/node.rs`, replace the test body at `:172-179`:
|
|
|
|
```rust
|
|
#[test]
|
|
fn port_spec_carries_firing() {
|
|
let a = PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "lhs".into() };
|
|
let b = PortSpec { kind: ScalarKind::F64, firing: Firing::Barrier(0), name: "rhs".into() };
|
|
assert_eq!(a.firing, Firing::Any);
|
|
assert_eq!(b.firing, Firing::Barrier(0));
|
|
assert_ne!(a.firing, b.firing);
|
|
// the name is carried (non-load-bearing, but present)
|
|
assert_eq!(a.name, "lhs");
|
|
assert_eq!(b.name, "rhs");
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Name the fixed-arity aura-std node input slots**
|
|
|
|
Edit each construction expression, appending `name`:
|
|
|
|
`crates/aura-std/src/sma.rs:31` — the single input becomes:
|
|
```rust
|
|
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "series".into() }],
|
|
```
|
|
`crates/aura-std/src/ema.rs:63` — identical, `name: "series".into()`.
|
|
|
|
`crates/aura-std/src/sub.rs:27-28` — the two inputs:
|
|
```rust
|
|
inputs: vec![
|
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "lhs".into() },
|
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "rhs".into() },
|
|
],
|
|
```
|
|
`crates/aura-std/src/add.rs:36-37` — the two inputs, identical names `"lhs"` / `"rhs"`.
|
|
|
|
`crates/aura-std/src/exposure.rs:32` — the single input: `name: "signal".into()`.
|
|
|
|
`crates/aura-std/src/sim_broker.rs:66-67` — the two inputs (the line comments move into the data):
|
|
```rust
|
|
inputs: vec![
|
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "exposure".into() },
|
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "price".into() },
|
|
],
|
|
```
|
|
|
|
- [ ] **Step 4: Name the variadic aura-std node input slots (generated in the loop)**
|
|
|
|
`crates/aura-std/src/lincomb.rs:47-49` — change `|_|` to `|i|` and generate the name (mirrors the `weights[i]` param loop two lines below):
|
|
```rust
|
|
let inputs = (0..arity)
|
|
.map(|i| PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: format!("term[{i}]") })
|
|
.collect();
|
|
```
|
|
|
|
`crates/aura-std/src/recorder.rs:41` — add `.enumerate()` and generate the name:
|
|
```rust
|
|
let inputs = kinds
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, &kind)| PortSpec { kind, firing, name: format!("col[{i}]") })
|
|
.collect();
|
|
```
|
|
|
|
- [ ] **Step 5: Carry `Role.name` into the derived composite input port**
|
|
|
|
`crates/aura-engine/src/blueprint.rs:76` — inside `derive_signature`'s `.map(|role| { ... })`, the input port stops dropping the boundary name (restores symmetry with the output side at `:84`, which already does `FieldSpec { name: leak_name(&of.name), kind }`):
|
|
```rust
|
|
PortSpec { kind, firing: Firing::Any, name: role.name.clone() }
|
|
```
|
|
|
|
- [ ] **Step 6: Name the engine/cli/ingest test fixtures (fixture names are fine)**
|
|
|
|
- `crates/aura-engine/src/blueprint.rs:742` (`f64_any`):
|
|
```rust
|
|
fn f64_any() -> PortSpec {
|
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "in".into() }
|
|
}
|
|
```
|
|
- `crates/aura-engine/src/blueprint.rs:842` (`sink_i64` input): `PortSpec { kind: ScalarKind::I64, firing: Firing::Any, name: "in".into() }`.
|
|
- `crates/aura-engine/src/blueprint.rs:1938` (the exploding-builder fixture): `PortSpec { kind: ScalarKind::I64, firing: Firing::Any, name: "in".into() }`.
|
|
- `crates/aura-engine/src/harness.rs:362` (`f64_port`): `PortSpec { kind: ScalarKind::F64, firing, name: "in".into() }` (keep its existing `firing` arg).
|
|
- `crates/aura-engine/src/harness.rs:380` (`recorder_sig` loop): mirror the real Recorder —
|
|
```rust
|
|
kinds.iter().enumerate().map(|(i, &kind)| PortSpec { kind, firing, name: format!("col[{i}]") }).collect()
|
|
```
|
|
- `crates/aura-engine/src/report.rs:210`: `PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "in".into() }`.
|
|
- `crates/aura-cli/src/main.rs:54`: `aura_engine::PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "in".into() }` (keep the `aura_engine::` qualifier).
|
|
- `crates/aura-ingest/tests/real_bars.rs:26`: `PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "in".into() }`.
|
|
- `crates/aura-engine/src/graph_model.rs:252-253` (the `sub_builder` test fixture, used by Task 2's leaf-name test) — name its two ports:
|
|
```rust
|
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "lhs".into() },
|
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Barrier(0), name: "rhs".into() },
|
|
```
|
|
|
|
- [ ] **Step 7: Build the workspace — confirm every site is threaded**
|
|
|
|
Run: `cargo build --workspace`
|
|
Expected: finishes with 0 errors (no `missing field name in initializer of PortSpec` and no `Copy`-move errors remain). If the compiler names any `PortSpec` literal still lacking `name`, add `name: "in".into()` (fixture) and rebuild.
|
|
|
|
- [ ] **Step 8: Run the full suite — confirm still green (port_json unchanged → golden intact)**
|
|
|
|
Run: `cargo test --workspace`
|
|
Expected: PASS, 0 failures. The model byte golden is unaffected because `port_json` still emits two-element tuples; no existing test asserts full `PortSpec` equality (the derive test checks only `.len()` and `.kind`).
|
|
|
|
---
|
|
|
|
## Task 2: `model_to_json` emits the name — `port_json` + golden + twins
|
|
|
|
**Files:**
|
|
- Modify: `crates/aura-engine/src/graph_model.rs:50-53` (port_json), `:422` (golden), `:433` + `:443` (substring pins)
|
|
|
|
- [ ] **Step 1: Append the name to `port_json`**
|
|
|
|
`crates/aura-engine/src/graph_model.rs:50-53` — rewrite the doc + body:
|
|
```rust
|
|
/// One input port as a model tuple: `["<kind>","<firing>","<name>"]`. The name is
|
|
/// the port's non-load-bearing label (`PortSpec.name`); for a composite boundary
|
|
/// it is the `Role.name` carried through `derive_signature`.
|
|
fn port_json(p: &aura_core::PortSpec) -> String {
|
|
format!(
|
|
"[{},{},{}]",
|
|
json_str(kind_str(p.kind)),
|
|
json_str(&firing_str(p.firing)),
|
|
json_str(&p.name)
|
|
)
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Run the golden test to see it go RED**
|
|
|
|
Run: `cargo test -p aura-engine model_golden`
|
|
Expected: FAIL — the emitted `ins` tuples now carry a third element, mismatching the stored golden at `:422`.
|
|
|
|
- [ ] **Step 3: Re-capture the byte golden**
|
|
|
|
Follow the re-capture recipe already in the test header (`graph_model.rs:419-421`):
|
|
run the print harness, copy the exact emitted JSON, and paste it as the new
|
|
`r##"..."##` golden string at `:422`. Every `"ins":[...]` tuple gains its third
|
|
element (leaf ports from `PortSpec.name`, e.g. `["f64","any","series"]`; the
|
|
composite `"inputs":[["f64","any"]]` becomes `[["f64","any","price"]]` from
|
|
`Role.name`; the `src_price` source keeps its empty `ins`).
|
|
|
|
Run: `cargo test -p aura-engine model_golden`
|
|
Expected: PASS (golden now matches the re-captured bytes).
|
|
|
|
- [ ] **Step 4: Update the two substring-pin twins**
|
|
|
|
`crates/aura-engine/src/graph_model.rs:433` (`sink_has_empty_outs`) — the Recorder
|
|
sink's single input now carries `col[0]`. Replace the asserted substring:
|
|
```rust
|
|
r#""role":"sink","params":[],"ins":[["f64","any","col[0]"]],"outs":[]"#
|
|
```
|
|
|
|
`crates/aura-engine/src/graph_model.rs:443` (`multi_input_composite_has_two_inputs`)
|
|
— the `two_input_composite()` fixture's roles are named `a`/`b`, carried through
|
|
`derive_signature`. Replace the `starts_with` prefix:
|
|
```rust
|
|
r#"{"inputs":[["f64","any","a"],["f64","any","b"]],"#
|
|
```
|
|
This assertion now also witnesses the composite-boundary `Role.name` carry.
|
|
|
|
- [ ] **Step 5: Run the engine suite green**
|
|
|
|
Run: `cargo test -p aura-engine`
|
|
Expected: PASS, 0 failures (golden re-captured, both twins updated, determinism
|
|
test still green). The two updated twins are exactly the spec's two model-name
|
|
witnesses: `sink_has_empty_outs` pins a **leaf** `PortSpec.name` reaching the model
|
|
(`Recorder`'s `col[0]` — a leaf primitive), and `multi_input_composite_has_two_inputs`
|
|
pins a **composite** boundary `Role.name` carried through `derive_signature`
|
|
(`a`/`b`).
|
|
|
|
---
|
|
|
|
## Task 3: Lock the per-node input-slot names (aura-std)
|
|
|
|
Each test pins the declared names so a future rename is a visible test change. Each
|
|
goes in the node's existing `#[cfg(test)] mod tests`.
|
|
|
|
**Files:**
|
|
- Test: `crates/aura-std/src/{sma,ema,sub,add,exposure,sim_broker,lincomb,recorder}.rs`
|
|
|
|
- [ ] **Step 1: Fixed-arity node name pins**
|
|
|
|
Add to each node's test module:
|
|
|
|
`sma.rs`:
|
|
```rust
|
|
#[test]
|
|
fn input_slot_is_named_series() {
|
|
assert_eq!(Sma::builder().schema().inputs[0].name, "series");
|
|
}
|
|
```
|
|
`ema.rs`:
|
|
```rust
|
|
#[test]
|
|
fn input_slot_is_named_series() {
|
|
assert_eq!(Ema::builder().schema().inputs[0].name, "series");
|
|
}
|
|
```
|
|
`sub.rs`:
|
|
```rust
|
|
#[test]
|
|
fn input_slots_are_named_lhs_rhs() {
|
|
let s = Sub::builder();
|
|
let names: Vec<&str> = s.schema().inputs.iter().map(|p| p.name.as_str()).collect();
|
|
assert_eq!(names, ["lhs", "rhs"]);
|
|
}
|
|
```
|
|
`add.rs`:
|
|
```rust
|
|
#[test]
|
|
fn input_slots_are_named_lhs_rhs() {
|
|
let a = Add::builder();
|
|
let names: Vec<&str> = a.schema().inputs.iter().map(|p| p.name.as_str()).collect();
|
|
assert_eq!(names, ["lhs", "rhs"]);
|
|
}
|
|
```
|
|
`exposure.rs`:
|
|
```rust
|
|
#[test]
|
|
fn input_slot_is_named_signal() {
|
|
assert_eq!(Exposure::builder().schema().inputs[0].name, "signal");
|
|
}
|
|
```
|
|
`sim_broker.rs` (the #21 self-documentation win):
|
|
```rust
|
|
#[test]
|
|
fn input_slots_are_named_exposure_price() {
|
|
let b = SimBroker::builder(1.0);
|
|
let names: Vec<&str> = b.schema().inputs.iter().map(|p| p.name.as_str()).collect();
|
|
assert_eq!(names, ["exposure", "price"]);
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Variadic node name pins (generated sequence)**
|
|
|
|
`lincomb.rs`:
|
|
```rust
|
|
#[test]
|
|
fn input_slots_are_named_term_index() {
|
|
let lc = LinComb::builder(3);
|
|
let names: Vec<String> = lc.schema().inputs.iter().map(|p| p.name.clone()).collect();
|
|
assert_eq!(names, ["term[0]", "term[1]", "term[2]"]);
|
|
}
|
|
```
|
|
`recorder.rs` (build a throwaway channel as the existing recorder tests do):
|
|
```rust
|
|
#[test]
|
|
fn input_slots_are_named_col_index() {
|
|
let (tx, _rx) = std::sync::mpsc::channel();
|
|
let r = Recorder::builder(vec![ScalarKind::F64, ScalarKind::F64], Firing::Any, tx);
|
|
let names: Vec<String> = r.schema().inputs.iter().map(|p| p.name.clone()).collect();
|
|
assert_eq!(names, ["col[0]", "col[1]"]);
|
|
}
|
|
```
|
|
`ScalarKind` and `Firing` are already in scope in `recorder.rs`'s test module (the
|
|
existing test at `:91` constructs `Recorder::builder(vec![ScalarKind::F64], Firing::Any, …)`),
|
|
so no new import is needed.
|
|
|
|
- [ ] **Step 3: Run aura-std + aura-core green**
|
|
|
|
Run: `cargo test -p aura-std -p aura-core`
|
|
Expected: PASS, 0 failures — all 8 node name pins plus the `port_spec_carries_firing` name assertion (Task 1) green.
|
|
|
|
---
|
|
|
|
## Task 4: Record the realization in the design ledger
|
|
|
|
**Files:**
|
|
- Modify: `docs/design/INDEX.md:296` (C8 section, after the cycle-0024 note, before `### C9`)
|
|
|
|
- [ ] **Step 1: Insert the C8 realization note**
|
|
|
|
At `docs/design/INDEX.md:296` (the blank line after C8's last realization note,
|
|
before `### C9`), insert, matching the existing realization-note format (bold lead
|
|
+ prose, as the FieldSpec/0005 note at `:219` and the param/0015 note at `:253`):
|
|
|
|
```markdown
|
|
**Realization (cycle 0027 — name input ports, refs #21/#51).** `PortSpec` now
|
|
carries a `name: String` (it drops `Copy`, like `ParamSpec`), so an input port is
|
|
named just as `FieldSpec.name` (output) and `ParamSpec.name` (param) already are.
|
|
The name is **non-load-bearing** (C23): wiring stays positional by slot, bootstrap
|
|
and the run loop never read it; it exists for tracing / graph rendering (#13). Leaf
|
|
primitives declare their slot names (`SimBroker`'s `exposure`/`price`, etc.);
|
|
`derive_signature` carries a composite's `Role.name` into the derived input port,
|
|
so the graph model (`model_to_json`) is homogeneously named across inputs, outputs,
|
|
and params and across both graph levels. This does **not** close #21 (a swapped
|
|
same-kind wiring is still only kind-checked) — it makes those slots
|
|
self-documenting; a name-consuming validation is its own future cycle.
|
|
```
|
|
|
|
- [ ] **Step 2: Verify the ledger still reads cleanly**
|
|
|
|
Run: `cargo doc --workspace --no-deps 2>&1 | rg -i "warning|error" || echo "doc clean"`
|
|
Expected: `doc clean` (the ledger is Markdown, not rustdoc — this gate just
|
|
confirms the code changes introduced no doc-build regression). The ledger edit
|
|
itself is prose; no code gate applies.
|