feat(aura-core): name input ports
PortSpec gains a non-load-bearing `name: String`, so an input port is named just as FieldSpec.name (output) and ParamSpec.name (param) already are — input ports were the lone unnamed member of the node signature. Identity stays positional by slot (C23); the name is render/debug only, never read by bootstrap or the run loop. PortSpec drops Copy (String is not Copy), exactly as ParamSpec already does. - Every aura-std node names its input slots: SMA/EMA "series", Sub/Add "lhs"/"rhs", Exposure "signal", SimBroker "exposure"/"price" (the slots become self-documenting), LinComb "term[i]" and Recorder "col[i]" generated in their build loops (mirroring LinComb's existing weights[i] param loop). - derive_signature carries a composite's Role.name into the derived input port (it was dropped before — the output side already carried FieldSpec.name), so the graph model is homogeneously named at both levels. - model_to_json (port_json + the composite-header inputs in scope_json) emits the name as a third tuple element: ["f64","any","exposure"]. The byte golden was re-captured (machine bytes) and its substring twins updated; the model is now fully named across inputs/outputs/params. - All 16 PortSpec construction sites threaded in one compile-gate change; test fixtures carry fixture names. C8 realization note added to the design ledger. Why name-only, no validation: the name is a pure debug symbol. Wire-by-name was rejected (it would be a C23 contract change). Bootstrap slot-wiring validation (which would close #21's same-kind swap footgun) is deferred to its own cycle — a name alone does not catch the swap; it makes the slots self-documenting and gives a future validation something to check against. Verified: cargo test --workspace 168 green; clippy --all-targets -D warnings clean; cargo build --workspace clean. Read-only render path (C9), no serde (C14), scalar kinds unchanged (C4). closes #50 refs #21 refs #51
This commit is contained in:
@@ -51,7 +51,7 @@ fn sample_harness() -> (
|
|||||||
let (tx_eq, rx_eq) = mpsc::channel();
|
let (tx_eq, rx_eq) = mpsc::channel();
|
||||||
let (tx_ex, rx_ex) = mpsc::channel();
|
let (tx_ex, rx_ex) = mpsc::channel();
|
||||||
let f64_recorder_sig = || aura_engine::NodeSchema {
|
let f64_recorder_sig = || aura_engine::NodeSchema {
|
||||||
inputs: vec![aura_engine::PortSpec { kind: ScalarKind::F64, firing: Firing::Any }],
|
inputs: vec![aura_engine::PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "in".into() }],
|
||||||
output: vec![],
|
output: vec![],
|
||||||
params: vec![],
|
params: vec![],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,14 +26,20 @@ pub enum Firing {
|
|||||||
Barrier(u8),
|
Barrier(u8),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One declared input **port** of a node: its scalar kind and firing policy (C6).
|
/// One declared input **port** of a node: its scalar kind, firing policy (C6), and
|
||||||
/// The lookback depth is NOT here — it is a build-time *sizing* concern answered by
|
/// a non-load-bearing name. The lookback depth is NOT here — it is a build-time
|
||||||
/// `Node::lookbacks()`, not part of the static signature (a node's lookback can
|
/// *sizing* concern answered by `Node::lookbacks()`, not part of the static
|
||||||
/// depend on an injected param, e.g. `Sma`'s window = its `length`).
|
/// signature (a node's lookback can depend on an injected param, e.g. `Sma`'s
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
/// 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 struct PortSpec {
|
||||||
pub kind: ScalarKind,
|
pub kind: ScalarKind,
|
||||||
pub firing: Firing,
|
pub firing: Firing,
|
||||||
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One declared output column of a node's record: its name (metadata for sinks /
|
/// One declared output column of a node's record: its name (metadata for sinks /
|
||||||
@@ -171,11 +177,14 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn port_spec_carries_firing() {
|
fn port_spec_carries_firing() {
|
||||||
let a = PortSpec { kind: ScalarKind::F64, firing: Firing::Any };
|
let a = PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "lhs".into() };
|
||||||
let b = PortSpec { kind: ScalarKind::F64, firing: Firing::Barrier(0) };
|
let b = PortSpec { kind: ScalarKind::F64, firing: Firing::Barrier(0), name: "rhs".into() };
|
||||||
assert_eq!(a.firing, Firing::Any);
|
assert_eq!(a.firing, Firing::Any);
|
||||||
assert_eq!(b.firing, Firing::Barrier(0));
|
assert_eq!(b.firing, Firing::Barrier(0));
|
||||||
assert_ne!(a.firing, b.firing);
|
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ fn derive_signature(c: &Composite) -> NodeSchema {
|
|||||||
.first()
|
.first()
|
||||||
.map(|t| interior_slot_kind(c.nodes(), c.edges(), t))
|
.map(|t| interior_slot_kind(c.nodes(), c.edges(), t))
|
||||||
.unwrap_or(ScalarKind::F64);
|
.unwrap_or(ScalarKind::F64);
|
||||||
PortSpec { kind, firing: Firing::Any }
|
PortSpec { kind, firing: Firing::Any, name: role.name.clone() }
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let output = c
|
let output = c
|
||||||
@@ -739,7 +739,7 @@ mod tests {
|
|||||||
|
|
||||||
/// One f64 input port with `Firing::Any` (the common case for these fixtures).
|
/// One f64 input port with `Firing::Any` (the common case for these fixtures).
|
||||||
fn f64_any() -> PortSpec {
|
fn f64_any() -> PortSpec {
|
||||||
PortSpec { kind: ScalarKind::F64, firing: Firing::Any }
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "in".into() }
|
||||||
}
|
}
|
||||||
/// A one-f64-field output record under name `v`.
|
/// A one-f64-field output record under name `v`.
|
||||||
fn out_v() -> Vec<FieldSpec> {
|
fn out_v() -> Vec<FieldSpec> {
|
||||||
@@ -839,7 +839,7 @@ mod tests {
|
|||||||
PrimitiveBuilder::new(
|
PrimitiveBuilder::new(
|
||||||
"SinkI64",
|
"SinkI64",
|
||||||
NodeSchema {
|
NodeSchema {
|
||||||
inputs: vec![PortSpec { kind: ScalarKind::I64, firing: Firing::Any }],
|
inputs: vec![PortSpec { kind: ScalarKind::I64, firing: Firing::Any, name: "in".into() }],
|
||||||
output: vec![],
|
output: vec![],
|
||||||
params: vec![],
|
params: vec![],
|
||||||
},
|
},
|
||||||
@@ -1935,7 +1935,7 @@ mod tests {
|
|||||||
let exploding = PrimitiveBuilder::new(
|
let exploding = PrimitiveBuilder::new(
|
||||||
"Boom",
|
"Boom",
|
||||||
NodeSchema {
|
NodeSchema {
|
||||||
inputs: vec![PortSpec { kind: ScalarKind::I64, firing: Firing::Any }],
|
inputs: vec![PortSpec { kind: ScalarKind::I64, firing: Firing::Any, name: "in".into() }],
|
||||||
output: vec![FieldSpec { name: "v", kind: ScalarKind::I64 }],
|
output: vec![FieldSpec { name: "v", kind: ScalarKind::I64 }],
|
||||||
params: vec![],
|
params: vec![],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -47,9 +47,16 @@ fn named_kind(name: &str, kind: ScalarKind) -> String {
|
|||||||
format!("[{},{}]", json_str(name), json_str(kind_str(kind)))
|
format!("[{},{}]", json_str(name), json_str(kind_str(kind)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An input port `[kind, firing]` (no name — `PortSpec` carries none).
|
/// 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 {
|
fn port_json(p: &aura_core::PortSpec) -> String {
|
||||||
format!("[{},{}]", json_str(kind_str(p.kind)), json_str(&firing_str(p.firing)))
|
format!(
|
||||||
|
"[{},{},{}]",
|
||||||
|
json_str(kind_str(p.kind)),
|
||||||
|
json_str(&firing_str(p.firing)),
|
||||||
|
json_str(&p.name)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Comma-join with no surrounding brackets.
|
/// Comma-join with no surrounding brackets.
|
||||||
@@ -130,13 +137,14 @@ fn scope_json(c: &Composite, is_root: bool) -> String {
|
|||||||
/// re-exported field), and the interior `nodes`/`edges` with `@role` / `#N`
|
/// re-exported field), and the interior `nodes`/`edges` with `@role` / `#N`
|
||||||
/// endpoints folded onto the edge list.
|
/// endpoints folded onto the edge list.
|
||||||
fn composite_def_json(c: &Composite) -> String {
|
fn composite_def_json(c: &Composite) -> String {
|
||||||
// inputs: one port per input role; kind from the interior slot it feeds.
|
// inputs: one port per input role; kind from the interior slot it feeds, name
|
||||||
|
// from the role (the boundary `Role.name`, mirroring `derive_signature`).
|
||||||
let inputs = join(c.input_roles().iter().map(|r| {
|
let inputs = join(c.input_roles().iter().map(|r| {
|
||||||
let kind = r
|
let kind = r
|
||||||
.source
|
.source
|
||||||
.or_else(|| r.targets.first().and_then(|t| input_slot_kind(c, t.node, t.slot)))
|
.or_else(|| r.targets.first().and_then(|t| input_slot_kind(c, t.node, t.slot)))
|
||||||
.unwrap_or(ScalarKind::F64);
|
.unwrap_or(ScalarKind::F64);
|
||||||
format!("[{},{}]", json_str(kind_str(kind)), json_str("any"))
|
format!("[{},{},{}]", json_str(kind_str(kind)), json_str("any"), json_str(&r.name))
|
||||||
}));
|
}));
|
||||||
// outputs: [name, kind] per OutField. Kind from the producing node's field.
|
// outputs: [name, kind] per OutField. Kind from the producing node's field.
|
||||||
let outputs = join(c.output().iter().map(|of| {
|
let outputs = join(c.output().iter().map(|of| {
|
||||||
@@ -249,8 +257,8 @@ mod tests {
|
|||||||
"Sub",
|
"Sub",
|
||||||
NodeSchema {
|
NodeSchema {
|
||||||
inputs: vec![
|
inputs: vec![
|
||||||
PortSpec { kind: ScalarKind::F64, firing: Firing::Any },
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "lhs".into() },
|
||||||
PortSpec { kind: ScalarKind::F64, firing: Firing::Barrier(0) },
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Barrier(0), name: "rhs".into() },
|
||||||
],
|
],
|
||||||
output: vec![FieldSpec { name: "diff", kind: ScalarKind::F64 }],
|
output: vec![FieldSpec { name: "diff", kind: ScalarKind::F64 }],
|
||||||
params: vec![],
|
params: vec![],
|
||||||
@@ -361,7 +369,7 @@ mod tests {
|
|||||||
let got = prim_record(&sub_builder());
|
let got = prim_record(&sub_builder());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
got,
|
got,
|
||||||
r#"{"prim":{"type":"Sub","role":"node","params":[],"ins":[["f64","any"],["f64","barrier 0"]],"outs":[["diff","f64"]]}}"#
|
r#"{"prim":{"type":"Sub","role":"node","params":[],"ins":[["f64","any","lhs"],["f64","barrier 0","rhs"]],"outs":[["diff","f64"]]}}"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,7 +398,7 @@ mod tests {
|
|||||||
_ => panic!(),
|
_ => panic!(),
|
||||||
};
|
};
|
||||||
let got = composite_def_json(inner);
|
let got = composite_def_json(inner);
|
||||||
assert!(got.contains(r#""inputs":[["f64","any"]]"#), "{got}");
|
assert!(got.contains(r#""inputs":[["f64","any","price"]]"#), "{got}");
|
||||||
// an interior input role becomes an @role endpoint:
|
// an interior input role becomes an @role endpoint:
|
||||||
assert!(got.contains(r#""@price""#), "{got}");
|
assert!(got.contains(r#""@price""#), "{got}");
|
||||||
// an output binding becomes a #N endpoint:
|
// an output binding becomes a #N endpoint:
|
||||||
@@ -419,7 +427,7 @@ mod tests {
|
|||||||
// Re-capture if an intended model-shape change lands: temporarily add a
|
// Re-capture if an intended model-shape change lands: temporarily add a
|
||||||
// `println!("{}", model_to_json(&sample_root()))` test, run with
|
// `println!("{}", model_to_json(&sample_root()))` test, run with
|
||||||
// `-- --nocapture`, and paste the fresh bytes below.
|
// `-- --nocapture`, and paste the fresh bytes below.
|
||||||
let expected = r##"{"root":{"nodes":{"0":{"comp":"sma_cross"},"1":{"prim":{"type":"Exposure","role":"node","params":[["scale","f64"]],"ins":[["f64","any"]],"outs":[["exposure","f64"]]}},"2":{"prim":{"type":"Recorder","role":"sink","params":[],"ins":[["f64","any"]],"outs":[]}},"src_price":{"prim":{"type":"price","role":"source","params":[],"ins":[],"outs":[["price","f64"]]}}},"edges":[["0.o0","1.i0"],["1.o0","2.i0"],["src_price.o0","0.i0"]]},"composites":{"sma_cross":{"inputs":[["f64","any"]],"outputs":[["out","f64"]],"nodes":{"0":{"prim":{"type":"SMA","role":"node","params":[["length","i64"]],"ins":[["f64","any"]],"outs":[["value","f64"]]}},"1":{"prim":{"type":"SMA","role":"node","params":[["length","i64"]],"ins":[["f64","any"]],"outs":[["value","f64"]]}},"2":{"prim":{"type":"Sub","role":"node","params":[],"ins":[["f64","any"],["f64","any"]],"outs":[["value","f64"]]}}},"edges":[["0.o0","2.i0"],["1.o0","2.i1"],["@price","0.i0"],["@price","1.i0"],["2.o0","#0"]]}}}"##;
|
let expected = r##"{"root":{"nodes":{"0":{"comp":"sma_cross"},"1":{"prim":{"type":"Exposure","role":"node","params":[["scale","f64"]],"ins":[["f64","any","signal"]],"outs":[["exposure","f64"]]}},"2":{"prim":{"type":"Recorder","role":"sink","params":[],"ins":[["f64","any","col[0]"]],"outs":[]}},"src_price":{"prim":{"type":"price","role":"source","params":[],"ins":[],"outs":[["price","f64"]]}}},"edges":[["0.o0","1.i0"],["1.o0","2.i0"],["src_price.o0","0.i0"]]},"composites":{"sma_cross":{"inputs":[["f64","any","price"]],"outputs":[["out","f64"]],"nodes":{"0":{"prim":{"type":"SMA","role":"node","params":[["length","i64"]],"ins":[["f64","any","series"]],"outs":[["value","f64"]]}},"1":{"prim":{"type":"SMA","role":"node","params":[["length","i64"]],"ins":[["f64","any","series"]],"outs":[["value","f64"]]}},"2":{"prim":{"type":"Sub","role":"node","params":[],"ins":[["f64","any","lhs"],["f64","any","rhs"]],"outs":[["value","f64"]]}}},"edges":[["0.o0","2.i0"],["1.o0","2.i1"],["@price","0.i0"],["@price","1.i0"],["2.o0","#0"]]}}}"##;
|
||||||
assert_eq!(model_to_json(&sample_root()), expected);
|
assert_eq!(model_to_json(&sample_root()), expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,7 +438,7 @@ mod tests {
|
|||||||
// the Recorder node in the root is a sink with no outputs.
|
// the Recorder node in the root is a sink with no outputs.
|
||||||
let model = model_to_json(&sample_root());
|
let model = model_to_json(&sample_root());
|
||||||
assert!(
|
assert!(
|
||||||
model.contains(r#""role":"sink","params":[],"ins":[["f64","any"]],"outs":[]"#),
|
model.contains(r#""role":"sink","params":[],"ins":[["f64","any","col[0]"]],"outs":[]"#),
|
||||||
"{model}"
|
"{model}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -440,7 +448,7 @@ mod tests {
|
|||||||
// a composite with two f64 input roles serialises exactly two ports in "inputs".
|
// a composite with two f64 input roles serialises exactly two ports in "inputs".
|
||||||
let c = two_input_composite();
|
let c = two_input_composite();
|
||||||
let def = composite_def_json(&c);
|
let def = composite_def_json(&c);
|
||||||
assert!(def.starts_with(r#"{"inputs":[["f64","any"],["f64","any"]],"#), "{def}");
|
assert!(def.starts_with(r#"{"inputs":[["f64","any","a"],["f64","any","b"]],"#), "{def}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -359,7 +359,7 @@ mod tests {
|
|||||||
/// One f64 input port with the given firing policy (lookback 1 is the bootstrap
|
/// One f64 input port with the given firing policy (lookback 1 is the bootstrap
|
||||||
/// default for these test fixtures, supplied by their `lookbacks()`).
|
/// default for these test fixtures, supplied by their `lookbacks()`).
|
||||||
fn f64_port(firing: Firing) -> PortSpec {
|
fn f64_port(firing: Firing) -> PortSpec {
|
||||||
PortSpec { kind: ScalarKind::F64, firing }
|
PortSpec { kind: ScalarKind::F64, firing, name: "in".into() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bootstrap a hand-wired graph from boxed nodes + their declared signatures.
|
/// Bootstrap a hand-wired graph from boxed nodes + their declared signatures.
|
||||||
@@ -377,7 +377,7 @@ mod tests {
|
|||||||
/// The declared signature of a `Recorder` over `kinds` with the given firing.
|
/// The declared signature of a `Recorder` over `kinds` with the given firing.
|
||||||
fn recorder_sig(kinds: &[ScalarKind], firing: Firing) -> NodeSchema {
|
fn recorder_sig(kinds: &[ScalarKind], firing: Firing) -> NodeSchema {
|
||||||
NodeSchema {
|
NodeSchema {
|
||||||
inputs: kinds.iter().map(|&kind| PortSpec { kind, firing }).collect(),
|
inputs: kinds.iter().enumerate().map(|(i, &kind)| PortSpec { kind, firing, name: format!("col[{i}]") }).collect(),
|
||||||
output: vec![],
|
output: vec![],
|
||||||
params: vec![],
|
params: vec![],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ mod tests {
|
|||||||
/// the two-sink harness uses).
|
/// the two-sink harness uses).
|
||||||
fn f64_recorder_sig() -> NodeSchema {
|
fn f64_recorder_sig() -> NodeSchema {
|
||||||
NodeSchema {
|
NodeSchema {
|
||||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any }],
|
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "in".into() }],
|
||||||
output: vec![],
|
output: vec![],
|
||||||
params: vec![],
|
params: vec![],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ fn run_sample_over(prices: Vec<(Timestamp, Scalar)>) -> RunReport {
|
|||||||
let (tx_eq, rx_eq) = mpsc::channel();
|
let (tx_eq, rx_eq) = mpsc::channel();
|
||||||
let (tx_ex, rx_ex) = mpsc::channel();
|
let (tx_ex, rx_ex) = mpsc::channel();
|
||||||
let f64_recorder_sig = || NodeSchema {
|
let f64_recorder_sig = || NodeSchema {
|
||||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any }],
|
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "in".into() }],
|
||||||
output: vec![],
|
output: vec![],
|
||||||
params: vec![],
|
params: vec![],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ impl Add {
|
|||||||
"Add",
|
"Add",
|
||||||
NodeSchema {
|
NodeSchema {
|
||||||
inputs: vec![
|
inputs: vec![
|
||||||
PortSpec { kind: ScalarKind::F64, firing: Firing::Any },
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "lhs".into() },
|
||||||
PortSpec { kind: ScalarKind::F64, firing: Firing::Any },
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "rhs".into() },
|
||||||
],
|
],
|
||||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||||
params: vec![],
|
params: vec![],
|
||||||
@@ -91,4 +91,11 @@ mod tests {
|
|||||||
inputs[1].push(Scalar::F64(4.0)).unwrap();
|
inputs[1].push(Scalar::F64(4.0)).unwrap();
|
||||||
assert_eq!(add.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::F64(14.0)].as_slice()));
|
assert_eq!(add.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::F64(14.0)].as_slice()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ impl Ema {
|
|||||||
PrimitiveBuilder::new(
|
PrimitiveBuilder::new(
|
||||||
"EMA",
|
"EMA",
|
||||||
NodeSchema {
|
NodeSchema {
|
||||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any }],
|
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "series".into() }],
|
||||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||||
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||||
},
|
},
|
||||||
@@ -161,4 +161,9 @@ mod tests {
|
|||||||
assert_eq!(b.schema().params[0].name, "length");
|
assert_eq!(b.schema().params[0].name, "length");
|
||||||
assert_eq!(b.schema().params[0].kind, ScalarKind::I64);
|
assert_eq!(b.schema().params[0].kind, ScalarKind::I64);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn input_slot_is_named_series() {
|
||||||
|
assert_eq!(Ema::builder().schema().inputs[0].name, "series");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ impl Exposure {
|
|||||||
PrimitiveBuilder::new(
|
PrimitiveBuilder::new(
|
||||||
"Exposure",
|
"Exposure",
|
||||||
NodeSchema {
|
NodeSchema {
|
||||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any }],
|
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "signal".into() }],
|
||||||
output: vec![FieldSpec { name: "exposure", kind: ScalarKind::F64 }],
|
output: vec![FieldSpec { name: "exposure", kind: ScalarKind::F64 }],
|
||||||
params: vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }],
|
params: vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }],
|
||||||
},
|
},
|
||||||
@@ -89,4 +89,9 @@ mod tests {
|
|||||||
let inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)];
|
let inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)];
|
||||||
assert_eq!(e.eval(Ctx::new(&inputs, Timestamp(0))), None);
|
assert_eq!(e.eval(Ctx::new(&inputs, Timestamp(0))), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn input_slot_is_named_signal() {
|
||||||
|
assert_eq!(Exposure::builder().schema().inputs[0].name, "signal");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ impl LinComb {
|
|||||||
/// are injected, slot by slot, through `LinComb::new` (the single sizing gate).
|
/// are injected, slot by slot, through `LinComb::new` (the single sizing gate).
|
||||||
pub fn builder(arity: usize) -> PrimitiveBuilder {
|
pub fn builder(arity: usize) -> PrimitiveBuilder {
|
||||||
let inputs = (0..arity)
|
let inputs = (0..arity)
|
||||||
.map(|_| PortSpec { kind: ScalarKind::F64, firing: Firing::Any })
|
.map(|i| PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: format!("term[{i}]") })
|
||||||
.collect();
|
.collect();
|
||||||
let params = (0..arity)
|
let params = (0..arity)
|
||||||
.map(|i| ParamSpec { name: format!("weights[{i}]"), kind: ScalarKind::F64 })
|
.map(|i| ParamSpec { name: format!("weights[{i}]"), kind: ScalarKind::F64 })
|
||||||
@@ -141,4 +141,11 @@ mod tests {
|
|||||||
fn lincomb_empty_weights_panics() {
|
fn lincomb_empty_weights_panics() {
|
||||||
let _ = LinComb::new(vec![]);
|
let _ = LinComb::new(vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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]"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,11 @@ impl Recorder {
|
|||||||
firing: Firing,
|
firing: Firing,
|
||||||
tx: Sender<(Timestamp, Vec<Scalar>)>,
|
tx: Sender<(Timestamp, Vec<Scalar>)>,
|
||||||
) -> PrimitiveBuilder {
|
) -> PrimitiveBuilder {
|
||||||
let inputs = kinds.iter().map(|&kind| PortSpec { kind, firing }).collect();
|
let inputs = kinds
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, &kind)| PortSpec { kind, firing, name: format!("col[{i}]") })
|
||||||
|
.collect();
|
||||||
let build_kinds = kinds.clone();
|
let build_kinds = kinds.clone();
|
||||||
PrimitiveBuilder::new(
|
PrimitiveBuilder::new(
|
||||||
"Recorder",
|
"Recorder",
|
||||||
@@ -137,4 +141,12 @@ mod tests {
|
|||||||
let rows: Vec<(Timestamp, Vec<Scalar>)> = rx.try_iter().collect();
|
let rows: Vec<(Timestamp, Vec<Scalar>)> = rx.try_iter().collect();
|
||||||
assert_eq!(rows, vec![(Timestamp(2), vec![Scalar::F64(1.0), Scalar::F64(2.0)])]);
|
assert_eq!(rows, vec![(Timestamp(2), vec![Scalar::F64(1.0), Scalar::F64(2.0)])]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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]"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,8 +63,8 @@ impl SimBroker {
|
|||||||
"SimBroker",
|
"SimBroker",
|
||||||
NodeSchema {
|
NodeSchema {
|
||||||
inputs: vec![
|
inputs: vec![
|
||||||
PortSpec { kind: ScalarKind::F64, firing: Firing::Any }, // 0 exposure
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "exposure".into() },
|
||||||
PortSpec { kind: ScalarKind::F64, firing: Firing::Any }, // 1 price
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "price".into() },
|
||||||
],
|
],
|
||||||
output: vec![FieldSpec { name: "equity", kind: ScalarKind::F64 }],
|
output: vec![FieldSpec { name: "equity", kind: ScalarKind::F64 }],
|
||||||
params: vec![],
|
params: vec![],
|
||||||
@@ -162,4 +162,11 @@ mod tests {
|
|||||||
let mut inputs = two_f64_inputs();
|
let mut inputs = two_f64_inputs();
|
||||||
assert_eq!(step(&mut b, &mut inputs, Some(1.0), 100.0), 0.0); // no prev price to mark
|
assert_eq!(step(&mut b, &mut inputs, Some(1.0), 100.0), 0.0); // no prev price to mark
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ impl Sma {
|
|||||||
PrimitiveBuilder::new(
|
PrimitiveBuilder::new(
|
||||||
"SMA",
|
"SMA",
|
||||||
NodeSchema {
|
NodeSchema {
|
||||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any }],
|
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "series".into() }],
|
||||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||||
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||||
},
|
},
|
||||||
@@ -150,4 +150,9 @@ mod tests {
|
|||||||
.is_empty()
|
.is_empty()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn input_slot_is_named_series() {
|
||||||
|
assert_eq!(Sma::builder().schema().inputs[0].name, "series");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ impl Sub {
|
|||||||
"Sub",
|
"Sub",
|
||||||
NodeSchema {
|
NodeSchema {
|
||||||
inputs: vec![
|
inputs: vec![
|
||||||
PortSpec { kind: ScalarKind::F64, firing: Firing::Any },
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "lhs".into() },
|
||||||
PortSpec { kind: ScalarKind::F64, firing: Firing::Any },
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "rhs".into() },
|
||||||
],
|
],
|
||||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||||
params: vec![],
|
params: vec![],
|
||||||
@@ -82,4 +82,11 @@ mod tests {
|
|||||||
inputs[1].push(Scalar::F64(4.0)).unwrap();
|
inputs[1].push(Scalar::F64(4.0)).unwrap();
|
||||||
assert_eq!(sub.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::F64(6.0)].as_slice()));
|
assert_eq!(sub.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::F64(6.0)].as_slice()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -294,6 +294,18 @@ primitive returns its recipe's schema; a composite *derives* it from the interio
|
|||||||
role kinds in, re-exported field kinds out, aggregated params), so "every node has a
|
role kinds in, re-exported field kinds out, aggregated params), so "every node has a
|
||||||
signature in the blueprint" holds for composites too.
|
signature in the blueprint" holds for composites too.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
### C9 — Fractal, acyclic composition
|
### C9 — Fractal, acyclic composition
|
||||||
**Guarantee.** A composite is itself a `Node` that wires a sub-graph and exposes
|
**Guarantee.** A composite is itself a `Node` that wires a sub-graph and exposes
|
||||||
one output; signal, combined signal, and (with execution) strategy are all the
|
one output; signal, combined signal, and (with execution) strategy are all the
|
||||||
|
|||||||
Reference in New Issue
Block a user