Compare commits
8 Commits
7cc3ce0d9e
...
05e9a00afc
| Author | SHA1 | Date | |
|---|---|---|---|
| 05e9a00afc | |||
| 120d116982 | |||
| 938397295d | |||
| 98342246f6 | |||
| fa7453dd9f | |||
| 8dbca82756 | |||
| 73ad87b08a | |||
| 7943b123ae |
@@ -48,7 +48,7 @@ Node types and their ports: aura graph introspect --vocabulary | --node <T>"#;
|
|||||||
/// values are the typed `Scalar` form (`{"I64":2}`); `kind` the capitalized
|
/// values are the typed `Scalar` form (`{"I64":2}`); `kind` the capitalized
|
||||||
/// `ScalarKind` form (`"F64"`) — both the #155 representations.
|
/// `ScalarKind` form (`"F64"`) — both the #155 representations.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(tag = "op", rename_all = "lowercase")]
|
#[serde(tag = "op", rename_all = "lowercase", deny_unknown_fields)]
|
||||||
enum OpDoc {
|
enum OpDoc {
|
||||||
Source { role: String, kind: ScalarKind },
|
Source { role: String, kind: ScalarKind },
|
||||||
Input { role: String },
|
Input { role: String },
|
||||||
@@ -285,9 +285,14 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd, env: &aura_runner::project
|
|||||||
}
|
}
|
||||||
} else if cmd.folds {
|
} else if cmd.folds {
|
||||||
// The fold vocabulary binds at graph-declared taps (C27), so the
|
// The fold vocabulary binds at graph-declared taps (C27), so the
|
||||||
// graph introspect namespace is its discovery surface (#315).
|
// graph introspect namespace is its discovery surface (#315). #332:
|
||||||
for f in aura_std::fold_vocabulary() {
|
// this renders the fold-REGISTRY roster — the same roster `aura run
|
||||||
println!("{:<7} binds {:<9} out {:<9} — {}", f.id, f.binds, f.out, f.doc);
|
// --tap TAP=FOLD` resolves labels against (aura-runner's layered
|
||||||
|
// `FoldRegistry`) — not the aura-std `FoldKind` table: labels here
|
||||||
|
// must be exactly what `--tap` accepts (lowercase), including the
|
||||||
|
// `record` entry that has no `FoldKind` counterpart.
|
||||||
|
for (label, doc) in aura_runner::FoldRegistry::core().roster() {
|
||||||
|
println!("{label:<7} — {doc}");
|
||||||
}
|
}
|
||||||
} else if let Some(type_id) = cmd.node.as_deref() {
|
} else if let Some(type_id) = cmd.node.as_deref() {
|
||||||
match introspect_node(type_id, env) {
|
match introspect_node(type_id, env) {
|
||||||
|
|||||||
@@ -354,6 +354,21 @@ fn graph_build_bad_stdin_content_is_runtime_exit_1() {
|
|||||||
assert!(stderr.contains("Nope"), "still names the cause: {stderr}");
|
assert!(stderr.contains("Nope"), "still names the cause: {stderr}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Property (#326): an op-list element carrying an unknown/typo'd key (e.g.
|
||||||
|
/// `params` where the schema expects `bind`) is refused at parse — not
|
||||||
|
/// silently dropped. Previously the unrecognized key vanished, the field it
|
||||||
|
/// meant to set kept its default, and the wrong graph built with zero
|
||||||
|
/// signal. Same content-fault family as `graph_build_bad_stdin_content_is_
|
||||||
|
/// runtime_exit_1` (both fire from the same top-level deserialize), so the
|
||||||
|
/// exit class matches: exit 1, stderr names the offending key.
|
||||||
|
#[test]
|
||||||
|
fn graph_build_rejects_an_unknown_op_field() {
|
||||||
|
let doc = r#"[{"op":"add","type":"Const","params":{"value":{"F64":1.0}}}]"#;
|
||||||
|
let (stderr, code) = run_code(&["graph", "build"], doc);
|
||||||
|
assert_eq!(code, Some(1), "unknown op field is a content fault -> exit 1; stderr: {stderr}");
|
||||||
|
assert!(stderr.contains("params"), "names the unknown key: {stderr}");
|
||||||
|
}
|
||||||
|
|
||||||
/// Property (exit-code partition, #175 iteration 2): within the SAME `graph
|
/// Property (exit-code partition, #175 iteration 2): within the SAME `graph
|
||||||
/// introspect` subcommand, an invalid `--node <T>` flag VALUE is a USAGE error (a
|
/// introspect` subcommand, an invalid `--node <T>` flag VALUE is a USAGE error (a
|
||||||
/// command-line fault the user must fix in argv) — exit 2 — distinct from bad stdin
|
/// command-line fault the user must fix in argv) — exit 2 — distinct from bad stdin
|
||||||
|
|||||||
@@ -103,18 +103,28 @@ fn graph_introspect_node_head_line_carries_the_meaning() {
|
|||||||
assert!(head.contains("moving average"), "head line carries the meaning: {out}");
|
assert!(head.contains("moving average"), "head line carries the meaning: {out}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// #315: the fold vocabulary is introspectable from the binary — name, bind
|
/// #332: `--folds` renders the fold-REGISTRY roster (the same roster
|
||||||
/// rule, output kind, and meaning per fold (it lived only in source comments).
|
/// `aura run --tap TAP=FOLD` resolves labels against), not the aura-std
|
||||||
|
/// `FoldKind` table — lowercase, parseable labels, including the `record`
|
||||||
|
/// entry that has no `FoldKind` counterpart. Previously this surface printed
|
||||||
|
/// capitalized `FoldKind` ids (`Mean`) and omitted `record`, so the help
|
||||||
|
/// text's own discovery surface described input `--tap` refused.
|
||||||
#[test]
|
#[test]
|
||||||
fn graph_introspect_folds_lists_the_fold_vocabulary() {
|
fn graph_introspect_folds_lists_the_fold_registry_roster() {
|
||||||
let (out, err, ok) = run(&["graph", "introspect", "--folds"]);
|
let (out, err, ok) = run(&["graph", "introspect", "--folds"]);
|
||||||
assert!(ok, "folds listing failed: {err}");
|
assert!(ok, "folds listing failed: {err}");
|
||||||
let lines: Vec<&str> = out.lines().filter(|l| !l.trim().is_empty()).collect();
|
let lines: Vec<&str> = out.lines().filter(|l| !l.trim().is_empty()).collect();
|
||||||
assert_eq!(lines.len(), 7, "one line per fold kind: {out}");
|
assert_eq!(lines.len(), 8, "one line per registry entry (record + 7 folds): {out}");
|
||||||
let mean = lines.iter().find(|l| l.starts_with("Mean")).expect("Mean row");
|
assert!(
|
||||||
assert!(mean.contains("f64") && mean.contains("mean"), "Mean row rules + meaning: {mean}");
|
!out.split_whitespace().any(|w| w == "Mean"),
|
||||||
let count = lines.iter().find(|l| l.starts_with("Count")).expect("Count row");
|
"no capitalized FoldKind id leaks through: {out}"
|
||||||
assert!(count.contains("i64") && count.contains("any"), "Count row rules: {count}");
|
);
|
||||||
|
let record = lines.iter().find(|l| l.starts_with("record ")).expect("record row");
|
||||||
|
assert!(record.contains("series"), "record row meaning: {record}");
|
||||||
|
let mean = lines.iter().find(|l| l.starts_with("mean ")).expect("mean row");
|
||||||
|
assert!(mean.contains("f64") && mean.contains("mean"), "mean row rules + meaning: {mean}");
|
||||||
|
let count = lines.iter().find(|l| l.starts_with("count ")).expect("count row");
|
||||||
|
assert!(count.contains("i64") && count.contains("any"), "count row rules: {count}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// #315: the metric roster carries each metric's one-line meaning beside its
|
/// #315: the metric roster carries each metric's one-line meaning beside its
|
||||||
|
|||||||
@@ -996,7 +996,7 @@ fn campaign_introspect_unwired_reports_the_spec_example_slots() {
|
|||||||
write_doc(&dir, "draft.campaign.json", draft);
|
write_doc(&dir, "draft.campaign.json", draft);
|
||||||
let (out, code) = run_code_in(&dir, &["campaign", "introspect", "--unwired", "draft.campaign.json"]);
|
let (out, code) = run_code_in(&dir, &["campaign", "introspect", "--unwired", "draft.campaign.json"]);
|
||||||
assert_eq!(code, Some(0));
|
assert_eq!(code, Some(0));
|
||||||
assert!(out.contains("open slot: process.ref (required, content id of a process document)"));
|
assert!(out.contains("open slot: process.ref (required, { content_id }: content id of a process document)"));
|
||||||
assert!(out.contains("open slot: strategies[0].axes.slow (axis declared empty — a P1 axis is a non-empty finite set)"));
|
assert!(out.contains("open slot: strategies[0].axes.slow (axis declared empty — a P1 axis is a non-empty finite set)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1022,7 +1022,7 @@ fn campaign_introspect_unwired_reports_placeholder_refs() {
|
|||||||
"strategy `ref: null` placeholder not enumerated: {out}"
|
"strategy `ref: null` placeholder not enumerated: {out}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
out.contains("open slot: process.ref (required, content id of a process document)"),
|
out.contains("open slot: process.ref (required, { content_id }: content id of a process document)"),
|
||||||
"process `ref: {{}}` placeholder not enumerated: {out}"
|
"process `ref: {{}}` placeholder not enumerated: {out}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -322,13 +322,17 @@ fn run_tap_selector_refuses_an_unknown_label_with_the_roster() {
|
|||||||
assert!(!cwd.join("runs").exists(), "a refused run must not write the store");
|
assert!(!cwd.join("runs").exists(), "a refused run must not write the store");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// #310: a selection naming a tap the blueprint does not declare is
|
/// #310/#333: a selection naming a tap the blueprint does not declare is
|
||||||
/// refused typed (UndeclaredTap), before any store write.
|
/// refused typed (UndeclaredTap), before any store write, and the refusal
|
||||||
|
/// enumerates the blueprint's declared taps (the same way the unknown-fold
|
||||||
|
/// refusal enumerates the fold-registry roster) — on the two-tap fixture,
|
||||||
|
/// both `fast_tap` and `slow_tap` must be named so the author does not have
|
||||||
|
/// to reopen the blueprint JSON.
|
||||||
#[test]
|
#[test]
|
||||||
fn run_tap_selector_refuses_an_undeclared_tap() {
|
fn run_tap_selector_refuses_an_undeclared_tap() {
|
||||||
let cwd = temp_cwd("tap-selector-undeclared");
|
let cwd = temp_cwd("tap-selector-undeclared");
|
||||||
let bp_path = cwd.join("tapped_r_sma.json");
|
let bp_path = cwd.join("two_tap.json");
|
||||||
std::fs::write(&bp_path, tap_blueprint_json()).expect("write tapped blueprint");
|
std::fs::write(&bp_path, two_tap_blueprint_json()).expect("write two-tap blueprint");
|
||||||
let out = Command::new(BIN)
|
let out = Command::new(BIN)
|
||||||
.args(["run", bp_path.to_str().expect("utf-8 path"), "--tap", "nope=mean"])
|
.args(["run", bp_path.to_str().expect("utf-8 path"), "--tap", "nope=mean"])
|
||||||
.current_dir(&cwd)
|
.current_dir(&cwd)
|
||||||
@@ -337,9 +341,61 @@ fn run_tap_selector_refuses_an_undeclared_tap() {
|
|||||||
assert!(!out.status.success(), "an undeclared tap must refuse");
|
assert!(!out.status.success(), "an undeclared tap must refuse");
|
||||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||||
assert!(stderr.contains("nope"), "refusal must name the offending tap: {stderr}");
|
assert!(stderr.contains("nope"), "refusal must name the offending tap: {stderr}");
|
||||||
|
assert!(
|
||||||
|
stderr.contains("fast_tap") && stderr.contains("slow_tap"),
|
||||||
|
"refusal must enumerate the blueprint's declared taps: {stderr}"
|
||||||
|
);
|
||||||
assert!(!cwd.join("runs").exists(), "a refused run must not write the store");
|
assert!(!cwd.join("runs").exists(), "a refused run must not write the store");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// #334: an explicit `--tap` plan that leaves a declared tap unbound emits
|
||||||
|
/// the C14 benign skipped-tap note on stderr, per unbound tap, naming it —
|
||||||
|
/// exit code stays 0 (this is a note, not a refusal). `fast_tap` (subscribed)
|
||||||
|
/// gets no such note; `slow_tap` (left unlisted) does.
|
||||||
|
#[test]
|
||||||
|
fn run_tap_selector_notes_unbound_declared_taps_on_stderr() {
|
||||||
|
let cwd = temp_cwd("tap-selector-unbound-note");
|
||||||
|
let bp_path = cwd.join("two_tap.json");
|
||||||
|
std::fs::write(&bp_path, two_tap_blueprint_json()).expect("write two-tap blueprint");
|
||||||
|
|
||||||
|
let out = Command::new(BIN)
|
||||||
|
.args(["run", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=mean"])
|
||||||
|
.current_dir(&cwd)
|
||||||
|
.output()
|
||||||
|
.expect("spawn aura run");
|
||||||
|
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||||
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||||
|
assert!(
|
||||||
|
stderr.contains("aura: note: declared tap \"slow_tap\" unbound this run"),
|
||||||
|
"stderr must note the unbound declared tap: {stderr}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!stderr.contains("\"fast_tap\" unbound"),
|
||||||
|
"the subscribed tap must not be noted as unbound: {stderr}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// #334: the record-all default (no `--tap` flag) leaves no declared tap
|
||||||
|
/// unbound — nothing is skipped, so the note must never appear.
|
||||||
|
#[test]
|
||||||
|
fn run_with_no_tap_flag_emits_no_unbound_note() {
|
||||||
|
let cwd = temp_cwd("tap-selector-default-no-note");
|
||||||
|
let bp_path = cwd.join("two_tap.json");
|
||||||
|
std::fs::write(&bp_path, two_tap_blueprint_json()).expect("write two-tap blueprint");
|
||||||
|
|
||||||
|
let out = Command::new(BIN)
|
||||||
|
.args(["run", bp_path.to_str().expect("utf-8 path")])
|
||||||
|
.current_dir(&cwd)
|
||||||
|
.output()
|
||||||
|
.expect("spawn aura run");
|
||||||
|
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||||
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||||
|
assert!(
|
||||||
|
!stderr.contains("unbound this run"),
|
||||||
|
"the record-all default must never note a skipped tap: {stderr}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// #310: a malformed or duplicate `--tap` is a usage error (exit 2)
|
/// #310: a malformed or duplicate `--tap` is a usage error (exit 2)
|
||||||
/// before anything runs.
|
/// before anything runs.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1102,7 +1102,7 @@ pub fn slot_kind_label(kind: SlotKind) -> &'static str {
|
|||||||
SlotKind::Strings => "list of strings",
|
SlotKind::Strings => "list of strings",
|
||||||
SlotKind::Windows => "list of { from_ms, to_ms }",
|
SlotKind::Windows => "list of { from_ms, to_ms }",
|
||||||
SlotKind::Ref => "one of { content_id } | { identity_id }",
|
SlotKind::Ref => "one of { content_id } | { identity_id }",
|
||||||
SlotKind::ContentRef => "content id of a process document",
|
SlotKind::ContentRef => "{ content_id }: content id of a process document",
|
||||||
SlotKind::Axes => "map: param name -> { kind, values }",
|
SlotKind::Axes => "map: param name -> { kind, values }",
|
||||||
SlotKind::EmitKinds => "list of: family_table | selection_report",
|
SlotKind::EmitKinds => "list of: family_table | selection_report",
|
||||||
SlotKind::TapKinds => "list of: equity | exposure | r_equity | net_r_equity",
|
SlotKind::TapKinds => "list of: equity | exposure | r_equity | net_r_equity",
|
||||||
@@ -1301,8 +1301,10 @@ pub fn open_slots_campaign(text: &str) -> Result<Vec<OpenSlot>, DocError> {
|
|||||||
}
|
}
|
||||||
if ref_slot_is_open(v.get("process").and_then(|p| p.get("ref"))) {
|
if ref_slot_is_open(v.get("process").and_then(|p| p.get("ref"))) {
|
||||||
// deliberately narrower than the strategy-ref hint: validate_campaign
|
// deliberately narrower than the strategy-ref hint: validate_campaign
|
||||||
// refuses an identity_id process ref, so the guide must not offer it
|
// refuses an identity_id process ref, so the guide must not offer it.
|
||||||
slots.push(open("process.ref", "required, content id of a process document"));
|
// The tagged { content_id } shape is stated explicitly (#329) so the
|
||||||
|
// hint cannot be misread as accepting a bare id string.
|
||||||
|
slots.push(open("process.ref", "required, { content_id }: content id of a process document"));
|
||||||
}
|
}
|
||||||
if v.get("seed").and_then(|s| s.as_u64()).is_none() {
|
if v.get("seed").and_then(|s| s.as_u64()).is_none() {
|
||||||
slots.push(open("seed", "required, non-negative integer"));
|
slots.push(open("seed", "required, non-negative integer"));
|
||||||
@@ -2374,6 +2376,14 @@ mod tests {
|
|||||||
label.contains("content"),
|
label.contains("content"),
|
||||||
"process_ref describe must still name the content-id form: {label}"
|
"process_ref describe must still name the content-id form: {label}"
|
||||||
);
|
);
|
||||||
|
// #329: the label must state the TAGGED shape ({ content_id: ... }) the
|
||||||
|
// parser actually requires, not read like a bare id string — mirroring
|
||||||
|
// the `{ content_id } | { identity_id }` bracket notation std::strategy
|
||||||
|
// already uses, narrowed to the one key process.ref accepts.
|
||||||
|
assert!(
|
||||||
|
label.contains("{ content_id }"),
|
||||||
|
"process_ref describe must state the tagged {{ content_id }} shape, not a bare id: {label}"
|
||||||
|
);
|
||||||
|
|
||||||
let strategy = describe_block("std::strategy").expect("strategy describable");
|
let strategy = describe_block("std::strategy").expect("strategy describable");
|
||||||
let strat_ref =
|
let strat_ref =
|
||||||
@@ -2430,7 +2440,7 @@ mod tests {
|
|||||||
"presentation": { "persist_taps": [], "emit": [] } }"#;
|
"presentation": { "persist_taps": [], "emit": [] } }"#;
|
||||||
let slots = open_slots_campaign(draft).unwrap();
|
let slots = open_slots_campaign(draft).unwrap();
|
||||||
assert!(slots.iter().any(|s| s.path == "process.ref"
|
assert!(slots.iter().any(|s| s.path == "process.ref"
|
||||||
&& s.hint == "required, content id of a process document"));
|
&& s.hint == "required, { content_id }: content id of a process document"));
|
||||||
assert!(slots.iter().any(|s| s.path == "strategies[0].axes.slow"
|
assert!(slots.iter().any(|s| s.path == "strategies[0].axes.slow"
|
||||||
&& s.hint == "axis declared empty — a P1 axis is a non-empty finite set"));
|
&& s.hint == "axis declared empty — a P1 axis is a non-empty finite set"));
|
||||||
|
|
||||||
@@ -2466,7 +2476,7 @@ mod tests {
|
|||||||
// ...and the `{}` process ref reports the narrower content-id-only hint.
|
// ...and the `{}` process ref reports the narrower content-id-only hint.
|
||||||
assert!(
|
assert!(
|
||||||
slots.iter().any(|s| s.path == "process.ref"
|
slots.iter().any(|s| s.path == "process.ref"
|
||||||
&& s.hint == "required, content id of a process document"),
|
&& s.hint == "required, { content_id }: content id of a process document"),
|
||||||
"process `ref: {{}}` placeholder not reported as an open slot: {slots:?}"
|
"process `ref: {{}}` placeholder not reported as an open slot: {slots:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1728,9 +1728,14 @@ mod tests {
|
|||||||
Ok(_) => panic!("unknown tap must be refused"),
|
Ok(_) => panic!("unknown tap must be refused"),
|
||||||
};
|
};
|
||||||
assert!(
|
assert!(
|
||||||
matches!(err, TapPlanError::UnknownTap { ref name } if name == "no_such_tap"),
|
matches!(err, TapPlanError::UnknownTap { ref name, .. } if name == "no_such_tap"),
|
||||||
"{err:?}"
|
"{err:?}"
|
||||||
);
|
);
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(
|
||||||
|
msg.contains("fast_tap"),
|
||||||
|
"unknown-tap refusal enumerates the declared taps (fixture declares only fast_tap): {msg}"
|
||||||
|
);
|
||||||
|
|
||||||
// Unknown label — the refusal enumerates the roster.
|
// Unknown label — the refusal enumerates the roster.
|
||||||
let mut flat2 = tapped_r_sma().compile_with_params(&[]).expect("tapped r_sma compiles");
|
let mut flat2 = tapped_r_sma().compile_with_params(&[]).expect("tapped r_sma compiles");
|
||||||
|
|||||||
@@ -161,17 +161,21 @@ impl FoldRegistry {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
for (label, doc, fold) in [
|
for (label, doc, fold) in [
|
||||||
("count", "number of warm rows (any kind; i64 row)", FoldKind::Count),
|
("count", "number of warm rows (any kind; i64 row); one row at the last warm ts", FoldKind::Count),
|
||||||
("sum", "sum of the series (f64 taps; f64 row)", FoldKind::Sum),
|
("sum", "sum of the series (f64 taps; f64 row); one row at the last warm ts", FoldKind::Sum),
|
||||||
("mean", "arithmetic mean of the series (f64 taps; f64 row)", FoldKind::Mean),
|
(
|
||||||
("min", "minimum of the series (f64 taps; f64 row)", FoldKind::Min),
|
"mean",
|
||||||
("max", "maximum of the series (f64 taps; f64 row)", FoldKind::Max),
|
"arithmetic mean of the series (f64 taps; f64 row); one row at the last warm ts",
|
||||||
|
FoldKind::Mean,
|
||||||
|
),
|
||||||
|
("min", "minimum of the series (f64 taps; f64 row); one row at the last warm ts", FoldKind::Min),
|
||||||
|
("max", "maximum of the series (f64 taps; f64 row); one row at the last warm ts", FoldKind::Max),
|
||||||
(
|
(
|
||||||
"first",
|
"first",
|
||||||
"first warm value, at its own timestamp (any kind; kind-preserving row)",
|
"first warm value, at its own timestamp (any kind; kind-preserving row)",
|
||||||
FoldKind::First,
|
FoldKind::First,
|
||||||
),
|
),
|
||||||
("last", "last warm value (any kind; kind-preserving row)", FoldKind::Last),
|
("last", "last warm value, at its own timestamp (any kind; kind-preserving row)", FoldKind::Last),
|
||||||
] {
|
] {
|
||||||
r.register(FoldEntry {
|
r.register(FoldEntry {
|
||||||
label,
|
label,
|
||||||
@@ -217,7 +221,7 @@ impl FoldRegistry {
|
|||||||
/// `aura: ` + exit-1 refusal register via `Display`.
|
/// `aura: ` + exit-1 refusal register via `Display`.
|
||||||
pub enum TapPlanError {
|
pub enum TapPlanError {
|
||||||
/// The plan names a tap the blueprint does not declare.
|
/// The plan names a tap the blueprint does not declare.
|
||||||
UnknownTap { name: String },
|
UnknownTap { name: String, declared: Vec<String> },
|
||||||
/// The plan names a label the registry does not carry.
|
/// The plan names a label the registry does not carry.
|
||||||
UnknownLabel { label: String, roster: Vec<&'static str> },
|
UnknownLabel { label: String, roster: Vec<&'static str> },
|
||||||
/// The entry's bind rule rejects the tap's column kind.
|
/// The entry's bind rule rejects the tap's column kind.
|
||||||
@@ -237,8 +241,12 @@ pub enum TapPlanError {
|
|||||||
impl fmt::Display for TapPlanError {
|
impl fmt::Display for TapPlanError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
TapPlanError::UnknownTap { name } => {
|
TapPlanError::UnknownTap { name, declared } => {
|
||||||
write!(f, "the tap plan names '{name}', but the blueprint declares no such tap")
|
write!(
|
||||||
|
f,
|
||||||
|
"the tap plan names '{name}', but the blueprint declares no such tap — declared taps: {}",
|
||||||
|
declared.join(", ")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
TapPlanError::UnknownLabel { label, roster } => {
|
TapPlanError::UnknownLabel { label, roster } => {
|
||||||
write!(f, "unknown fold '{label}' — available: {}", roster.join(", "))
|
write!(f, "unknown fold '{label}' — available: {}", roster.join(", "))
|
||||||
@@ -402,7 +410,10 @@ pub fn bind_tap_plan(
|
|||||||
// Unknown-tap guard: every plan name must be declared.
|
// Unknown-tap guard: every plan name must be declared.
|
||||||
for name in plan.by_name.keys() {
|
for name in plan.by_name.keys() {
|
||||||
if !declared_taps.iter().any(|t| &t.name == name) {
|
if !declared_taps.iter().any(|t| &t.name == name) {
|
||||||
return Err(TapPlanError::UnknownTap { name: name.clone() });
|
return Err(TapPlanError::UnknownTap {
|
||||||
|
name: name.clone(),
|
||||||
|
declared: declared_taps.iter().map(|t| t.name.clone()).collect(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,7 +433,18 @@ pub fn bind_tap_plan(
|
|||||||
Some((label, params)) => {
|
Some((label, params)) => {
|
||||||
Resolved::Named { label: label.clone(), params: params.clone() }
|
Resolved::Named { label: label.clone(), params: params.clone() }
|
||||||
}
|
}
|
||||||
None => continue, // unbound, inert (C27)
|
// Unbound, inert (C27) — but only reachable when `plan` carries
|
||||||
|
// no default (an EXPLICIT plan, e.g. from `--tap`): record-all
|
||||||
|
// (`default_named` = `Some(("record", …))`) always resolves the
|
||||||
|
// Some arm above, so this arm never fires under record-all and
|
||||||
|
// the note is exactly the C14 benign skipped-tap class (#334).
|
||||||
|
// Emitted here (aura-runner), beside the pre-existing
|
||||||
|
// runner-side `eprintln!` registers in this module/`member.rs`/
|
||||||
|
// `measure.rs` — the runner→CLI print migration is #297.
|
||||||
|
None => {
|
||||||
|
eprintln!("aura: note: declared tap \"{}\" unbound this run", tap.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if let Resolved::Named { label, params } = &sub {
|
if let Resolved::Named { label, params } = &sub {
|
||||||
@@ -504,6 +526,64 @@ mod tests {
|
|||||||
assert!(r.roster().iter().all(|(_, doc)| !doc.is_empty()), "every entry documents itself");
|
assert!(r.roster().iter().all(|(_, doc)| !doc.is_empty()), "every entry documents itself");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// C29 entry seam for the registry roster: every entry ships a gate-clean
|
||||||
|
/// one-line meaning, and the bind/output prose inside it matches the
|
||||||
|
/// entry's executable rules — the drift-pin the retired aura-std
|
||||||
|
/// `fold_vocabulary` table carried, moved here with the surface (#332).
|
||||||
|
#[test]
|
||||||
|
fn roster_docs_pass_the_doc_gate_and_match_the_executable_rules() {
|
||||||
|
let r = FoldRegistry::core();
|
||||||
|
for entry in r.entries.values() {
|
||||||
|
aura_core::doc_gate(entry.label, entry.doc)
|
||||||
|
.unwrap_or_else(|f| panic!("fold {} doc fails the gate: {f:?}", entry.label));
|
||||||
|
if entry.doc.contains("f64 taps") {
|
||||||
|
assert!(
|
||||||
|
(entry.binds_at)(ScalarKind::F64) && !(entry.binds_at)(ScalarKind::I64),
|
||||||
|
"{} claims f64-only but binds wider",
|
||||||
|
entry.label
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
assert!(
|
||||||
|
entry.doc.contains("any kind"),
|
||||||
|
"{}: bind prose must be 'f64 taps' or 'any kind': {}",
|
||||||
|
entry.label,
|
||||||
|
entry.doc
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
(entry.binds_at)(ScalarKind::I64) && (entry.binds_at)(ScalarKind::Bool),
|
||||||
|
"{} claims any-kind but refuses a kind",
|
||||||
|
entry.label
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if entry.doc.contains("i64 row") {
|
||||||
|
assert!(
|
||||||
|
matches!((entry.output)(ScalarKind::F64), FoldOutput::Row(ScalarKind::I64)),
|
||||||
|
"{} claims an i64 row but outputs otherwise",
|
||||||
|
entry.label
|
||||||
|
);
|
||||||
|
} else if entry.doc.contains("f64 row") {
|
||||||
|
assert!(
|
||||||
|
matches!((entry.output)(ScalarKind::F64), FoldOutput::Row(ScalarKind::F64)),
|
||||||
|
"{} claims an f64 row but outputs otherwise",
|
||||||
|
entry.label
|
||||||
|
);
|
||||||
|
} else if entry.doc.contains("kind-preserving row") {
|
||||||
|
assert!(
|
||||||
|
matches!((entry.output)(ScalarKind::F64), FoldOutput::Row(ScalarKind::F64))
|
||||||
|
&& matches!((entry.output)(ScalarKind::I64), FoldOutput::Row(ScalarKind::I64)),
|
||||||
|
"{} claims kind-preserving but outputs otherwise",
|
||||||
|
entry.label
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
assert!(
|
||||||
|
matches!((entry.output)(ScalarKind::F64), FoldOutput::Series),
|
||||||
|
"{}: doc names no row kind, so it must be the series entry",
|
||||||
|
entry.label
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unknown_label_refusal_enumerates_the_roster() {
|
fn unknown_label_refusal_enumerates_the_roster() {
|
||||||
let r = FoldRegistry::core();
|
let r = FoldRegistry::core();
|
||||||
|
|||||||
@@ -74,6 +74,6 @@ pub use sma::Sma;
|
|||||||
pub use sqrt::Sqrt;
|
pub use sqrt::Sqrt;
|
||||||
pub use sub::Sub;
|
pub use sub::Sub;
|
||||||
pub use tap_cell::newest_cell;
|
pub use tap_cell::newest_cell;
|
||||||
pub use tap_fold::{fold_binds_at, fold_output_kind, fold_vocabulary, FoldKind, FoldSchema, TapFold};
|
pub use tap_fold::{fold_binds_at, fold_output_kind, FoldKind, TapFold};
|
||||||
pub use tap_live::TapLive;
|
pub use tap_live::TapLive;
|
||||||
pub use when::When;
|
pub use when::When;
|
||||||
|
|||||||
@@ -45,32 +45,6 @@ pub fn fold_output_kind(fold: FoldKind, kind: ScalarKind) -> ScalarKind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One fold entry's introspection row (C29): id, bind/output rules in prose,
|
|
||||||
/// one-line meaning. `fold_vocabulary` is the binary's discovery surface for
|
|
||||||
/// [`FoldKind`] (`aura graph introspect --folds`, #315); the unit tests pin
|
|
||||||
/// each row's rule prose against [`fold_binds_at`] / [`fold_output_kind`] so
|
|
||||||
/// the table cannot drift from the executable rules.
|
|
||||||
pub struct FoldSchema {
|
|
||||||
pub id: &'static str,
|
|
||||||
pub kind: FoldKind,
|
|
||||||
pub binds: &'static str,
|
|
||||||
pub out: &'static str,
|
|
||||||
pub doc: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The closed fold vocabulary as introspection rows, in [`FoldKind`] order.
|
|
||||||
pub fn fold_vocabulary() -> &'static [FoldSchema] {
|
|
||||||
&[
|
|
||||||
FoldSchema { id: "Count", kind: FoldKind::Count, binds: "any tap", out: "i64", doc: "number of rows the tap recorded" },
|
|
||||||
FoldSchema { id: "Sum", kind: FoldKind::Sum, binds: "f64 taps", out: "f64", doc: "sum of the tap's recorded values" },
|
|
||||||
FoldSchema { id: "Mean", kind: FoldKind::Mean, binds: "f64 taps", out: "f64", doc: "arithmetic mean of the tap's recorded values" },
|
|
||||||
FoldSchema { id: "Min", kind: FoldKind::Min, binds: "f64 taps", out: "f64", doc: "smallest recorded value" },
|
|
||||||
FoldSchema { id: "Max", kind: FoldKind::Max, binds: "f64 taps", out: "f64", doc: "largest recorded value" },
|
|
||||||
FoldSchema { id: "First", kind: FoldKind::First, binds: "any tap", out: "tap kind", doc: "earliest recorded value, kind-preserving" },
|
|
||||||
FoldSchema { id: "Last", kind: FoldKind::Last, binds: "any tap", out: "tap kind", doc: "latest recorded value, kind-preserving" },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Owned accumulator for every fold kind at once (a handful of words; keeping
|
/// Owned accumulator for every fold kind at once (a handful of words; keeping
|
||||||
/// it total avoids a per-kind state enum). Sequential `sum += v` matches a
|
/// it total avoids a per-kind state enum). Sequential `sum += v` matches a
|
||||||
/// slice-driven mean's operation order, so streamed and slice `Mean` agree
|
/// slice-driven mean's operation order, so streamed and slice `Mean` agree
|
||||||
@@ -190,47 +164,6 @@ mod tests {
|
|||||||
use aura_core::AnyColumn;
|
use aura_core::AnyColumn;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
|
|
||||||
/// The vocabulary rows are prose renderings of the executable rules —
|
|
||||||
/// pin them against `fold_binds_at` / `fold_output_kind` so the
|
|
||||||
/// introspection surface cannot drift from what the engine enforces.
|
|
||||||
#[test]
|
|
||||||
fn fold_vocabulary_rows_match_the_executable_rules() {
|
|
||||||
let rows = fold_vocabulary();
|
|
||||||
assert_eq!(rows.len(), 7, "one row per FoldKind");
|
|
||||||
for row in rows {
|
|
||||||
let f64_only = fold_binds_at(row.kind, ScalarKind::F64)
|
|
||||||
&& !fold_binds_at(row.kind, ScalarKind::I64);
|
|
||||||
match row.binds {
|
|
||||||
"f64 taps" => assert!(f64_only, "{} claims f64-only but binds wider", row.id),
|
|
||||||
"any tap" => assert!(
|
|
||||||
fold_binds_at(row.kind, ScalarKind::I64)
|
|
||||||
&& fold_binds_at(row.kind, ScalarKind::Bool),
|
|
||||||
"{} claims any-tap but refuses a kind",
|
|
||||||
row.id
|
|
||||||
),
|
|
||||||
other => panic!("unknown binds prose {other:?} on {}", row.id),
|
|
||||||
}
|
|
||||||
match row.out {
|
|
||||||
"i64" => assert_eq!(fold_output_kind(row.kind, ScalarKind::F64), ScalarKind::I64),
|
|
||||||
"f64" => assert_eq!(fold_output_kind(row.kind, ScalarKind::F64), ScalarKind::F64),
|
|
||||||
"tap kind" => {
|
|
||||||
assert_eq!(fold_output_kind(row.kind, ScalarKind::F64), ScalarKind::F64);
|
|
||||||
assert_eq!(fold_output_kind(row.kind, ScalarKind::I64), ScalarKind::I64);
|
|
||||||
}
|
|
||||||
other => panic!("unknown out prose {other:?} on {}", row.id),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// C29 entry seam: every fold row ships a gate-clean one-line meaning.
|
|
||||||
#[test]
|
|
||||||
fn fold_vocabulary_docs_pass_the_doc_gate() {
|
|
||||||
for row in fold_vocabulary() {
|
|
||||||
aura_core::doc_gate(row.id, row.doc)
|
|
||||||
.unwrap_or_else(|f| panic!("fold {} doc fails the gate: {f:?}", row.id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Drive a fold over an f64 series and return the emitted rows.
|
/// Drive a fold over an f64 series and return the emitted rows.
|
||||||
fn run_f64_fold(fold: FoldKind, values: &[f64]) -> Vec<(Timestamp, Vec<Scalar>)> {
|
fn run_f64_fold(fold: FoldKind, values: &[f64]) -> Vec<(Timestamp, Vec<Scalar>)> {
|
||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ impl CarryCost {
|
|||||||
"CarryCost",
|
"CarryCost",
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![ParamSpec { name: "carry_per_cycle".into(), kind: ScalarKind::F64 }],
|
vec![ParamSpec { name: "carry_per_cycle".into(), kind: ScalarKind::F64 }],
|
||||||
|
"cost-model node: cost accrued per held cycle (param carry_per_cycle)",
|
||||||
|p| Box::new(CarryCost::new(p[0].f64())),
|
|p| Box::new(CarryCost::new(p[0].f64())),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ impl ConstantCost {
|
|||||||
"ConstantCost",
|
"ConstantCost",
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![ParamSpec { name: "cost_per_trade".into(), kind: ScalarKind::F64 }],
|
vec![ParamSpec { name: "cost_per_trade".into(), kind: ScalarKind::F64 }],
|
||||||
|
"cost-model node: fixed cost per trade (param cost_per_trade), charged at close",
|
||||||
|p| Box::new(ConstantCost::new(p[0].f64())),
|
|p| Box::new(ConstantCost::new(p[0].f64())),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,26 +214,20 @@ impl<F: CostNode> Node for CostRunner<F> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Assemble a cost-node `PrimitiveBuilder`: the 4 geometry inputs ++ the factor's
|
/// Assemble a cost-node `PrimitiveBuilder`: the 4 geometry inputs ++ the factor's
|
||||||
/// extra inputs, the standard 3-field cost output, the given params, and a build
|
/// extra inputs, the standard 3-field cost output, the given params, a
|
||||||
/// closure. The single home for the cost-node schema shape.
|
/// factor-specific one-line `doc` (C29 — each cost node names its own charge
|
||||||
|
/// basis rather than sharing one generic sentence, #330), and a build closure.
|
||||||
|
/// The single home for the cost-node schema shape.
|
||||||
pub fn cost_node_builder(
|
pub fn cost_node_builder(
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
extra_inputs: Vec<PortSpec>,
|
extra_inputs: Vec<PortSpec>,
|
||||||
params: Vec<ParamSpec>,
|
params: Vec<ParamSpec>,
|
||||||
|
doc: &'static str,
|
||||||
build: impl Fn(&[Cell]) -> Box<dyn Node> + 'static,
|
build: impl Fn(&[Cell]) -> Box<dyn Node> + 'static,
|
||||||
) -> PrimitiveBuilder {
|
) -> PrimitiveBuilder {
|
||||||
let mut inputs = geometry_input_ports();
|
let mut inputs = geometry_input_ports();
|
||||||
inputs.extend(extra_inputs);
|
inputs.extend(extra_inputs);
|
||||||
PrimitiveBuilder::new(
|
PrimitiveBuilder::new(name, NodeSchema { inputs, output: cost_output_fields(), params, doc }, build)
|
||||||
name,
|
|
||||||
NodeSchema {
|
|
||||||
inputs,
|
|
||||||
output: cost_output_fields(),
|
|
||||||
params,
|
|
||||||
doc: "cost-model node: charges its cost in R from position geometry, at close or accrued per held cycle",
|
|
||||||
},
|
|
||||||
build,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -501,6 +495,7 @@ mod tests {
|
|||||||
"Probe",
|
"Probe",
|
||||||
extra.clone(),
|
extra.clone(),
|
||||||
params.clone(),
|
params.clone(),
|
||||||
|
"cost-model node: probe factor for the builder-assembly test",
|
||||||
|_p| -> Box<dyn Node> { Box::new(CostRunner::new(StubCost(0.0))) },
|
|_p| -> Box<dyn Node> { Box::new(CostRunner::new(StubCost(0.0))) },
|
||||||
);
|
);
|
||||||
let schema = builder.schema();
|
let schema = builder.schema();
|
||||||
@@ -537,6 +532,21 @@ mod tests {
|
|||||||
assert_eq!(a, "volatility");
|
assert_eq!(a, "volatility");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cost_node_descriptions_are_pairwise_distinct() {
|
||||||
|
// C29: `aura graph introspect --vocabulary` must be able to tell the
|
||||||
|
// three cost nodes apart by their charge basis (flat-per-trade vs
|
||||||
|
// per-held-cycle-accrual vs volatility-scaled), not read one
|
||||||
|
// identical generic sentence three times (#330).
|
||||||
|
use crate::{CarryCost, ConstantCost, VolSlippageCost};
|
||||||
|
let constant_doc = ConstantCost::builder().schema().doc;
|
||||||
|
let carry_doc = CarryCost::builder().schema().doc;
|
||||||
|
let vol_slippage_doc = VolSlippageCost::builder().schema().doc;
|
||||||
|
assert_ne!(constant_doc, carry_doc, "ConstantCost vs CarryCost doc must differ");
|
||||||
|
assert_ne!(constant_doc, vol_slippage_doc, "ConstantCost vs VolSlippageCost doc must differ");
|
||||||
|
assert_ne!(carry_doc, vol_slippage_doc, "CarryCost vs VolSlippageCost doc must differ");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn producer_and_aggregator_share_the_triple() {
|
fn producer_and_aggregator_share_the_triple() {
|
||||||
// The structural lockstep: producer output and aggregator output/inputs all
|
// The structural lockstep: producer output and aggregator output/inputs all
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ impl VolSlippageCost {
|
|||||||
"VolSlippageCost",
|
"VolSlippageCost",
|
||||||
volatility_input_ports(),
|
volatility_input_ports(),
|
||||||
vec![ParamSpec { name: "slip_vol_mult".into(), kind: ScalarKind::F64 }],
|
vec![ParamSpec { name: "slip_vol_mult".into(), kind: ScalarKind::F64 }],
|
||||||
|
"cost-model node: slippage proportional to volatility (slip_vol_mult × volatility input), charged at close",
|
||||||
|p| Box::new(VolSlippageCost::new(p[0].f64())),
|
|p| Box::new(VolSlippageCost::new(p[0].f64())),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,12 @@ consumer (`aura-runner::tap_recorder::TapRecorder`) holds the trace store's
|
|||||||
streaming writer in-graph — `initialize` opens (deferred acquisition), `eval`
|
streaming writer in-graph — `initialize` opens (deferred acquisition), `eval`
|
||||||
appends `(Timestamp, Cell)` (zero per-cycle heap, #77), `finalize` reports
|
appends `(Timestamp, Cell)` (zero per-cycle heap, #77), `finalize` reports
|
||||||
exactly one terminal outcome; fold consumers (`aura-std::TapFold`) land one
|
exactly one terminal outcome; fold consumers (`aura-std::TapFold`) land one
|
||||||
summary row; live closures run inline (`aura-std::TapLive`). The sweep/reduce
|
summary row, emitted at finalize and stamped with the instant of the last
|
||||||
|
contributing (warm) value — `first` alone pins the first contributing
|
||||||
|
instant; `min`/`max` deliberately do not carry the extremum's timestamp (a
|
||||||
|
whole-window row privileges no interior instant) — ratified as-is, #335;
|
||||||
|
live closures run inline
|
||||||
|
(`aura-std::TapLive`). The sweep/reduce
|
||||||
path never calls `bind_tap`.
|
path never calls `bind_tap`.
|
||||||
|
|
||||||
The chain-pruning benefit — a sweep paying zero for the study wires behind an
|
The chain-pruning benefit — a sweep paying zero for the study wires behind an
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ for whom the binary is the only always-present teacher. Field evidence
|
|||||||
execution semantics and document schemas by CAS forensics — the removed
|
execution semantics and document schemas by CAS forensics — the removed
|
||||||
failure class is exactly that forensic recovery.
|
failure class is exactly that forensic recovery.
|
||||||
|
|
||||||
Rendering of the carried texts on the help/introspection surfaces is #315;
|
Rendering of the carried texts on the help/introspection surfaces shipped
|
||||||
the generated agent bootstrap card is #267; a fold introspection surface
|
with #315; the agent bootstrap card was retired as superseded (#267 — the
|
||||||
is blocked on #310.
|
C29 surfaces themselves carry it); the fold introspection surface shipped
|
||||||
|
with #310 and renders the fold-registry roster — labels exactly as the
|
||||||
|
`--tap` selector accepts them, doc lines from the registry entries (#332).
|
||||||
|
|||||||
+1
-1
@@ -327,7 +327,7 @@ A named recorded stream produced by a recording `sink` — the addressable label
|
|||||||
|
|
||||||
Since C27 (#282) the word also names a second, author-facing sense: a **declared tap** — a `Composite.taps` entry `Tap { name, from: {node, field} }` a hand-authored blueprint declares on an interior output wire, the output-side twin of an `input_role`. It is a pure declaration (no channel endpoint); the harness binds it run-mode-aware (a single `aura run` constructs a recorder at each and persists the series through the trace store; a sweep leaves it unbound and inert). This is an OPEN, per-blueprint author-declared name — distinct from the CLOSED campaign `persist_taps` vocabulary above, which selects among fixed observables of the standard R-harness. Both senses land as `ColumnarTrace`s in the same trace store; the closed vocabulary is what a `campaign document` selects, the declared tap is what a blueprint author names.
|
Since C27 (#282) the word also names a second, author-facing sense: a **declared tap** — a `Composite.taps` entry `Tap { name, from: {node, field} }` a hand-authored blueprint declares on an interior output wire, the output-side twin of an `input_role`. It is a pure declaration (no channel endpoint); the harness binds it run-mode-aware (a single `aura run` constructs a recorder at each and persists the series through the trace store; a sweep leaves it unbound and inert). This is an OPEN, per-blueprint author-declared name — distinct from the CLOSED campaign `persist_taps` vocabulary above, which selects among fixed observables of the standard R-harness. Both senses land as `ColumnarTrace`s in the same trace store; the closed vocabulary is what a `campaign document` selects, the declared tap is what a blueprint author names.
|
||||||
|
|
||||||
Since #283 what CONSUMES a declared tap is itself declared per run by a **tap plan**: a subscription is either `Named { label, params }` — resolved against a layered **fold registry** whose core vocabulary is `record | count | sum | mean | min | max | first | last`, each entry carrying a doc line (the help surface and the roster-enumerating refusal) and a scalar-typed param schema (all core entries param-less; growth is a new Rust entry per C25, and higher layers register entries without touching the core) — or `Live(closure)`, the single non-data variant (an in-process consumer with consumer-owned loss policy). `record` streams the full series to the trace store at constant memory (no buffer-then-drain); folds keep an O(1) accumulator and land one summary row at finalize. Both declared-tap entry points are arms of the single verb `aura run` (`aura measure` is the post-hoc IC analysis over already-persisted traces and constructs no tap plan); `aura run` subscribes every declared tap to `record` by default, and its repeatable `--tap TAP=FOLD` selector (#310) replaces that default with an explicit plan — only listed taps are bound, unlisted taps stay unbound/inert.
|
Since #283 what CONSUMES a declared tap is itself declared per run by a **tap plan**: a subscription is either `Named { label, params }` — resolved against a layered **fold registry** whose core vocabulary is `record | count | sum | mean | min | max | first | last`, each entry carrying a doc line (the help surface and the roster-enumerating refusal) and a scalar-typed param schema (all core entries param-less; growth is a new Rust entry per C25, and higher layers register entries without touching the core) — or `Live(closure)`, the single non-data variant (an in-process consumer with consumer-owned loss policy). `record` streams the full series to the trace store at constant memory (no buffer-then-drain); folds keep an O(1) accumulator and land one summary row at finalize. Both declared-tap entry points are arms of the single verb `aura run` (`aura measure` is the post-hoc IC analysis over already-persisted traces and constructs no tap plan); `aura run` subscribes every declared tap to `record` by default, and its repeatable `--tap TAP=FOLD` selector (#310) replaces that default with an explicit plan — only listed taps are bound, unlisted taps stay unbound/inert. A fold's one summary row is emitted at finalize and stamped with the instant of the last contributing (warm) value — `first` alone pins the first contributing instant, and `min`/`max` deliberately do not carry the extremum's instant (ratified #335).
|
||||||
|
|
||||||
### topology hash
|
### topology hash
|
||||||
**Avoid:** —
|
**Avoid:** —
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[
|
||||||
|
{"op": "doc", "text": "SMA crossover bias with two measurement taps for fold discovery"},
|
||||||
|
{"op": "source", "role": "price", "kind": "F64"},
|
||||||
|
{"op": "add", "type": "SMA", "name": "fast", "bind": {"length": {"I64": 3}}},
|
||||||
|
{"op": "add", "type": "SMA", "name": "slow", "bind": {"length": {"I64": 6}}},
|
||||||
|
{"op": "add", "type": "SMA", "name": "px", "bind": {"length": {"I64": 1}}},
|
||||||
|
{"op": "feed", "role": "price", "into": ["fast.series", "slow.series", "px.series"]},
|
||||||
|
{"op": "add", "type": "Sub", "name": "sub"},
|
||||||
|
{"op": "connect", "from": "fast.value", "to": "sub.lhs"},
|
||||||
|
{"op": "connect", "from": "slow.value", "to": "sub.rhs"},
|
||||||
|
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 0.5}}},
|
||||||
|
{"op": "connect", "from": "sub.value", "to": "bias.signal"},
|
||||||
|
{"op": "tap", "from": "sub.value", "as": "spread"},
|
||||||
|
{"op": "tap", "from": "px.value", "as": "price"},
|
||||||
|
{"op": "expose", "from": "bias.bias", "as": "bias"}
|
||||||
|
]
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[
|
||||||
|
{"op": "source", "role": "price", "kind": "F64"},
|
||||||
|
{"op": "add", "type": "SMA", "name": "fast", "params": {"length": {"I64": 3}}},
|
||||||
|
{"op": "add", "type": "SMA", "name": "slow", "bind": {"length": {"I64": 6}}},
|
||||||
|
{"op": "feed", "role": "price", "into": ["fast.series", "slow.series"]},
|
||||||
|
{"op": "add", "type": "Sub", "name": "sub"},
|
||||||
|
{"op": "connect", "from": "fast.value", "to": "sub.lhs"},
|
||||||
|
{"op": "connect", "from": "slow.value", "to": "sub.rhs"},
|
||||||
|
{"op": "add", "type": "Bias", "name": "bias"},
|
||||||
|
{"op": "connect", "from": "sub.value", "to": "bias.signal"},
|
||||||
|
{"op": "expose", "from": "bias.bias", "as": "bias"}
|
||||||
|
]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[
|
||||||
|
{"op": "doc", "text": "SMA crossover exposing three measurement taps to probe the skipped-tap note"},
|
||||||
|
{"op": "source", "role": "price", "kind": "F64"},
|
||||||
|
{"op": "add", "type": "SMA", "name": "fast", "bind": {"length": {"I64": 3}}},
|
||||||
|
{"op": "add", "type": "SMA", "name": "slow", "bind": {"length": {"I64": 6}}},
|
||||||
|
{"op": "feed", "role": "price", "into": ["fast.series", "slow.series"]},
|
||||||
|
{"op": "add", "type": "Sub", "name": "sub"},
|
||||||
|
{"op": "connect", "from": "fast.value", "to": "sub.lhs"},
|
||||||
|
{"op": "connect", "from": "slow.value", "to": "sub.rhs"},
|
||||||
|
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 1.0}}},
|
||||||
|
{"op": "connect", "from": "sub.value", "to": "bias.signal"},
|
||||||
|
{"op": "tap", "from": "fast.value", "as": "fast_ma"},
|
||||||
|
{"op": "tap", "from": "slow.value", "as": "slow_ma"},
|
||||||
|
{"op": "tap", "from": "sub.value", "as": "spread"},
|
||||||
|
{"op": "expose", "from": "bias.bias", "as": "bias"}
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user