Compare commits
14 Commits
8c19260e8d
...
cfc2d9f5b1
| Author | SHA1 | Date | |
|---|---|---|---|
| cfc2d9f5b1 | |||
| b04b56148d | |||
| 96e79184e0 | |||
| 2d988fdf3d | |||
| bd7585b609 | |||
| 9c7f60b269 | |||
| 7126886b81 | |||
| 593e233e66 | |||
| 750ee93180 | |||
| d6694d0641 | |||
| ab3f16879b | |||
| d8c6938027 | |||
| a32dc38d18 | |||
| df3c2f06bd |
@@ -159,6 +159,16 @@ design decision, not a refactor, and belongs in the ledger.
|
||||
harness and is an execution viewer / trace explorer (structure before, live
|
||||
streams during, recorded traces after) — never a scene editor; topology is
|
||||
grown in Rust + hot-reload, runtime params are UI-tunable.
|
||||
13. **Self-description: every surface explains itself.** Every closed-
|
||||
vocabulary entry the binary exposes (nodes, metrics, tap slots, folds,
|
||||
blocks) carries a one-line meaning, enforced at its entry seam —
|
||||
compile/unit for engine-shipped entries, load for native node crates,
|
||||
register for the content-addressed store (documents: an optional gated
|
||||
`description`). The gate is deterministic string shape (`doc_gate`),
|
||||
never content judgement: the engine never evaluates description text
|
||||
(no freetext logic hole, invariant 10), descriptions never influence
|
||||
execution, determinism (C1), or identity ids, and registered artifacts
|
||||
are never retroactively invalidated. (C29)
|
||||
|
||||
## HTML surfaces
|
||||
|
||||
|
||||
@@ -119,8 +119,9 @@ aura sweep crossover.bp.json --list-axes
|
||||
```
|
||||
|
||||
The op kinds are `source`, `input`, `add`, `feed`, `connect`, `expose`, `tap`,
|
||||
and `gang` (`tap` declares a recorded measurement point on an interior wire —
|
||||
see the authoring guide). See
|
||||
`gang`, and `doc` (`tap` declares a recorded measurement point on an interior
|
||||
wire; `doc` declares the composite's one-line meaning, required at register —
|
||||
C29; see the authoring guide). See
|
||||
`aura graph introspect --node <T>` for a type's exact ports and the op-script
|
||||
grammar in the design ledger for the full semantics.
|
||||
|
||||
|
||||
@@ -1214,6 +1214,7 @@ mod tests {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "in".into() }],
|
||||
output: vec![],
|
||||
params: vec![],
|
||||
doc: "test-only schema",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,12 @@ impl PositionManagement {
|
||||
.collect();
|
||||
PrimitiveBuilder::new(
|
||||
"PositionManagement",
|
||||
NodeSchema { inputs, output, params: vec![] },
|
||||
NodeSchema {
|
||||
inputs,
|
||||
output,
|
||||
params: vec![],
|
||||
doc: "turns bias plus protective stop into a managed position in R",
|
||||
},
|
||||
|_| Box::new(PositionManagement::new()),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ impl SimBroker {
|
||||
],
|
||||
output: vec![FieldSpec { name: "equity".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "frictionless sim-optimal broker: integrates held exposure times price return into cumulative pip equity",
|
||||
},
|
||||
move |_| Box::new(SimBroker::new(pip_size)),
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"surface": "campaign_heavy",
|
||||
"metrics": {
|
||||
"cpu_percent": 2116.0,
|
||||
"peak_rss_mb": 98.25,
|
||||
"wall_s": 5.604210818
|
||||
"cpu_percent": 2152.0,
|
||||
"peak_rss_mb": 101.3828125,
|
||||
"wall_s": 5.572580056
|
||||
},
|
||||
"fingerprint": "cells=8 c0s0w17 c0s1v[0.1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24] c0s3b2883 c1s0w17 c1s1v[0.1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24] c1s3b2883 c2s0w17 c2s1v[0.1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24] c2s3b2883 c3s0w17 c3s1v[0.1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24] c3s3b2883 c4s0w17 c4s1v[0.1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24] c4s3b2883 c5s0w17 c5s1v[0.1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24] c5s3b2883 c6s0w17 c6s1v[0.1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24] c6s3b2883 c7s0w17 c7s1v[0.1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24] c7s3b2883",
|
||||
"reps": 3,
|
||||
@@ -12,6 +12,6 @@
|
||||
"nproc": 24
|
||||
},
|
||||
"profile": "release",
|
||||
"commit": "d62dede",
|
||||
"date": "2026-07-17"
|
||||
"commit": "9c7f60b",
|
||||
"date": "2026-07-23"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"surface": "campaign_sweep",
|
||||
"metrics": {
|
||||
"cpu_percent": 2170.0,
|
||||
"peak_rss_mb": 98.21875,
|
||||
"wall_s": 1.418092216
|
||||
"cpu_percent": 2190.0,
|
||||
"peak_rss_mb": 92.203125,
|
||||
"wall_s": 1.394470036
|
||||
},
|
||||
"fingerprint": "cells=8 c0s0w17 c1s0w17 c2s0w17 c3s0w17 c4s0w17 c5s0w17 c6s0w17 c7s0w17",
|
||||
"reps": 3,
|
||||
@@ -12,6 +12,6 @@
|
||||
"nproc": 24
|
||||
},
|
||||
"profile": "release",
|
||||
"commit": "d62dede",
|
||||
"date": "2026-07-17"
|
||||
"commit": "9c7f60b",
|
||||
"date": "2026-07-23"
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"surface": "cli_fixed_cost",
|
||||
"metrics": {
|
||||
"help_ms": 1.5094509999999999,
|
||||
"run_ms": 3.679743
|
||||
"help_ms": 1.5046899999999999,
|
||||
"run_ms": 3.63305
|
||||
},
|
||||
"fingerprint": "run_line_fnv=792dc4434f8e3dee",
|
||||
"fingerprint": "run_line_fnv=6bb0d796f760d140",
|
||||
"reps": 3,
|
||||
"host": {
|
||||
"hostname": "Raki",
|
||||
"nproc": 24
|
||||
},
|
||||
"profile": "release",
|
||||
"commit": "13d8500",
|
||||
"date": "2026-07-17"
|
||||
"commit": "9c7f60b",
|
||||
"date": "2026-07-23"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"surface": "engine_throughput",
|
||||
"metrics": {
|
||||
"bars_per_s": 13591474.080269944,
|
||||
"wall_s": 0.735755367
|
||||
"bars_per_s": 14005588.039151624,
|
||||
"wall_s": 0.714000724
|
||||
},
|
||||
"fingerprint": "last=0.417351 max_dd=8.274577 flips=265308 n=10000000",
|
||||
"reps": 3,
|
||||
@@ -11,6 +11,6 @@
|
||||
"nproc": 24
|
||||
},
|
||||
"profile": "release",
|
||||
"commit": "d62dede",
|
||||
"date": "2026-07-17"
|
||||
"commit": "9c7f60b",
|
||||
"date": "2026-07-23"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"surface": "ingest_throughput",
|
||||
"metrics": {
|
||||
"bars_per_s": 12912289.748572402,
|
||||
"wall_s": 0.388838858
|
||||
"bars_per_s": 13000457.745164338,
|
||||
"wall_s": 0.386201786
|
||||
},
|
||||
"fingerprint": "n=251040 xor=0019885debd3bd5e",
|
||||
"reps": 3,
|
||||
@@ -11,6 +11,6 @@
|
||||
"nproc": 24
|
||||
},
|
||||
"profile": "release",
|
||||
"commit": "d62dede",
|
||||
"date": "2026-07-17"
|
||||
"commit": "9c7f60b",
|
||||
"date": "2026-07-23"
|
||||
}
|
||||
|
||||
@@ -67,8 +67,8 @@ pub const HEAVY_PROCESS_DOC_QUICK: &str = r#"{
|
||||
/// `crates/aura-cli/examples/r_sma.json` except the second variant's bound
|
||||
/// lengths (3/6 instead of 2/4), which gives it a distinct content id — two
|
||||
/// strategies, same axes surface.
|
||||
pub const BP_SMA_A: &str = r#"{"format_version":1,"blueprint":{"name":"sma_signal","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}"#;
|
||||
pub const BP_SMA_B: &str = r#"{"format_version":1,"blueprint":{"name":"sma_signal","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":6}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}"#;
|
||||
pub const BP_SMA_A: &str = r#"{"format_version":1,"blueprint":{"name":"sma_signal","doc":"fast/slow SMA difference clamped into a directional bias","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}"#;
|
||||
pub const BP_SMA_B: &str = r#"{"format_version":1,"blueprint":{"name":"sma_signal","doc":"fast/slow SMA difference clamped into a directional bias","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":6}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}"#;
|
||||
|
||||
fn run_in(bin: &Path, dir: &Path, args: &[&str]) -> Result<(String, Option<i32>), String> {
|
||||
let out = std::process::Command::new(bin)
|
||||
|
||||
@@ -85,7 +85,7 @@ fn metric_vocabulary_covers_all_shipped_scalar_metrics() {
|
||||
scalar_fields.extend(selection_scalars);
|
||||
|
||||
let vocabulary: BTreeSet<String> =
|
||||
aura_research::metric_vocabulary().iter().map(|s| s.to_string()).collect();
|
||||
aura_research::metric_vocabulary().iter().map(|m| m.id.to_string()).collect();
|
||||
|
||||
let missing_from_vocabulary: Vec<_> = scalar_fields.difference(&vocabulary).collect();
|
||||
let stale_in_vocabulary: Vec<_> = vocabulary.difference(&scalar_fields).collect();
|
||||
@@ -128,7 +128,7 @@ fn rankable_roster_is_single_sourced_and_nested() {
|
||||
}
|
||||
for name in aura_campaign::PER_MEMBER_METRICS {
|
||||
assert!(
|
||||
aura_research::metric_vocabulary().contains(name),
|
||||
aura_research::metric_vocabulary().iter().any(|m| m.id == *name),
|
||||
"per-member metric '{name}' missing from the research vocabulary"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"format_version":1,"blueprint":{"name":"r_breakout_signal","nodes":[{"primitive":{"type":"Delay","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"RollingMax","name":"channel_hi","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"RollingMin","name":"channel_lo","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":1,"slot":0,"from_field":0},{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":3,"slot":1,"from_field":0},{"from":2,"to":4,"slot":0,"from_field":0},{"from":3,"to":5,"slot":0,"from_field":0},{"from":4,"to":5,"slot":1,"from_field":0},{"from":4,"to":6,"slot":0,"from_field":0},{"from":3,"to":6,"slot":1,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":6,"to":7,"slot":1,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":3,"slot":0},{"node":4,"slot":1}],"source":"F64"}],"output":[{"node":7,"field":0,"name":"bias"}]}}
|
||||
{"format_version":1,"blueprint":{"name":"r_breakout_signal","doc":"rolling-extreme breakout latched into a long/short signal","nodes":[{"primitive":{"type":"Delay","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"RollingMax","name":"channel_hi","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"RollingMin","name":"channel_lo","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":1,"slot":0,"from_field":0},{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":3,"slot":1,"from_field":0},{"from":2,"to":4,"slot":0,"from_field":0},{"from":3,"to":5,"slot":0,"from_field":0},{"from":4,"to":5,"slot":1,"from_field":0},{"from":4,"to":6,"slot":0,"from_field":0},{"from":3,"to":6,"slot":1,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":6,"to":7,"slot":1,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":3,"slot":0},{"node":4,"slot":1}],"source":"F64"}],"output":[{"node":7,"field":0,"name":"bias"}]}}
|
||||
@@ -1 +1 @@
|
||||
{"format_version":1,"blueprint":{"name":"hl_channel","nodes":[{"primitive":{"type":"Delay","name":"prev_high","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"Delay","name":"prev_low","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"RollingMax","name":"channel_hi","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"RollingMin","name":"channel_lo","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":3,"slot":0,"from_field":0},{"from":2,"to":4,"slot":1,"from_field":0},{"from":3,"to":5,"slot":0,"from_field":0},{"from":4,"to":6,"slot":0,"from_field":0},{"from":5,"to":6,"slot":1,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":4,"to":7,"slot":1,"from_field":0},{"from":6,"to":8,"slot":0,"from_field":0},{"from":7,"to":8,"slot":1,"from_field":0}],"input_roles":[{"name":"high","targets":[{"node":0,"slot":0}],"source":"F64"},{"name":"low","targets":[{"node":1,"slot":0}],"source":"F64"},{"name":"close","targets":[{"node":4,"slot":0},{"node":5,"slot":1}],"source":"F64"}],"output":[{"node":8,"field":0,"name":"bias"}]}}
|
||||
{"format_version":1,"blueprint":{"name":"hl_channel","doc":"prior high/low channel breaks latched into a directional signal","nodes":[{"primitive":{"type":"Delay","name":"prev_high","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"Delay","name":"prev_low","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"RollingMax","name":"channel_hi","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"RollingMin","name":"channel_lo","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":3,"slot":0,"from_field":0},{"from":2,"to":4,"slot":1,"from_field":0},{"from":3,"to":5,"slot":0,"from_field":0},{"from":4,"to":6,"slot":0,"from_field":0},{"from":5,"to":6,"slot":1,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":4,"to":7,"slot":1,"from_field":0},{"from":6,"to":8,"slot":0,"from_field":0},{"from":7,"to":8,"slot":1,"from_field":0}],"input_roles":[{"name":"high","targets":[{"node":0,"slot":0}],"source":"F64"},{"name":"low","targets":[{"node":1,"slot":0}],"source":"F64"},{"name":"close","targets":[{"node":4,"slot":0},{"node":5,"slot":1}],"source":"F64"}],"output":[{"node":8,"field":0,"name":"bias"}]}}
|
||||
@@ -1 +1 @@
|
||||
{"format_version":1,"blueprint":{"name":"r_meanrev_signal","nodes":[{"primitive":{"type":"EMA","name":"mean_window","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Mul"}},{"primitive":{"type":"EMA","name":"var_window","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"Sqrt"}},{"primitive":{"type":"Scale","name":"band","bound":[{"pos":0,"name":"factor","kind":"F64","value":{"F64":2.0}}]}},{"primitive":{"type":"Add"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":1,"slot":1,"from_field":0},{"from":1,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0},{"from":3,"to":4,"slot":0,"from_field":0},{"from":4,"to":5,"slot":0,"from_field":0},{"from":0,"to":6,"slot":0,"from_field":0},{"from":5,"to":6,"slot":1,"from_field":0},{"from":0,"to":7,"slot":0,"from_field":0},{"from":5,"to":7,"slot":1,"from_field":0},{"from":6,"to":8,"slot":1,"from_field":0},{"from":7,"to":9,"slot":0,"from_field":0},{"from":8,"to":10,"slot":0,"from_field":0},{"from":9,"to":10,"slot":1,"from_field":0},{"from":9,"to":11,"slot":0,"from_field":0},{"from":8,"to":11,"slot":1,"from_field":0},{"from":11,"to":12,"slot":0,"from_field":0},{"from":10,"to":12,"slot":1,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0},{"node":8,"slot":0},{"node":9,"slot":1}],"source":"F64"}],"output":[{"node":12,"field":0,"name":"bias"}]}}
|
||||
{"format_version":1,"blueprint":{"name":"r_meanrev_signal","doc":"EMA deviation against a volatility band, latched into a mean-reversion signal","nodes":[{"primitive":{"type":"EMA","name":"mean_window","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Mul"}},{"primitive":{"type":"EMA","name":"var_window","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"Sqrt"}},{"primitive":{"type":"Scale","name":"band","bound":[{"pos":0,"name":"factor","kind":"F64","value":{"F64":2.0}}]}},{"primitive":{"type":"Add"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":1,"slot":1,"from_field":0},{"from":1,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0},{"from":3,"to":4,"slot":0,"from_field":0},{"from":4,"to":5,"slot":0,"from_field":0},{"from":0,"to":6,"slot":0,"from_field":0},{"from":5,"to":6,"slot":1,"from_field":0},{"from":0,"to":7,"slot":0,"from_field":0},{"from":5,"to":7,"slot":1,"from_field":0},{"from":6,"to":8,"slot":1,"from_field":0},{"from":7,"to":9,"slot":0,"from_field":0},{"from":8,"to":10,"slot":0,"from_field":0},{"from":9,"to":10,"slot":1,"from_field":0},{"from":9,"to":11,"slot":0,"from_field":0},{"from":8,"to":11,"slot":1,"from_field":0},{"from":11,"to":12,"slot":0,"from_field":0},{"from":10,"to":12,"slot":1,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0},{"node":8,"slot":0},{"node":9,"slot":1}],"source":"F64"}],"output":[{"node":12,"field":0,"name":"bias"}]}}
|
||||
@@ -1 +1 @@
|
||||
{"format_version":1,"blueprint":{"name":"sma_signal","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
|
||||
{"format_version":1,"blueprint":{"name":"sma_signal","doc":"fast/slow SMA difference clamped into a directional bias","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
|
||||
@@ -54,6 +54,9 @@ enum OpDoc {
|
||||
as_name: String,
|
||||
into: Vec<String>,
|
||||
},
|
||||
/// Declare the composite's one-line meaning (C29, #316) — the op-script
|
||||
/// twin of the builder's `.doc(...)`.
|
||||
Doc { text: String },
|
||||
}
|
||||
|
||||
impl OpDoc {
|
||||
@@ -68,6 +71,7 @@ impl OpDoc {
|
||||
OpDoc::Expose { .. } => "expose",
|
||||
OpDoc::Tap { .. } => "tap",
|
||||
OpDoc::Gang { .. } => "gang",
|
||||
OpDoc::Doc { .. } => "doc",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +89,7 @@ impl From<OpDoc> for Op {
|
||||
OpDoc::Expose { from, as_name } => Op::Expose { from, as_name },
|
||||
OpDoc::Tap { from, as_name } => Op::Tap { from, as_name },
|
||||
OpDoc::Gang { as_name, into } => Op::Gang { as_name, into },
|
||||
OpDoc::Doc { text } => Op::Doc { text },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,6 +134,7 @@ fn format_op_error(e: &OpError) -> String {
|
||||
}
|
||||
OpError::GangArity { gang } => format!("gang `{gang}`: needs at least two members"),
|
||||
OpError::Incomplete(ce) => format!("{ce:?}"),
|
||||
OpError::DuplicateDoc => "a doc op may appear at most once".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,9 +92,10 @@ fn guard_one_mode(cmd: &DocIntrospectCmd, family: &str) {
|
||||
/// vocabulary, but the mode rides the shared introspect struct and answers
|
||||
/// for both families.
|
||||
fn print_metric_roster() {
|
||||
for name in aura_research::metric_vocabulary() {
|
||||
let rankable = aura_campaign::RANKABLE_METRICS.contains(name);
|
||||
let gate = aura_campaign::PER_MEMBER_METRICS.contains(name);
|
||||
for m in aura_research::metric_vocabulary() {
|
||||
let name = m.id;
|
||||
let rankable = aura_campaign::RANKABLE_METRICS.contains(&name);
|
||||
let gate = aura_campaign::PER_MEMBER_METRICS.contains(&name);
|
||||
// The generalize applicability is the registry's own R-expectancy
|
||||
// predicate — no fourth roster site (#190/#207).
|
||||
let generalize = aura_registry::check_r_metric(name).is_ok();
|
||||
@@ -181,7 +182,7 @@ pub(crate) fn doc_fault_prose(f: &DocFault) -> String {
|
||||
DocFault::UnknownEmitKind(kind) => format!("presentation.emit: unknown kind \"{kind}\""),
|
||||
DocFault::UnknownTap { index, tap } => format!(
|
||||
"presentation.persist_taps[{index}]: unknown tap \"{tap}\" (taps: {})",
|
||||
tap_vocabulary().join(" | ")
|
||||
tap_vocabulary().iter().map(|t| t.id).collect::<Vec<_>>().join(" | ")
|
||||
),
|
||||
DocFault::UnknownBindingColumn { role, column } => format!(
|
||||
"data.bindings.{role}: \"{column}\" names no archive column (columns: {})",
|
||||
@@ -190,6 +191,16 @@ pub(crate) fn doc_fault_prose(f: &DocFault) -> String {
|
||||
DocFault::ProcessRefMustBeContentId => {
|
||||
"process.ref: a process is referenced by content id in this version".into()
|
||||
}
|
||||
DocFault::BadDescription { subject, fault } => match fault {
|
||||
aura_core::DocGateFault::Empty => format!(
|
||||
"description: `{subject}`'s description is empty — omit the \
|
||||
field or write a one-line meaning (C29)"
|
||||
),
|
||||
aura_core::DocGateFault::RestatesName => format!(
|
||||
"description: `{subject}`'s description merely restates the \
|
||||
document name (C29)"
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -816,6 +827,35 @@ mod tests {
|
||||
assert!(!prose.contains("UnknownTap"), "Debug leak: {prose}");
|
||||
}
|
||||
|
||||
/// C29 (#316): the description-quality fault names the subject and the
|
||||
/// rule (C29) for both sub-faults — an empty description and one that
|
||||
/// merely restates the document name — never leaking the Debug variant
|
||||
/// names of either `DocFault` or the nested `DocGateFault`.
|
||||
#[test]
|
||||
fn bad_description_prose_names_subject_and_rule_for_both_faults() {
|
||||
let empty = doc_fault_prose(&DocFault::BadDescription {
|
||||
subject: "ger40-momentum".into(),
|
||||
fault: aura_core::DocGateFault::Empty,
|
||||
});
|
||||
assert!(
|
||||
empty.contains("`ger40-momentum`'s description is empty"),
|
||||
"subject and emptiness named: {empty}"
|
||||
);
|
||||
assert!(empty.contains("C29"), "rule cited: {empty}");
|
||||
assert!(!empty.contains("BadDescription") && !empty.contains("DocGateFault"), "Debug leak: {empty}");
|
||||
|
||||
let restated = doc_fault_prose(&DocFault::BadDescription {
|
||||
subject: "ger40-momentum".into(),
|
||||
fault: aura_core::DocGateFault::RestatesName,
|
||||
});
|
||||
assert!(
|
||||
restated.contains("`ger40-momentum`'s description merely restates the document name"),
|
||||
"subject and restatement named: {restated}"
|
||||
);
|
||||
assert!(restated.contains("C29"), "rule cited: {restated}");
|
||||
assert!(!restated.contains("BadDescription") && !restated.contains("DocGateFault"), "Debug leak: {restated}");
|
||||
}
|
||||
|
||||
/// #260: the instrument-map key-mismatch fault is path-addressed and names
|
||||
/// both directions — the campaign instruments the map misses AND the map
|
||||
/// keys naming no campaign instrument — in one line, no Debug leak. The
|
||||
|
||||
@@ -152,6 +152,8 @@ impl Scale {
|
||||
pub fn new(factor: f64) -> Self {
|
||||
Self { factor, out: [Cell::from_f64(0.0)] }
|
||||
}
|
||||
// Replace `doc` below when renaming this sample node — it must keep
|
||||
// describing the node's own behaviour, not this scaffolding step.
|
||||
pub fn builder() -> PrimitiveBuilder {
|
||||
PrimitiveBuilder::new(
|
||||
"__NS__::Scale",
|
||||
@@ -163,6 +165,7 @@ impl Scale {
|
||||
}],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![ParamSpec { name: "factor".into(), kind: ScalarKind::F64 }],
|
||||
doc: "scalar gain: emits the input times the factor param",
|
||||
},
|
||||
|p| Box::new(Scale::new(p[0].f64())),
|
||||
)
|
||||
@@ -219,7 +222,7 @@ crate is attached to its project via `[nodes]` in the project's `Aura.toml`.
|
||||
|
||||
const GITIGNORE_PROJECT: &str = "/runs\n";
|
||||
|
||||
const SIGNAL_JSON_STD: &str = r#"{"format_version":1,"blueprint":{"name":"__NAME_SNAKE___signal","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}"#;
|
||||
const SIGNAL_JSON_STD: &str = r#"{"format_version":1,"blueprint":{"name":"__NAME_SNAKE___signal","doc":"fast/slow SMA difference clamped into a directional bias","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}"#;
|
||||
|
||||
const CLAUDE_MD_PROJECT: &str = r#"# __NAME__ — an aura research project (data-only)
|
||||
|
||||
|
||||
@@ -60,9 +60,9 @@ fn persist_taps_from(trace: bool) -> Vec<String> {
|
||||
tap_vocabulary()
|
||||
.iter()
|
||||
.filter(|t| {
|
||||
aura_runner::runner::tap_channel(t) != Some(aura_runner::runner::TapChannel::Net)
|
||||
aura_runner::runner::tap_channel(t.id) != Some(aura_runner::runner::TapChannel::Net)
|
||||
})
|
||||
.map(|t| t.to_string())
|
||||
.map(|t| t.id.to_string())
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
|
||||
@@ -2182,7 +2182,7 @@ const HL_RANGE_CLOSED_BLUEPRINT: &str = r#"{
|
||||
"output": [{"node":0,"field":0,"name":"bias"}]
|
||||
}
|
||||
}"#;
|
||||
const HL_SIGNAL_OPEN_BLUEPRINT: &str = r#"{"format_version":1,"blueprint":{"name":"hl_signal","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"high","targets":[{"node":0,"slot":0}],"source":"F64"},{"name":"low","targets":[{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}"#;
|
||||
const HL_SIGNAL_OPEN_BLUEPRINT: &str = r#"{"format_version":1,"blueprint":{"name":"hl_signal","doc":"high/low SMA difference clamped into a directional bias","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"high","targets":[{"node":0,"slot":0}],"source":"F64"},{"name":"low","targets":[{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}"#;
|
||||
|
||||
/// Property (#231 task 4 quality follow-up): `aura run` over a multi-column
|
||||
/// blueprint with NO `--real` (the synthetic default) refuses through the
|
||||
|
||||
@@ -26,6 +26,7 @@ impl Identity {
|
||||
}],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "one-input f64 pass-through",
|
||||
},
|
||||
|_| Box::new(Identity::new()),
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ impl Identity {
|
||||
}],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "one-input f64 pass-through",
|
||||
},
|
||||
|_| Box::new(Identity::new()),
|
||||
)
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"format_version":1,"blueprint":{"name":"r_breakout_signal","nodes":[{"primitive":{"type":"Delay","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"RollingMax","name":"channel_hi"}},{"primitive":{"type":"RollingMin","name":"channel_lo"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":1,"slot":0,"from_field":0},{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":3,"slot":1,"from_field":0},{"from":2,"to":4,"slot":0,"from_field":0},{"from":3,"to":5,"slot":0,"from_field":0},{"from":4,"to":5,"slot":1,"from_field":0},{"from":4,"to":6,"slot":0,"from_field":0},{"from":3,"to":6,"slot":1,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":6,"to":7,"slot":1,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":3,"slot":0},{"node":4,"slot":1}],"source":"F64"}],"output":[{"node":7,"field":0,"name":"bias"}],"gangs":[{"name":"channel_length","kind":"I64","members":[{"node":1,"pos":0,"name":"length"},{"node":2,"pos":0,"name":"length"}]}]}}
|
||||
{"format_version":1,"blueprint":{"name":"r_breakout_signal","doc":"rolling-extreme breakout latched into a long/short signal","nodes":[{"primitive":{"type":"Delay","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"RollingMax","name":"channel_hi"}},{"primitive":{"type":"RollingMin","name":"channel_lo"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":1,"slot":0,"from_field":0},{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":3,"slot":1,"from_field":0},{"from":2,"to":4,"slot":0,"from_field":0},{"from":3,"to":5,"slot":0,"from_field":0},{"from":4,"to":5,"slot":1,"from_field":0},{"from":4,"to":6,"slot":0,"from_field":0},{"from":3,"to":6,"slot":1,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":6,"to":7,"slot":1,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":3,"slot":0},{"node":4,"slot":1}],"source":"F64"}],"output":[{"node":7,"field":0,"name":"bias"}],"gangs":[{"name":"channel_length","kind":"I64","members":[{"node":1,"pos":0,"name":"length"},{"node":2,"pos":0,"name":"length"}]}]}}
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"format_version":1,"blueprint":{"name":"hl_channel","nodes":[{"primitive":{"type":"Delay","name":"prev_high","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"Delay","name":"prev_low","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"RollingMax","name":"channel_hi"}},{"primitive":{"type":"RollingMin","name":"channel_lo"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":3,"slot":0,"from_field":0},{"from":2,"to":4,"slot":1,"from_field":0},{"from":3,"to":5,"slot":0,"from_field":0},{"from":4,"to":6,"slot":0,"from_field":0},{"from":5,"to":6,"slot":1,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":4,"to":7,"slot":1,"from_field":0},{"from":6,"to":8,"slot":0,"from_field":0},{"from":7,"to":8,"slot":1,"from_field":0}],"input_roles":[{"name":"high","targets":[{"node":0,"slot":0}],"source":"F64"},{"name":"low","targets":[{"node":1,"slot":0}],"source":"F64"},{"name":"close","targets":[{"node":4,"slot":0},{"node":5,"slot":1}],"source":"F64"}],"output":[{"node":8,"field":0,"name":"bias"}],"gangs":[{"name":"channel_length","kind":"I64","members":[{"node":2,"pos":0,"name":"length"},{"node":3,"pos":0,"name":"length"}]}]}}
|
||||
{"format_version":1,"blueprint":{"name":"hl_channel","doc":"prior high/low channel breaks latched into a directional signal","nodes":[{"primitive":{"type":"Delay","name":"prev_high","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"Delay","name":"prev_low","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"RollingMax","name":"channel_hi"}},{"primitive":{"type":"RollingMin","name":"channel_lo"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":3,"slot":0,"from_field":0},{"from":2,"to":4,"slot":1,"from_field":0},{"from":3,"to":5,"slot":0,"from_field":0},{"from":4,"to":6,"slot":0,"from_field":0},{"from":5,"to":6,"slot":1,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":4,"to":7,"slot":1,"from_field":0},{"from":6,"to":8,"slot":0,"from_field":0},{"from":7,"to":8,"slot":1,"from_field":0}],"input_roles":[{"name":"high","targets":[{"node":0,"slot":0}],"source":"F64"},{"name":"low","targets":[{"node":1,"slot":0}],"source":"F64"},{"name":"close","targets":[{"node":4,"slot":0},{"node":5,"slot":1}],"source":"F64"}],"output":[{"node":8,"field":0,"name":"bias"}],"gangs":[{"name":"channel_length","kind":"I64","members":[{"node":2,"pos":0,"name":"length"},{"node":3,"pos":0,"name":"length"}]}]}}
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"format_version":1,"blueprint":{"name":"r_meanrev_signal","nodes":[{"primitive":{"type":"EMA","name":"mean_window"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Mul"}},{"primitive":{"type":"EMA","name":"var_window"}},{"primitive":{"type":"Sqrt"}},{"primitive":{"type":"Scale","name":"band"}},{"primitive":{"type":"Add"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":1,"slot":1,"from_field":0},{"from":1,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0},{"from":3,"to":4,"slot":0,"from_field":0},{"from":4,"to":5,"slot":0,"from_field":0},{"from":0,"to":6,"slot":0,"from_field":0},{"from":5,"to":6,"slot":1,"from_field":0},{"from":0,"to":7,"slot":0,"from_field":0},{"from":5,"to":7,"slot":1,"from_field":0},{"from":6,"to":8,"slot":1,"from_field":0},{"from":7,"to":9,"slot":0,"from_field":0},{"from":8,"to":10,"slot":0,"from_field":0},{"from":9,"to":10,"slot":1,"from_field":0},{"from":9,"to":11,"slot":0,"from_field":0},{"from":8,"to":11,"slot":1,"from_field":0},{"from":11,"to":12,"slot":0,"from_field":0},{"from":10,"to":12,"slot":1,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0},{"node":8,"slot":0},{"node":9,"slot":1}],"source":"F64"}],"output":[{"node":12,"field":0,"name":"bias"}],"gangs":[{"name":"window","kind":"I64","members":[{"node":0,"pos":0,"name":"length"},{"node":3,"pos":0,"name":"length"}]}]}}
|
||||
{"format_version":1,"blueprint":{"name":"r_meanrev_signal","doc":"EMA deviation against a volatility band, latched into a mean-reversion signal","nodes":[{"primitive":{"type":"EMA","name":"mean_window"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Mul"}},{"primitive":{"type":"EMA","name":"var_window"}},{"primitive":{"type":"Sqrt"}},{"primitive":{"type":"Scale","name":"band"}},{"primitive":{"type":"Add"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":1,"slot":1,"from_field":0},{"from":1,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0},{"from":3,"to":4,"slot":0,"from_field":0},{"from":4,"to":5,"slot":0,"from_field":0},{"from":0,"to":6,"slot":0,"from_field":0},{"from":5,"to":6,"slot":1,"from_field":0},{"from":0,"to":7,"slot":0,"from_field":0},{"from":5,"to":7,"slot":1,"from_field":0},{"from":6,"to":8,"slot":1,"from_field":0},{"from":7,"to":9,"slot":0,"from_field":0},{"from":8,"to":10,"slot":0,"from_field":0},{"from":9,"to":10,"slot":1,"from_field":0},{"from":9,"to":11,"slot":0,"from_field":0},{"from":8,"to":11,"slot":1,"from_field":0},{"from":11,"to":12,"slot":0,"from_field":0},{"from":10,"to":12,"slot":1,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0},{"node":8,"slot":0},{"node":9,"slot":1}],"source":"F64"}],"output":[{"node":12,"field":0,"name":"bias"}],"gangs":[{"name":"window","kind":"I64","members":[{"node":0,"pos":0,"name":"length"},{"node":3,"pos":0,"name":"length"}]}]}}
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"format_version":1,"blueprint":{"name":"sma_signal","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
|
||||
{"format_version":1,"blueprint":{"name":"sma_signal","doc":"fast/slow SMA difference clamped into a directional bias","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
|
||||
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
/runs
|
||||
@@ -0,0 +1 @@
|
||||
# restated fixture — static project context only (C17); paths only (cycle 0102).
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "restated-project"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
aura-core = { path = "../../../../aura-core" }
|
||||
|
||||
# Standalone workspace root: never a member of the engine workspace
|
||||
# (docs/project-layout.md, the empty-[workspace] note).
|
||||
[workspace]
|
||||
@@ -0,0 +1,83 @@
|
||||
//! Fixture project for the C29 load-seam e2e (#316): one fully resolvable
|
||||
//! pass-through node under the `restated` namespace whose `NodeSchema` doc
|
||||
//! merely restates its own type id (`DocGateFault::RestatesName`, the fault
|
||||
//! arm `undescribed-project` does not exercise). The charter is satisfied
|
||||
//! (prefixed id, list/resolver in sync) so the refusal under test is exactly
|
||||
//! the doc gate's name-restatement branch.
|
||||
|
||||
use aura_core::{
|
||||
Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder,
|
||||
ScalarKind,
|
||||
};
|
||||
|
||||
/// One-input f64 pass-through whose doc is a no-content restatement of its
|
||||
/// own name.
|
||||
pub struct Echo {
|
||||
out: [Cell; 1],
|
||||
}
|
||||
|
||||
impl Echo {
|
||||
pub fn new() -> Self {
|
||||
Self { out: [Cell::from_f64(0.0)] }
|
||||
}
|
||||
pub fn builder() -> PrimitiveBuilder {
|
||||
PrimitiveBuilder::new(
|
||||
"restated::Echo",
|
||||
NodeSchema {
|
||||
inputs: vec![PortSpec {
|
||||
kind: ScalarKind::F64,
|
||||
firing: Firing::Any,
|
||||
name: "value".into(),
|
||||
}],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
// The one deliberate violation this fixture exists for: this
|
||||
// norm-equals the type id "restated::Echo" once punctuation
|
||||
// and case are stripped, so it carries no meaning beyond the
|
||||
// name itself.
|
||||
doc: "Restated Echo",
|
||||
},
|
||||
|_| Box::new(Echo::new()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Echo {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Node for Echo {
|
||||
fn lookbacks(&self) -> Vec<usize> {
|
||||
vec![1]
|
||||
}
|
||||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
|
||||
let w = ctx.f64_in(0);
|
||||
if w.is_empty() {
|
||||
return None;
|
||||
}
|
||||
self.out[0] = Cell::from_f64(w[0]);
|
||||
Some(&self.out)
|
||||
}
|
||||
fn label(&self) -> String {
|
||||
"restated::Echo".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn vocabulary(type_id: &str) -> Option<PrimitiveBuilder> {
|
||||
match type_id {
|
||||
"restated::Echo" => Some(Echo::builder()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn type_ids() -> &'static [&'static str] {
|
||||
&["restated::Echo"]
|
||||
}
|
||||
|
||||
aura_core::aura_project! {
|
||||
namespace: "restated",
|
||||
vocabulary: vocabulary,
|
||||
type_ids: type_ids,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
/runs
|
||||
@@ -0,0 +1 @@
|
||||
# undescribed fixture — static project context only (C17); paths only (cycle 0102).
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "undescribed-project"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
aura-core = { path = "../../../../aura-core" }
|
||||
|
||||
# Standalone workspace root: never a member of the engine workspace
|
||||
# (docs/project-layout.md, the empty-[workspace] note).
|
||||
[workspace]
|
||||
@@ -0,0 +1,78 @@
|
||||
//! Fixture project for the C29 load-seam e2e (#316): one fully resolvable
|
||||
//! pass-through node under the `und` namespace whose `NodeSchema` carries the
|
||||
//! empty-string alibi `doc: ""` — compile-legal (the field is present), but
|
||||
//! the load gate must refuse it. The charter is satisfied (prefixed id,
|
||||
//! list/resolver in sync) so the refusal under test is exactly the doc gate.
|
||||
|
||||
use aura_core::{
|
||||
Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder,
|
||||
ScalarKind,
|
||||
};
|
||||
|
||||
/// One-input f64 pass-through with a deliberately empty doc.
|
||||
pub struct Opaque {
|
||||
out: [Cell; 1],
|
||||
}
|
||||
|
||||
impl Opaque {
|
||||
pub fn new() -> Self {
|
||||
Self { out: [Cell::from_f64(0.0)] }
|
||||
}
|
||||
pub fn builder() -> PrimitiveBuilder {
|
||||
PrimitiveBuilder::new(
|
||||
"und::Opaque",
|
||||
NodeSchema {
|
||||
inputs: vec![PortSpec {
|
||||
kind: ScalarKind::F64,
|
||||
firing: Firing::Any,
|
||||
name: "value".into(),
|
||||
}],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
// The one deliberate violation this fixture exists for.
|
||||
doc: "",
|
||||
},
|
||||
|_| Box::new(Opaque::new()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Opaque {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Node for Opaque {
|
||||
fn lookbacks(&self) -> Vec<usize> {
|
||||
vec![1]
|
||||
}
|
||||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
|
||||
let w = ctx.f64_in(0);
|
||||
if w.is_empty() {
|
||||
return None;
|
||||
}
|
||||
self.out[0] = Cell::from_f64(w[0]);
|
||||
Some(&self.out)
|
||||
}
|
||||
fn label(&self) -> String {
|
||||
"und::Opaque".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn vocabulary(type_id: &str) -> Option<PrimitiveBuilder> {
|
||||
match type_id {
|
||||
"und::Opaque" => Some(Opaque::builder()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn type_ids() -> &'static [&'static str] {
|
||||
&["und::Opaque"]
|
||||
}
|
||||
|
||||
aura_core::aura_project! {
|
||||
namespace: "und",
|
||||
vocabulary: vocabulary,
|
||||
type_ids: type_ids,
|
||||
}
|
||||
@@ -54,6 +54,40 @@ const SIGNAL_DOC: &str = r#"[
|
||||
{"op":"expose","from":"bias.bias","as":"bias"}
|
||||
]"#;
|
||||
|
||||
/// SIGNAL_DOC with the C29 doc op — the register-reaching variant (a store
|
||||
/// write requires a described composite; build-only tests keep SIGNAL_DOC).
|
||||
const SIGNAL_DOC_DESCRIBED: &str = r#"[
|
||||
{"op":"doc","text":"fast/slow SMA difference clamped into a directional bias"},
|
||||
{"op":"source","role":"price","kind":"F64"},
|
||||
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":2}}},
|
||||
{"op":"add","type":"SMA","name":"slow","bind":{"length":{"I64":4}}},
|
||||
{"op":"add","type":"Sub"},
|
||||
{"op":"add","type":"Bias"},
|
||||
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
|
||||
{"op":"connect","from":"fast.value","to":"sub.lhs"},
|
||||
{"op":"connect","from":"slow.value","to":"sub.rhs"},
|
||||
{"op":"connect","from":"sub.value","to":"bias.signal"},
|
||||
{"op":"expose","from":"bias.bias","as":"bias"}
|
||||
]"#;
|
||||
|
||||
/// SIGNAL_DOC with a `doc` op whose text merely restates the op-script
|
||||
/// composite's name ("graph", the literal `replay("graph", ..)` gives every
|
||||
/// CLI-built op-script) -- the RestatesName arm of the C29 shape gate,
|
||||
/// reached via the op-script route rather than a builder-authored blueprint.
|
||||
const SIGNAL_DOC_NAME_RESTATED: &str = r#"[
|
||||
{"op":"doc","text":"Graph"},
|
||||
{"op":"source","role":"price","kind":"F64"},
|
||||
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":2}}},
|
||||
{"op":"add","type":"SMA","name":"slow","bind":{"length":{"I64":4}}},
|
||||
{"op":"add","type":"Sub"},
|
||||
{"op":"add","type":"Bias"},
|
||||
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
|
||||
{"op":"connect","from":"fast.value","to":"sub.lhs"},
|
||||
{"op":"connect","from":"slow.value","to":"sub.rhs"},
|
||||
{"op":"connect","from":"sub.value","to":"bias.signal"},
|
||||
{"op":"expose","from":"bias.bias","as":"bias"}
|
||||
]"#;
|
||||
|
||||
/// Every op below applies cleanly (all eager checks pass), but `sub.rhs` is
|
||||
/// never wired — a 0-cover input slot only the holistic finalize gate can
|
||||
/// reject. The smallest document that is op-valid yet whole-document-invalid.
|
||||
@@ -74,6 +108,21 @@ fn graph_build_emits_blueprint_for_a_valid_document() {
|
||||
assert!(stdout.contains("\"SMA\""), "carries the nodes: {stdout}");
|
||||
}
|
||||
|
||||
/// C29 (#316): the op-script `doc` op's text lands verbatim in the built
|
||||
/// blueprint's `doc` field -- `graph build` is a pure translation from ops to
|
||||
/// the canonical envelope, so a doc op is not consumed or reworded on the way
|
||||
/// through, only threaded (the shape gate is a separate, register-time check;
|
||||
/// see `graph_register_refuses_an_op_script_doc_that_restates_the_composite_name`).
|
||||
#[test]
|
||||
fn graph_build_emits_the_op_script_doc_verbatim_into_the_blueprint() {
|
||||
let (stdout, _stderr, ok) = run(&["graph", "build"], SIGNAL_DOC_DESCRIBED);
|
||||
assert!(ok, "exit success");
|
||||
assert!(
|
||||
stdout.contains(r#""doc":"fast/slow SMA difference clamped into a directional bias""#),
|
||||
"the doc op's exact text appears in the blueprint's doc field: {stdout}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The canonical blueprint artifact carries no trailing newline: `aura graph build`
|
||||
/// emits exactly the library `blueprint_to_json` bytes (the form #158 content-addresses),
|
||||
/// not a display-framed line. Byte-canonical across the CLI and library surfaces (#164).
|
||||
@@ -462,6 +511,47 @@ fn graph_register_is_idempotent() {
|
||||
assert_eq!(registered_id(&out1), registered_id(&out2), "same id twice");
|
||||
}
|
||||
|
||||
/// C29 register seam end-to-end (#316): a doc-less root composite refuses at
|
||||
/// `aura graph register` naming the composite and the rule (exit 1, C14); a
|
||||
/// doc-less named nested composite refuses naming the nested name; the
|
||||
/// described shipped example registers. The gate lives at the store write
|
||||
/// every register path shares — this pins its verb surface.
|
||||
#[test]
|
||||
fn register_refuses_undescribed_composites_end_to_end() {
|
||||
let dir = temp_cwd("register-doc-gate");
|
||||
// doc-less root: the shipped example minus its doc member
|
||||
let text = std::fs::read_to_string(example("r_sma.json")).expect("read example");
|
||||
let mut v: serde_json::Value = serde_json::from_str(&text).expect("example parses");
|
||||
v["blueprint"].as_object_mut().expect("blueprint object").remove("doc");
|
||||
let docless = dir.join("docless.json");
|
||||
std::fs::write(&docless, serde_json::to_string(&v).unwrap()).expect("write fixture");
|
||||
let (out, err, code) = run_in(&dir, &["graph", "register", docless.to_str().unwrap()]);
|
||||
assert_eq!(code, Some(1), "doc-less root refuses: {out} {err}");
|
||||
assert!(
|
||||
err.contains("composite `sma_signal` carries no doc"),
|
||||
"stderr names the composite and the rule: {err}"
|
||||
);
|
||||
assert!(err.contains("C29"), "stderr cites the contract: {err}");
|
||||
// doc-less named nested composite inside a described root
|
||||
v["blueprint"]["doc"] = serde_json::Value::String("a described root".into());
|
||||
v["blueprint"]["nodes"]
|
||||
.as_array_mut()
|
||||
.expect("nodes array")
|
||||
.push(serde_json::json!({"composite": {"name": "inner", "nodes": []}}));
|
||||
let nested = dir.join("nested.json");
|
||||
std::fs::write(&nested, serde_json::to_string(&v).unwrap()).expect("write fixture");
|
||||
let (out, err, code) = run_in(&dir, &["graph", "register", nested.to_str().unwrap()]);
|
||||
assert_eq!(code, Some(1), "doc-less nested refuses: {out} {err}");
|
||||
assert!(
|
||||
err.contains("composite `inner` carries no doc"),
|
||||
"stderr names the nested composite: {err}"
|
||||
);
|
||||
// the described shipped example registers
|
||||
let (out, err, code) = run_in(&dir, &["graph", "register", &example("r_sma.json")]);
|
||||
assert_eq!(code, Some(0), "described example registers: {out} {err}");
|
||||
assert!(out.starts_with("registered blueprint "), "prints the register line: {out}");
|
||||
}
|
||||
|
||||
/// Property (#196, the campaign-axis namespace): `--params <FILE>` prints the
|
||||
/// RAW composite param space — one `{name}:{kind:?}` line per open param, in
|
||||
/// lowering order, WITHOUT the harness-wrap prefix `aura sweep --list-axes`
|
||||
@@ -647,11 +737,12 @@ fn graph_register_rejects_an_unknown_node_type_with_prose() {
|
||||
#[test]
|
||||
fn graph_register_accepts_an_op_script_and_stores_the_envelope_content_id() {
|
||||
let dir = temp_cwd("register-op-script");
|
||||
// The op-script form (a JSON array of ops), written to a file.
|
||||
// The op-script form (a JSON array of ops), written to a file. Register
|
||||
// requires a described composite (C29), so the described twin is used here.
|
||||
let op_script = dir.join("op-script.json");
|
||||
std::fs::write(&op_script, SIGNAL_DOC).expect("write op-script fixture");
|
||||
std::fs::write(&op_script, SIGNAL_DOC_DESCRIBED).expect("write op-script fixture");
|
||||
// Its `graph build` envelope (a JSON object), the shape `register` already accepts.
|
||||
let (envelope_bytes, _e, built) = run(&["graph", "build"], SIGNAL_DOC);
|
||||
let (envelope_bytes, _e, built) = run(&["graph", "build"], SIGNAL_DOC_DESCRIBED);
|
||||
assert!(built, "graph build produces the envelope");
|
||||
let envelope = dir.join("envelope.json");
|
||||
std::fs::write(&envelope, &envelope_bytes).expect("write envelope fixture");
|
||||
@@ -669,6 +760,33 @@ fn graph_register_accepts_an_op_script_and_stores_the_envelope_content_id() {
|
||||
registered_id(&env_out),
|
||||
"op-script and its built envelope register to the same content id"
|
||||
);
|
||||
|
||||
// C29: the doc-less op-script form refuses at register like any other
|
||||
// doc-less composite entering the store.
|
||||
let docless_script = dir.join("docless-script.json");
|
||||
std::fs::write(&docless_script, SIGNAL_DOC).expect("write op-script fixture");
|
||||
let (dl_out, dl_err, dl_code) =
|
||||
run_in(&dir, &["graph", "register", docless_script.to_str().unwrap()]);
|
||||
assert_eq!(dl_code, Some(1), "doc-less op-script refuses: {dl_out} {dl_err}");
|
||||
assert!(dl_err.contains("carries no doc"), "stderr names the rule: {dl_err}");
|
||||
assert!(dl_err.contains("C29"), "stderr cites the contract: {dl_err}");
|
||||
}
|
||||
|
||||
/// C29: an op-script's `doc` op is not a bare presence checkbox -- its text
|
||||
/// goes through the same shape gate a builder-authored doc does. A doc that
|
||||
/// merely restates the composite's name (here "graph", every CLI op-script's
|
||||
/// literal name) refuses at register exactly like the builder-authored
|
||||
/// RestatesName case already pinned at the registry-unit layer, but reached
|
||||
/// end-to-end through the op-script CLI seam this time.
|
||||
#[test]
|
||||
fn graph_register_refuses_an_op_script_doc_that_restates_the_composite_name() {
|
||||
let dir = temp_cwd("register-op-script-restated-doc");
|
||||
let restated_script = dir.join("restated-doc-script.json");
|
||||
std::fs::write(&restated_script, SIGNAL_DOC_NAME_RESTATED).expect("write op-script fixture");
|
||||
let (out, err, code) = run_in(&dir, &["graph", "register", restated_script.to_str().unwrap()]);
|
||||
assert_eq!(code, Some(1), "name-restating doc refuses: {out} {err}");
|
||||
assert!(err.contains("merely restates"), "stderr names the rule: {err}");
|
||||
assert!(err.contains("C29"), "stderr cites the contract: {err}");
|
||||
}
|
||||
|
||||
/// Property (#202, the same on-ramp seam at the sibling verb): `aura graph
|
||||
@@ -1157,6 +1275,26 @@ fn graph_build_reports_gang_kind_mismatch_as_prose() {
|
||||
assert!(!stderr.contains("GangKindMismatch"), "does not leak the Debug variant name: {stderr}");
|
||||
}
|
||||
|
||||
/// The `doc` op's duplicate refusal (a second meaning line in one op-script)
|
||||
/// reads as prose at the binary seam, attributed to the second `doc` op's
|
||||
/// index -- never the raw `DuplicateDoc` Debug variant name.
|
||||
#[test]
|
||||
fn graph_build_reports_duplicate_doc_fault_as_prose() {
|
||||
let doc = r#"[
|
||||
{"op":"source","role":"price","kind":"F64"},
|
||||
{"op":"doc","text":"first"},
|
||||
{"op":"doc","text":"second"}
|
||||
]"#;
|
||||
let (stdout, stderr, ok) = run(&["graph", "build"], doc);
|
||||
assert!(!ok, "non-zero exit on a duplicate doc fault");
|
||||
assert!(stdout.is_empty(), "no blueprint emitted on a fault: {stdout}");
|
||||
assert!(
|
||||
stderr.contains("op 2 (doc): a doc op may appear at most once"),
|
||||
"names the op + cause as prose: {stderr}"
|
||||
);
|
||||
assert!(!stderr.contains("DuplicateDoc"), "does not leak the Debug variant name: {stderr}");
|
||||
}
|
||||
|
||||
/// Property (corrupt-gang-blueprint, the `gang_fault_prose` seam): `aura graph
|
||||
/// register` on a hand-corrupted blueprint whose gangs section fails structural
|
||||
/// validation (a single-member gang) refuses with the `blueprint_load_prose` ->
|
||||
|
||||
@@ -19,6 +19,14 @@ fn badcharter_dir() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/badcharter-project")
|
||||
}
|
||||
|
||||
fn undescribed_dir() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/undescribed-project")
|
||||
}
|
||||
|
||||
fn restated_dir() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/restated-project")
|
||||
}
|
||||
|
||||
fn aura(args: &[&str], cwd: &Path) -> Output {
|
||||
Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(args)
|
||||
@@ -164,6 +172,78 @@ fn vocabulary_charter_violation_refuses_end_to_end() {
|
||||
assert!(err.contains("lacks the project prefix"), "stderr must name the charter cause: {err}");
|
||||
}
|
||||
|
||||
/// C29 load seam end-to-end (#316): an extension node whose doc fails the
|
||||
/// shape gate (the empty-string alibi the compiler cannot catch) refuses at
|
||||
/// load with the entry and the rule named — through the real
|
||||
/// `aura-cli::project::load` path (libloading + descriptor read), not merely
|
||||
/// the pure `doc_gate` unit function. The described twin's load is pinned by
|
||||
/// `project_run_resolves_demo_node_and_is_bit_identical`.
|
||||
#[test]
|
||||
fn undescribed_vocabulary_entry_refuses_end_to_end() {
|
||||
let dir = undescribed_dir();
|
||||
let build = Command::new("cargo")
|
||||
.arg("build")
|
||||
.current_dir(&dir)
|
||||
.output()
|
||||
.expect("spawn cargo build for the undescribed fixture");
|
||||
assert!(
|
||||
build.status.success(),
|
||||
"undescribed fixture build failed:\n{}",
|
||||
String::from_utf8_lossy(&build.stderr)
|
||||
);
|
||||
let run = aura(&["run", "x.json"], &dir);
|
||||
assert_eq!(
|
||||
run.status.code(),
|
||||
Some(1),
|
||||
"an undescribed vocabulary entry is a runtime refusal (exit 1), \
|
||||
not a usage error"
|
||||
);
|
||||
let err = String::from_utf8_lossy(&run.stderr);
|
||||
assert!(
|
||||
err.contains("vocabulary entry `und::Opaque` has an empty doc"),
|
||||
"stderr must name the entry and the rule: {err}"
|
||||
);
|
||||
assert!(err.contains("C29"), "stderr must cite the contract: {err}");
|
||||
}
|
||||
|
||||
/// C29 load seam end-to-end, the sibling fault arm (#316): a doc that merely
|
||||
/// restates its own type id (`DocGateFault::RestatesName`) refuses at load
|
||||
/// exactly like the empty-doc case above, through the same real
|
||||
/// `aura-cli::project::load` path. `RestatesName`'s rendered prose was
|
||||
/// previously pinned only against the pure `Display` impl in
|
||||
/// `aura-runner::project`'s unit tests (never reachable at all through the
|
||||
/// CLI) — a regression that reordered `doc_gate`'s two fault arms, or that
|
||||
/// skipped the gate call for this arm specifically, would pass every
|
||||
/// existing test and still ship a self-description hole for exactly this
|
||||
/// case: an alibi doc that types-checks but says nothing.
|
||||
#[test]
|
||||
fn restated_name_vocabulary_entry_refuses_end_to_end() {
|
||||
let dir = restated_dir();
|
||||
let build = Command::new("cargo")
|
||||
.arg("build")
|
||||
.current_dir(&dir)
|
||||
.output()
|
||||
.expect("spawn cargo build for the restated fixture");
|
||||
assert!(
|
||||
build.status.success(),
|
||||
"restated fixture build failed:\n{}",
|
||||
String::from_utf8_lossy(&build.stderr)
|
||||
);
|
||||
let run = aura(&["run", "x.json"], &dir);
|
||||
assert_eq!(
|
||||
run.status.code(),
|
||||
Some(1),
|
||||
"a name-restating vocabulary entry is a runtime refusal (exit 1), \
|
||||
not a usage error"
|
||||
);
|
||||
let err = String::from_utf8_lossy(&run.stderr);
|
||||
assert!(
|
||||
err.contains("vocabulary entry `restated::Echo` has a doc that merely restates its name"),
|
||||
"stderr must name the entry and the rule: {err}"
|
||||
);
|
||||
assert!(err.contains("C29"), "stderr must cite the contract: {err}");
|
||||
}
|
||||
|
||||
/// Force a file's modification time to a fixed instant (explicit mtime, no
|
||||
/// sleeping — mtime granularity would make a "touch then compare" race).
|
||||
fn set_mtime(path: &Path, secs_since_epoch: u64) {
|
||||
|
||||
@@ -211,3 +211,33 @@ fn attached_crate_namespace_resolves_after_build() {
|
||||
"{}", String::from_utf8_lossy(&out.stdout)
|
||||
);
|
||||
}
|
||||
|
||||
/// Property (#316 self-description): the scaffold's own seed node ships a
|
||||
/// `doc:` line that itself passes the C29 shape gate (`aura_core::doc_gate`)
|
||||
/// — non-empty and not a bare restatement of the type name. This exact text
|
||||
/// is what a new extension author copy-pastes as the very first node of
|
||||
/// their crate; if the scaffold's own doc failed the gate it would teach the
|
||||
/// wrong pattern (an alibi doc) to every project built from it, and nothing
|
||||
/// else would catch that — `NodeSchema.doc` is required at compile time
|
||||
/// (E0063 without it) but its *shape* is never checked by the compiler.
|
||||
#[test]
|
||||
fn nodes_new_scaffold_doc_passes_the_doc_gate() {
|
||||
let cwd = temp_cwd("doc-gate");
|
||||
assert!(aura(&["new", "lab"], &cwd).status.success());
|
||||
let proj = cwd.join("lab");
|
||||
let engine = env!("CARGO_MANIFEST_DIR").to_string() + "/../..";
|
||||
assert!(aura(&["nodes", "new", "lab-nodes", "--engine-path", &engine], &proj).status.success());
|
||||
let lib = std::fs::read_to_string(cwd.join("lab-nodes/src/lib.rs")).unwrap();
|
||||
let doc_line = lib
|
||||
.lines()
|
||||
.find(|l| l.trim_start().starts_with("doc:"))
|
||||
.unwrap_or_else(|| panic!("scaffold must carry a doc: field: {lib}"));
|
||||
let doc_text = doc_line
|
||||
.split_once('"')
|
||||
.and_then(|(_, rest)| rest.rsplit_once('"'))
|
||||
.map(|(text, _)| text)
|
||||
.unwrap_or_else(|| panic!("doc: field must be a string literal: {doc_line}"));
|
||||
aura_core::doc_gate("lab_nodes::Scale", doc_text).unwrap_or_else(|f| {
|
||||
panic!("scaffold's own doc line fails its own gate: {f:?} — {doc_text:?}")
|
||||
});
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ const GER40_TO_MS: &str = "1727740799999";
|
||||
/// via `aura nodes new ... --namespace knob_lab`) supplies the `knob_lab`
|
||||
/// vocabulary namespace — `Scale` is the starter node every node-crate
|
||||
/// scaffold emits (#183).
|
||||
const OPEN_BLUEPRINT: &str = r#"{"format_version":1,"blueprint":{"name":"scaled_signal","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}},{"primitive":{"type":"knob_lab::Scale","name":"gain"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0},{"from":3,"to":4,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":4,"field":0,"name":"bias"}]}}"#;
|
||||
const OPEN_BLUEPRINT: &str = r#"{"format_version":1,"blueprint":{"name":"scaled_signal","doc":"fast/slow SMA bias scaled by an extension gain node","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}},{"primitive":{"type":"knob_lab::Scale","name":"gain"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0},{"from":3,"to":4,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":4,"field":0,"name":"bias"}]}}"#;
|
||||
|
||||
/// The minimal executable process pipeline (one sweep stage) — the campaign
|
||||
/// cell's methodology reference.
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
//! C29 coverage: every engine-shipped vocabulary entry carries a
|
||||
//! gate-passing one-line meaning. Blocks, metrics, tap slots, folds, and
|
||||
//! the std node vocabulary — the compile/unit seam of the self-description
|
||||
//! contract.
|
||||
|
||||
use aura_core::doc_gate;
|
||||
|
||||
#[test]
|
||||
fn every_shipped_vocabulary_entry_passes_the_doc_gate() {
|
||||
// process + campaign blocks (BlockSchema.doc — the pre-existing model)
|
||||
for b in aura_research::process_vocabulary()
|
||||
.iter()
|
||||
.chain(aura_research::campaign_vocabulary().iter())
|
||||
{
|
||||
doc_gate(b.id, b.doc)
|
||||
.unwrap_or_else(|f| panic!("block {}: {:?}", b.id, f));
|
||||
}
|
||||
// metrics
|
||||
for m in aura_research::metric_vocabulary() {
|
||||
doc_gate(m.id, m.doc)
|
||||
.unwrap_or_else(|f| panic!("metric {}: {:?}", m.id, f));
|
||||
}
|
||||
// tap slots
|
||||
for t in aura_research::tap_vocabulary() {
|
||||
doc_gate(t.id, t.doc)
|
||||
.unwrap_or_else(|f| panic!("tap {}: {:?}", t.id, f));
|
||||
}
|
||||
// tap folds (the #283 registry roster already carries docs)
|
||||
for (name, doc) in aura_runner::tap_plan::FoldRegistry::core().roster() {
|
||||
doc_gate(name, doc)
|
||||
.unwrap_or_else(|f| panic!("fold {}: {:?}", name, f));
|
||||
}
|
||||
// std node vocabulary: resolve each shipped type id to its schema
|
||||
let env = aura_runner::project::Env::std();
|
||||
for &type_id in aura_vocabulary::std_vocabulary_types() {
|
||||
let builder = env
|
||||
.resolve(type_id)
|
||||
.unwrap_or_else(|| panic!("std type {type_id} must resolve"));
|
||||
let schema = builder.schema();
|
||||
doc_gate(type_id, schema.doc)
|
||||
.unwrap_or_else(|f| panic!("node {type_id}: {:?}", f));
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ impl ConstLongBias {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "price".into() }],
|
||||
output: vec![FieldSpec { name: "bias".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "test-only schema",
|
||||
},
|
||||
|_| Box::new(ConstLongBias { out: [Cell::from_f64(0.0)] }),
|
||||
)
|
||||
|
||||
@@ -44,8 +44,8 @@ pub use column::{Column, Window};
|
||||
pub use ctx::Ctx;
|
||||
pub use error::KindMismatch;
|
||||
pub use node::{
|
||||
zip_params, BindOpError, BoundParam, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec,
|
||||
PrimitiveBuilder,
|
||||
doc_gate, zip_params, BindOpError, BoundParam, DocGateFault, FieldSpec, Firing, Node,
|
||||
NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder,
|
||||
};
|
||||
pub use scalar::{Scalar, ScalarKind, Timestamp};
|
||||
pub use series_fold::SeriesFold;
|
||||
|
||||
@@ -345,6 +345,36 @@ pub struct NodeSchema {
|
||||
pub inputs: Vec<PortSpec>,
|
||||
pub output: Vec<FieldSpec>,
|
||||
pub params: Vec<ParamSpec>,
|
||||
/// One-line meaning of the node — non-load-bearing metadata, shown by
|
||||
/// introspection surfaces.
|
||||
pub doc: &'static str,
|
||||
}
|
||||
|
||||
/// Shape fault of a vocabulary entry's meaning line (C29 gate).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DocGateFault {
|
||||
Empty,
|
||||
RestatesName,
|
||||
}
|
||||
|
||||
/// Deterministic shape check for a vocabulary entry's meaning line —
|
||||
/// string shape only, never content judgement (the engine must not
|
||||
/// evaluate prose).
|
||||
pub fn doc_gate(name: &str, doc: &str) -> Result<(), DocGateFault> {
|
||||
if doc.trim().is_empty() {
|
||||
return Err(DocGateFault::Empty);
|
||||
}
|
||||
fn norm(s: &str) -> String {
|
||||
s.chars()
|
||||
.filter(|c| c.is_ascii_alphanumeric())
|
||||
.map(|c| c.to_ascii_lowercase())
|
||||
.collect()
|
||||
}
|
||||
let short_name = name.rsplit("::").next().unwrap_or(name);
|
||||
if norm(doc) == norm(name) || norm(doc) == norm(short_name) {
|
||||
return Err(DocGateFault::RestatesName);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The universal composable dataflow unit (C8): a **producer, consumer, or both**.
|
||||
@@ -446,13 +476,14 @@ mod tests {
|
||||
inputs: vec![],
|
||||
output: vec![],
|
||||
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
doc: "test-only schema",
|
||||
},
|
||||
|_| Box::new(Bare),
|
||||
);
|
||||
assert_eq!(with.label(), "SMA");
|
||||
let none = PrimitiveBuilder::new(
|
||||
"Sub",
|
||||
NodeSchema { inputs: vec![], output: vec![], params: vec![] },
|
||||
NodeSchema { inputs: vec![], output: vec![], params: vec![], doc: "test-only schema" },
|
||||
|_| Box::new(Bare),
|
||||
);
|
||||
assert_eq!(none.label(), "Sub");
|
||||
@@ -482,7 +513,7 @@ mod tests {
|
||||
fn node_name_strips_namespace_prefix() {
|
||||
let b = PrimitiveBuilder::new(
|
||||
"demo::Identity",
|
||||
NodeSchema { inputs: vec![], output: vec![], params: vec![] },
|
||||
NodeSchema { inputs: vec![], output: vec![], params: vec![], doc: "test-only schema" },
|
||||
|_| unreachable!("never built in this test"),
|
||||
);
|
||||
assert_eq!(b.node_name(), "identity");
|
||||
@@ -514,7 +545,7 @@ mod tests {
|
||||
fn primitive_builder_build_runs_the_closure() {
|
||||
let f = PrimitiveBuilder::new(
|
||||
"Bare",
|
||||
NodeSchema { inputs: vec![], output: vec![], params: vec![] },
|
||||
NodeSchema { inputs: vec![], output: vec![], params: vec![], doc: "test-only schema" },
|
||||
|_| Box::new(Bare),
|
||||
);
|
||||
assert_eq!(f.params(), Vec::<ParamSpec>::new());
|
||||
@@ -529,6 +560,7 @@ mod tests {
|
||||
inputs: vec![],
|
||||
output: vec![],
|
||||
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
doc: "test-only schema",
|
||||
},
|
||||
|_| Box::new(Bare),
|
||||
);
|
||||
@@ -538,6 +570,21 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_carries_declared_doc() {
|
||||
let b = PrimitiveBuilder::new(
|
||||
"SMA",
|
||||
NodeSchema {
|
||||
inputs: vec![],
|
||||
output: vec![],
|
||||
params: vec![],
|
||||
doc: "simple moving average",
|
||||
},
|
||||
|_| Box::new(Bare),
|
||||
);
|
||||
assert_eq!(b.schema().doc, "simple moving average");
|
||||
}
|
||||
|
||||
/// A node whose label echoes the param vector its constructor received — lets a
|
||||
/// test read back the positional vector `.build()` reconstructed after binds.
|
||||
struct Probe(Vec<Cell>);
|
||||
@@ -565,6 +612,7 @@ mod tests {
|
||||
ParamSpec { name: "b".into(), kind: ScalarKind::I64 },
|
||||
ParamSpec { name: "c".into(), kind: ScalarKind::F64 },
|
||||
],
|
||||
doc: "test-only schema",
|
||||
},
|
||||
|p| Box::new(Probe(p.to_vec())),
|
||||
)
|
||||
@@ -641,6 +689,7 @@ mod tests {
|
||||
inputs: vec![],
|
||||
output: vec![],
|
||||
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
doc: "test-only schema",
|
||||
},
|
||||
|_| Box::new(Bare),
|
||||
);
|
||||
@@ -659,6 +708,7 @@ mod tests {
|
||||
ParamSpec { name: "dup".into(), kind: ScalarKind::I64 },
|
||||
ParamSpec { name: "dup".into(), kind: ScalarKind::I64 },
|
||||
],
|
||||
doc: "test-only schema",
|
||||
},
|
||||
|_| Box::new(Bare),
|
||||
);
|
||||
@@ -674,6 +724,7 @@ mod tests {
|
||||
inputs: vec![],
|
||||
output: vec![],
|
||||
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
doc: "test-only schema",
|
||||
},
|
||||
|_| Box::new(Bare),
|
||||
);
|
||||
@@ -725,6 +776,7 @@ mod tests {
|
||||
inputs: vec![],
|
||||
output: vec![],
|
||||
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
doc: "test-only schema",
|
||||
},
|
||||
|_| Box::new(Bare),
|
||||
)
|
||||
@@ -746,6 +798,43 @@ mod tests {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doc_gate_refuses_empty_and_whitespace() {
|
||||
assert_eq!(doc_gate("EMA", ""), Err(DocGateFault::Empty));
|
||||
assert_eq!(doc_gate("EMA", " \t"), Err(DocGateFault::Empty));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doc_gate_refuses_name_restatement() {
|
||||
assert_eq!(doc_gate("EMA", "EMA"), Err(DocGateFault::RestatesName));
|
||||
assert_eq!(doc_gate("EMA", "e_m_a"), Err(DocGateFault::RestatesName));
|
||||
assert_eq!(doc_gate("rolling_max", "Rolling Max"), Err(DocGateFault::RestatesName));
|
||||
}
|
||||
|
||||
/// C29 tightening (#316 fieldtest): for a namespaced entry, the bare
|
||||
/// display segment is a restatement too — a doc that adds nothing beyond
|
||||
/// the name must not pass just because the name carries a `::` prefix.
|
||||
/// Genuine meaning lines that merely *contain* the name stay legal.
|
||||
#[test]
|
||||
fn doc_gate_refuses_namespaced_short_name_restatement() {
|
||||
// The most natural alibi at the extension-author seam: the node's
|
||||
// own display name as its doc.
|
||||
assert_eq!(doc_gate("lab316_nodes::Scale", "Scale"), Err(DocGateFault::RestatesName));
|
||||
// Guard: a real meaning line stays Ok even when it contains the
|
||||
// short name as a substring...
|
||||
assert_eq!(
|
||||
doc_gate("lab316_nodes::Scale", "scalar gain: emits the input times the factor param"),
|
||||
Ok(())
|
||||
);
|
||||
// ...and a namespaced entry with a genuine doc is untouched.
|
||||
assert_eq!(doc_gate("und::Opaque", "one-input f64 pass-through"), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doc_gate_accepts_a_meaning_line() {
|
||||
assert_eq!(doc_gate("EMA", "exponential moving average over the input series"), Ok(()));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -129,7 +129,15 @@ fn derive_signature(c: &Composite) -> NodeSchema {
|
||||
.collect();
|
||||
let mut params = Vec::new();
|
||||
collect_params(c.nodes(), c.gangs(), "", &mut params);
|
||||
NodeSchema { inputs, output, params }
|
||||
NodeSchema {
|
||||
inputs,
|
||||
output,
|
||||
params,
|
||||
// A derived composite signature is graph wiring, not a vocabulary entry:
|
||||
// no seam walks this doc. The described surface is the composite's own
|
||||
// `doc`, gated at register (C29, register seam).
|
||||
doc: "",
|
||||
}
|
||||
}
|
||||
|
||||
/// The scalar kind of the interior input slot a composite target addresses,
|
||||
@@ -1838,6 +1846,7 @@ mod tests {
|
||||
inputs: vec![],
|
||||
output: vec![FieldSpec { name: "v".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "test-only schema",
|
||||
};
|
||||
let to = NodeSchema {
|
||||
inputs: vec![
|
||||
@@ -1846,6 +1855,7 @@ mod tests {
|
||||
],
|
||||
output: vec![],
|
||||
params: vec![],
|
||||
doc: "test-only schema",
|
||||
};
|
||||
assert!(edge_kind_check(&from, 0, &to, 0).is_ok());
|
||||
assert_eq!(
|
||||
@@ -2269,7 +2279,7 @@ mod tests {
|
||||
fn pass1() -> BlueprintNode {
|
||||
PrimitiveBuilder::new(
|
||||
"Pass1",
|
||||
NodeSchema { inputs: vec![f64_any()], output: out_v(), params: vec![] },
|
||||
NodeSchema { inputs: vec![f64_any()], output: out_v(), params: vec![], doc: "test-only schema" },
|
||||
|_| Box::new(Pass1 { out: [Cell::from_f64(0.0)] }),
|
||||
)
|
||||
.into()
|
||||
@@ -2277,7 +2287,12 @@ mod tests {
|
||||
fn join2() -> BlueprintNode {
|
||||
PrimitiveBuilder::new(
|
||||
"Join2",
|
||||
NodeSchema { inputs: vec![f64_any(), f64_any()], output: out_v(), params: vec![] },
|
||||
NodeSchema {
|
||||
inputs: vec![f64_any(), f64_any()],
|
||||
output: out_v(),
|
||||
params: vec![],
|
||||
doc: "test-only schema",
|
||||
},
|
||||
|_| Box::new(Join2 { out: [Cell::from_f64(0.0)] }),
|
||||
)
|
||||
.into()
|
||||
@@ -2285,7 +2300,7 @@ mod tests {
|
||||
fn sink_f64() -> BlueprintNode {
|
||||
PrimitiveBuilder::new(
|
||||
"SinkF64",
|
||||
NodeSchema { inputs: vec![f64_any()], output: vec![], params: vec![] },
|
||||
NodeSchema { inputs: vec![f64_any()], output: vec![], params: vec![], doc: "test-only schema" },
|
||||
|_| Box::new(SinkF64),
|
||||
)
|
||||
.into()
|
||||
@@ -2297,6 +2312,7 @@ mod tests {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::I64, firing: Firing::Any, name: "in".into() }],
|
||||
output: vec![],
|
||||
params: vec![],
|
||||
doc: "test-only schema",
|
||||
},
|
||||
|_| Box::new(SinkI64),
|
||||
)
|
||||
@@ -2849,6 +2865,7 @@ mod tests {
|
||||
inputs: vec![f64_any()],
|
||||
output: vec![],
|
||||
params: vec![],
|
||||
doc: "test-only schema",
|
||||
};
|
||||
let h = Harness::bootstrap(FlatGraph {
|
||||
nodes: vec![
|
||||
@@ -3454,6 +3471,7 @@ mod tests {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::I64, firing: Firing::Any, name: "in".into() }],
|
||||
output: vec![FieldSpec { name: "v".into(), kind: ScalarKind::I64 }],
|
||||
params: vec![],
|
||||
doc: "test-only schema",
|
||||
},
|
||||
|_| panic!("build must not run when validation fails pre-build"),
|
||||
);
|
||||
@@ -3556,7 +3574,7 @@ mod tests {
|
||||
// cannot take .named).
|
||||
let paramless_sma = PrimitiveBuilder::new(
|
||||
"Pass1",
|
||||
NodeSchema { inputs: vec![f64_any()], output: out_v(), params: vec![] },
|
||||
NodeSchema { inputs: vec![f64_any()], output: out_v(), params: vec![], doc: "test-only schema" },
|
||||
|_| Box::new(Pass1 { out: [Cell::from_f64(0.0)] }),
|
||||
)
|
||||
.named("sma");
|
||||
|
||||
@@ -42,6 +42,10 @@ pub enum Op {
|
||||
Tap { from: String, as_name: String },
|
||||
/// Fuse two or more sibling params into one public knob (#61).
|
||||
Gang { as_name: String, into: Vec<String> },
|
||||
/// Declare the composite's one-line meaning (C29, #316) — the op-script
|
||||
/// twin of the builder's `.doc(...)`. At most one per script; a second
|
||||
/// is a fault (a declarative script carries no last-wins ambiguity).
|
||||
Doc { text: String },
|
||||
}
|
||||
|
||||
/// A per-op construction fault, by-identifier so the cause names the op.
|
||||
@@ -92,6 +96,8 @@ pub enum OpError {
|
||||
/// A holistic finalize fault (totality / injectivity / unbound root role),
|
||||
/// wrapping the unchanged engine gate's `CompileError`.
|
||||
Incomplete(CompileError),
|
||||
/// A second `doc` op — the meaning line is declared at most once.
|
||||
DuplicateDoc,
|
||||
}
|
||||
|
||||
/// A per-op-fallible blueprint accumulator. Holds the same interior data a
|
||||
@@ -113,6 +119,7 @@ pub struct GraphSession<'v> {
|
||||
tap_names: HashSet<String>,
|
||||
coverage: HashMap<(usize, usize), usize>,
|
||||
gangs: Vec<Gang>,
|
||||
doc: Option<String>,
|
||||
}
|
||||
|
||||
impl<'v> GraphSession<'v> {
|
||||
@@ -135,6 +142,7 @@ impl<'v> GraphSession<'v> {
|
||||
tap_names: HashSet::new(),
|
||||
coverage: HashMap::new(),
|
||||
gangs: Vec::new(),
|
||||
doc: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +158,13 @@ impl<'v> GraphSession<'v> {
|
||||
Op::Expose { from, as_name } => self.expose(from, as_name),
|
||||
Op::Tap { from, as_name } => self.tap(from, as_name),
|
||||
Op::Gang { as_name, into } => self.gang(as_name, into),
|
||||
Op::Doc { text } => {
|
||||
if self.doc.is_some() {
|
||||
return Err(OpError::DuplicateDoc);
|
||||
}
|
||||
self.doc = Some(text);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,13 +439,14 @@ impl<'v> GraphSession<'v> {
|
||||
.into_iter()
|
||||
.map(|(name, source, targets)| Role { name, targets, source })
|
||||
.collect();
|
||||
// No doc is attached here: the op-script vocabulary deliberately has no
|
||||
// doc-carrying surface yet (#125 scope cut) — an op-built composite
|
||||
// carries no authored rationale until that vocabulary grows a slot.
|
||||
let c = Composite::new(self.name, self.nodes, self.edges, roles, self.out)
|
||||
// The doc op (C29, #316) threads through `Composite::with_doc`.
|
||||
let mut c = Composite::new(self.name, self.nodes, self.edges, roles, self.out)
|
||||
.with_taps(self.taps)
|
||||
.with_gangs(self.gangs)
|
||||
.map_err(OpError::Incomplete)?;
|
||||
if let Some(t) = self.doc {
|
||||
c = c.with_doc(t);
|
||||
}
|
||||
check_param_namespace_injective(&c.param_space()).map_err(OpError::Incomplete)?;
|
||||
validate_wiring(c.nodes(), c.edges(), c.input_roles(), c.output(), c.taps()).map_err(|e| match e {
|
||||
CompileError::UnconnectedPort { node, slot } => OpError::UnconnectedPort {
|
||||
@@ -866,6 +882,32 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// C29 (#316): the doc op sets the composite's meaning line — the
|
||||
/// op-script twin of the builder's .doc(...).
|
||||
#[test]
|
||||
fn doc_op_sets_the_composite_doc() {
|
||||
let mut s = scaffold();
|
||||
s.apply(Op::Doc { text: "a described op-built composite".into() }).unwrap();
|
||||
s.apply(Op::Feed { role: "price".into(), into: vec!["fast.series".into(), "slow.series".into()] })
|
||||
.unwrap();
|
||||
s.apply(Op::Connect { from: "fast.value".into(), to: "sub.lhs".into() }).unwrap();
|
||||
s.apply(Op::Connect { from: "slow.value".into(), to: "sub.rhs".into() }).unwrap();
|
||||
s.apply(Op::Expose { from: "sub.value".into(), as_name: "out".into() }).unwrap();
|
||||
let c = s.finish().expect("finishes");
|
||||
assert_eq!(c.doc(), Some("a described op-built composite"));
|
||||
}
|
||||
|
||||
/// A second doc op is a fault, not a silent overwrite.
|
||||
#[test]
|
||||
fn duplicate_doc_op_is_refused() {
|
||||
let mut s = scaffold();
|
||||
s.apply(Op::Doc { text: "first".into() }).unwrap();
|
||||
assert_eq!(
|
||||
s.apply(Op::Doc { text: "second".into() }),
|
||||
Err(OpError::DuplicateDoc)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finish_reports_incomplete_on_unwired_slot() {
|
||||
// a fully-named scaffold missing the sub.rhs connection -> holistic 0-arm
|
||||
|
||||
@@ -348,6 +348,7 @@ mod tests {
|
||||
],
|
||||
output: vec![FieldSpec { name: "diff".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "test-only schema",
|
||||
},
|
||||
|_| Box::new(Bare),
|
||||
)
|
||||
@@ -462,6 +463,7 @@ mod tests {
|
||||
FieldSpec { name: "lo".into(), kind: ScalarKind::I64 },
|
||||
],
|
||||
params: vec![],
|
||||
doc: "test-only schema",
|
||||
},
|
||||
|_| Box::new(Bare),
|
||||
)
|
||||
|
||||
@@ -878,6 +878,7 @@ mod tests {
|
||||
inputs: kinds.iter().enumerate().map(|(i, &kind)| PortSpec { kind, firing, name: format!("col[{i}]") }).collect(),
|
||||
output: vec![],
|
||||
params: vec![],
|
||||
doc: "test-only schema",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -895,6 +896,7 @@ mod tests {
|
||||
inputs: vec![f64_port(Firing::Any), f64_port(Firing::Any)],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "test-only schema",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -924,6 +926,7 @@ mod tests {
|
||||
inputs: vec![f64_port(Firing::Barrier(0)), f64_port(Firing::Barrier(0))],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "test-only schema",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -951,6 +954,7 @@ mod tests {
|
||||
inputs: vec![f64_port(Firing::Any)],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::Bool }],
|
||||
params: vec![],
|
||||
doc: "test-only schema",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -984,6 +988,7 @@ mod tests {
|
||||
],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "test-only schema",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1022,6 +1027,7 @@ mod tests {
|
||||
FieldSpec { name: "volume".into(), kind: ScalarKind::F64 },
|
||||
],
|
||||
params: vec![],
|
||||
doc: "test-only schema",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1057,6 +1063,7 @@ mod tests {
|
||||
FieldSpec { name: "i".into(), kind: ScalarKind::I64 },
|
||||
],
|
||||
params: vec![],
|
||||
doc: "test-only schema",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1084,6 +1091,7 @@ mod tests {
|
||||
inputs: vec![f64_port(Firing::Any)],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "test-only schema",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2442,6 +2450,7 @@ mod tests {
|
||||
FieldSpec { name: "i".into(), kind: ScalarKind::I64 },
|
||||
],
|
||||
params: vec![],
|
||||
doc: "test-only schema",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ fn recorder_sig(kinds: &[ScalarKind]) -> NodeSchema {
|
||||
.collect(),
|
||||
output: vec![],
|
||||
params: vec![],
|
||||
doc: "test-only schema",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ fn run_sample_over(prices: Vec<(Timestamp, Scalar)>) -> RunReport {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "in".into() }],
|
||||
output: vec![],
|
||||
params: vec![],
|
||||
doc: "test-only schema",
|
||||
};
|
||||
let mut h = Harness::bootstrap(FlatGraph {
|
||||
nodes: vec![
|
||||
|
||||
@@ -35,6 +35,7 @@ fn run_sample(window: (Timestamp, Timestamp), source: Box<dyn Source>) -> RunRep
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "in".into() }],
|
||||
output: vec![],
|
||||
params: vec![],
|
||||
doc: "test-only schema",
|
||||
};
|
||||
let mut h = Harness::bootstrap(FlatGraph {
|
||||
nodes: vec![
|
||||
|
||||
@@ -88,6 +88,7 @@ impl Resample {
|
||||
FieldSpec { name: "close".into(), kind: ScalarKind::F64 },
|
||||
],
|
||||
params: vec![ParamSpec { name: "period_minutes".into(), kind: ScalarKind::I64 }],
|
||||
doc: "emits a completed bar once per resample boundary, never mid-bar",
|
||||
},
|
||||
|p| Box::new(Resample::new(p[0].i64())),
|
||||
)
|
||||
|
||||
@@ -72,6 +72,7 @@ impl Session {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "trigger".into() }],
|
||||
output: vec![FieldSpec { name: "bars_since_open".into(), kind: ScalarKind::I64 }],
|
||||
params: vec![],
|
||||
doc: "bars elapsed since the session open, from the configured open time and timezone",
|
||||
},
|
||||
move |_| Box::new(Session::new(open_hour, open_minute, tz, period_minutes)),
|
||||
)
|
||||
@@ -99,6 +100,7 @@ impl SessionFrankfurt {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "trigger".into() }],
|
||||
output: vec![FieldSpec { name: "bars_since_open".into(), kind: ScalarKind::I64 }],
|
||||
params: vec![ParamSpec { name: "period_minutes".into(), kind: ScalarKind::I64 }],
|
||||
doc: "bars elapsed since the Frankfurt session open (09:00 Europe/Berlin)",
|
||||
},
|
||||
|p| Box::new(Session::new(9, 0, chrono_tz::Europe::Berlin, p[0].i64())),
|
||||
)
|
||||
|
||||
@@ -21,8 +21,8 @@ use aura_core::PrimitiveBuilder;
|
||||
use aura_analysis::{one_sided_p_laplace, MetricStats, MetricVocabulary, SplitMix64};
|
||||
use aura_backtest::{RunMetrics, RunReport};
|
||||
use aura_engine::{
|
||||
blueprint_from_json, blueprint_identity_json, expected_max_of_normals, FamilySelection,
|
||||
SelectionMode,
|
||||
blueprint_from_json, blueprint_identity_json, expected_max_of_normals, BlueprintDoc,
|
||||
CompositeData, FamilySelection, NodeData, SelectionMode,
|
||||
};
|
||||
use aura_research::{CampaignDoc, DocRef};
|
||||
|
||||
@@ -143,12 +143,26 @@ impl Registry {
|
||||
/// so a repeated write re-writes identical content. The registry does NOT
|
||||
/// verify `sha256(bytes) == hash` (no `sha2` dep here): the caller owns the
|
||||
/// hash, and reproduction's bit-identical metric compare is the integrity check.
|
||||
pub fn put_blueprint(&self, hash: &str, canonical_json: &str) -> Result<(), RegistryError> {
|
||||
/// Raw write — C29-gated callers go through [`Registry::put_blueprint`]; this
|
||||
/// stays for store-mechanics tests and pre-C29 store content.
|
||||
fn put_blueprint_unchecked(&self, hash: &str, canonical_json: &str) -> Result<(), RegistryError> {
|
||||
fs::create_dir_all(self.blueprints_dir())?;
|
||||
fs::write(self.blueprint_path(hash), canonical_json)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write-once content-addressed put, C29-gated (#316): only a canonical
|
||||
/// blueprint whose root composite and every named nested composite carry
|
||||
/// a gate-passing doc enters the store. The gate guards the store
|
||||
/// boundary itself — verb, sugar, and any future caller alike — and the
|
||||
/// write path only: already-registered doc-less entries stay readable.
|
||||
pub fn put_blueprint(&self, hash: &str, canonical_json: &str) -> Result<(), RegistryError> {
|
||||
let doc: BlueprintDoc = serde_json::from_str(canonical_json)
|
||||
.map_err(RegistryError::MalformedBlueprint)?;
|
||||
gate_composite_docs(&doc.blueprint)?;
|
||||
self.put_blueprint_unchecked(hash, canonical_json)
|
||||
}
|
||||
|
||||
/// Read a stored blueprint by content id; `Ok(None)` if absent — the same
|
||||
/// treat-as-empty discipline `load` applies to a missing runs store.
|
||||
pub fn get_blueprint(&self, hash: &str) -> Result<Option<String>, RegistryError> {
|
||||
@@ -259,6 +273,25 @@ impl Registry {
|
||||
}
|
||||
}
|
||||
|
||||
/// C29 walk (#316): the root and every named nested composite must carry a
|
||||
/// gate-passing doc. `doc: None` refuses exactly like `Some("")` — both are
|
||||
/// the Empty fault ("carries no doc").
|
||||
fn gate_composite_docs(c: &CompositeData) -> Result<(), RegistryError> {
|
||||
let fault = match c.doc.as_deref() {
|
||||
None => Some(aura_core::DocGateFault::Empty),
|
||||
Some(d) => aura_core::doc_gate(&c.name, d).err(),
|
||||
};
|
||||
if let Some(fault) = fault {
|
||||
return Err(RegistryError::UndescribedComposite { name: c.name.clone(), fault });
|
||||
}
|
||||
for n in &c.nodes {
|
||||
if let NodeData::Composite(nested) = n {
|
||||
gate_composite_docs(nested)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Referential-validation findings for a campaign document. By-identifier
|
||||
/// and Display-free (the CLI phrases them).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -897,6 +930,14 @@ pub enum RegistryError {
|
||||
NonRMetric(String),
|
||||
/// A generalization was asked for with fewer than two instruments.
|
||||
TooFewInstruments(usize),
|
||||
/// C29 register seam (#316): the canonical bytes offered to the blueprint
|
||||
/// store do not parse as a canonical blueprint — the gate cannot certify
|
||||
/// what it cannot read.
|
||||
MalformedBlueprint(serde_json::Error),
|
||||
/// C29 register seam (#316): a root or named nested composite entering
|
||||
/// the store without a gate-passing doc. Registered artifacts are never
|
||||
/// retroactively invalidated — the gate guards the write path only.
|
||||
UndescribedComposite { name: String, fault: aura_core::DocGateFault },
|
||||
}
|
||||
|
||||
impl fmt::Display for RegistryError {
|
||||
@@ -920,6 +961,23 @@ impl fmt::Display for RegistryError {
|
||||
f,
|
||||
"a generalization needs >= 2 instruments, got {n}"
|
||||
),
|
||||
RegistryError::MalformedBlueprint(e) => {
|
||||
write!(f, "blueprint: not a canonical blueprint: {e}")
|
||||
}
|
||||
RegistryError::UndescribedComposite { name, fault } => match fault {
|
||||
aura_core::DocGateFault::Empty => write!(
|
||||
f,
|
||||
"blueprint: composite `{name}` carries no doc — a registered \
|
||||
composite describes itself (C29); add a doc line (builder \
|
||||
.doc(\"...\"), blueprint \"doc\" member, or op-script \
|
||||
{{\"op\":\"doc\"}}) before register"
|
||||
),
|
||||
aura_core::DocGateFault::RestatesName => write!(
|
||||
f,
|
||||
"blueprint: composite `{name}` has a doc that merely restates \
|
||||
its name — a registered composite describes itself (C29)"
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1070,13 +1128,50 @@ mod tests {
|
||||
fn blueprint_store_round_trips_by_content_id() {
|
||||
let reg = Registry::open(temp_family_dir("blueprint_store_round_trip"));
|
||||
let canonical = r#"{"format_version":1,"input_roles":[],"nodes":[],"edges":[]}"#;
|
||||
reg.put_blueprint("deadbeef", canonical).expect("put");
|
||||
reg.put_blueprint_unchecked("deadbeef", canonical).expect("put");
|
||||
// exact bytes come back, keyed by content id
|
||||
assert_eq!(reg.get_blueprint("deadbeef").expect("get"), Some(canonical.to_string()));
|
||||
// an absent id is Ok(None), not an error (treat-as-empty discipline)
|
||||
assert_eq!(reg.get_blueprint("never-written").expect("get"), None);
|
||||
}
|
||||
|
||||
/// C29 (#316): the store refuses a doc-less root (None and "" alike),
|
||||
/// a name-restating doc, and admits a described composite. The raw
|
||||
/// unchecked path still round-trips pre-C29 doc-less bytes — registered
|
||||
/// artifacts are never retroactively invalidated.
|
||||
#[test]
|
||||
fn blueprint_store_gates_docs_at_the_write_boundary() {
|
||||
let reg = Registry::open(temp_family_dir("blueprint_store_doc_gate"));
|
||||
let docless = r#"{"format_version":1,"blueprint":{"name":"s","nodes":[]}}"#;
|
||||
let e = reg.put_blueprint("h1", docless).expect_err("doc-less root must refuse");
|
||||
assert!(
|
||||
e.to_string().contains("composite `s` carries no doc"),
|
||||
"prose names the composite and the rule: {e}"
|
||||
);
|
||||
let restated = r#"{"format_version":1,"blueprint":{"name":"echo","doc":"Echo","nodes":[]}}"#;
|
||||
let e = reg.put_blueprint("h2", restated).expect_err("restated doc must refuse");
|
||||
assert!(e.to_string().contains("merely restates"), "prose names the rule: {e}");
|
||||
let described = r#"{"format_version":1,"blueprint":{"name":"s","doc":"a described fixture blueprint","nodes":[]}}"#;
|
||||
reg.put_blueprint("h3", described).expect("described root registers");
|
||||
assert_eq!(reg.get_blueprint("h3").expect("get"), Some(described.to_string()));
|
||||
// never-retroactive witness: a pre-C29 doc-less entry stays readable
|
||||
reg.put_blueprint_unchecked("h4", docless).expect("raw write");
|
||||
assert_eq!(reg.get_blueprint("h4").expect("get"), Some(docless.to_string()));
|
||||
}
|
||||
|
||||
/// A doc-less named NESTED composite refuses even when the root is
|
||||
/// described — the walk covers every named nested composite (C29).
|
||||
#[test]
|
||||
fn blueprint_store_gates_nested_composite_docs() {
|
||||
let reg = Registry::open(temp_family_dir("blueprint_store_nested_doc_gate"));
|
||||
let nested_docless = r#"{"format_version":1,"blueprint":{"name":"root","doc":"a described root","nodes":[{"composite":{"name":"inner","nodes":[]}}]}}"#;
|
||||
let e = reg.put_blueprint("h1", nested_docless).expect_err("doc-less nested must refuse");
|
||||
assert!(
|
||||
e.to_string().contains("composite `inner` carries no doc"),
|
||||
"prose names the nested composite: {e}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_stores_round_trip_by_content_id() {
|
||||
let reg = Registry::open(temp_family_dir("document_store_round_trip"));
|
||||
@@ -1810,7 +1905,8 @@ mod tests {
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
);
|
||||
)
|
||||
.with_doc("fixture composite for the registry seeder test");
|
||||
let space = composite.param_space();
|
||||
let real = space.first().expect("fixture composite has an open param");
|
||||
let real_name = real.name.clone();
|
||||
@@ -1941,6 +2037,7 @@ mod tests {
|
||||
vec![],
|
||||
vec![],
|
||||
)
|
||||
.with_doc("fixture composite for the registry seeder test")
|
||||
}
|
||||
|
||||
/// A second, identity-distinct fixture: same shape over the zero-arg
|
||||
@@ -1955,6 +2052,7 @@ mod tests {
|
||||
vec![],
|
||||
vec![],
|
||||
)
|
||||
.with_doc("fixture composite for the registry seeder test")
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2095,7 +2193,8 @@ mod tests {
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
);
|
||||
)
|
||||
.with_doc("fixture composite for the registry seeder test");
|
||||
let (_json_b, content_b, identity_twin) = seed_blueprint(®, &twin);
|
||||
assert_eq!(identity, identity_twin, "twins must share one identity id");
|
||||
assert_ne!(content_a, content_b, "twins must have distinct content ids");
|
||||
@@ -2150,7 +2249,8 @@ mod tests {
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
);
|
||||
)
|
||||
.with_doc("fixture composite for the registry seeder test");
|
||||
assert!(composite.param_space().is_empty(), "fixture is fully bound");
|
||||
let bound = composite.bound_param_space();
|
||||
let bound_param = bound.first().expect("fixture has one bound param");
|
||||
@@ -2243,7 +2343,8 @@ mod tests {
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
);
|
||||
)
|
||||
.with_doc("fixture composite for the registry seeder test");
|
||||
let space = composite.param_space();
|
||||
assert_eq!(space.len(), 2, "fixture exposes two open params");
|
||||
let covered = space[0].name.clone(); // e.g. "b1.scale"
|
||||
@@ -2316,7 +2417,8 @@ mod tests {
|
||||
source: Some(aura_core::ScalarKind::F64),
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
)
|
||||
.with_doc("fixture composite for the registry seeder test");
|
||||
let space = composite.param_space();
|
||||
let axis = space.first().expect("fixture has an open param").name.clone();
|
||||
let blueprint_json = blueprint_to_json(&composite).expect("serializes");
|
||||
|
||||
+114
-28
@@ -13,7 +13,7 @@
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use aura_core::{Scalar, ScalarKind};
|
||||
use aura_core::{doc_gate, DocGateFault, Scalar, ScalarKind};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The one accepted document format version.
|
||||
@@ -727,6 +727,14 @@ pub fn content_id_of(canonical: &str) -> String {
|
||||
// Intrinsic validation (store-free).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A shipped metric with its one-line meaning (C29). One carrier — the
|
||||
/// #190 guard keeps this list, the RMetrics serde fields, and the CLI
|
||||
/// count in lockstep; the doc column adds no fourth roster site.
|
||||
pub struct MetricSchema {
|
||||
pub id: &'static str,
|
||||
pub doc: &'static str,
|
||||
}
|
||||
|
||||
/// The closed declared-metric vocabulary: the scalar field names of the
|
||||
/// shipped metrics/selection types (RMetrics + RunMetrics scalars, plus the
|
||||
/// FamilySelection annotation scores). Single source for gate/stage checks
|
||||
@@ -746,25 +754,25 @@ pub fn content_id_of(canonical: &str) -> String {
|
||||
/// hand when the upstream field sets change (this list stays a hand-copy by
|
||||
/// the crate's ratified core-only isolation; #147 single-sourced the rankable
|
||||
/// roster elsewhere and the extended #190 guard pins this list's nesting).
|
||||
pub fn metric_vocabulary() -> &'static [&'static str] {
|
||||
pub fn metric_vocabulary() -> &'static [MetricSchema] {
|
||||
&[
|
||||
"expectancy_r",
|
||||
"win_rate",
|
||||
"avg_win_r",
|
||||
"avg_loss_r",
|
||||
"profit_factor",
|
||||
"max_r_drawdown",
|
||||
"sqn",
|
||||
"sqn_normalized",
|
||||
"net_expectancy_r",
|
||||
"n_trades",
|
||||
"n_open_at_end",
|
||||
"total_pips",
|
||||
"max_drawdown",
|
||||
"bias_sign_flips",
|
||||
"deflated_score",
|
||||
"overfit_probability",
|
||||
"neighbourhood_score",
|
||||
MetricSchema { id: "expectancy_r", doc: "mean realized R per closed trade — the headline E[R]" },
|
||||
MetricSchema { id: "win_rate", doc: "fraction of closed trades with positive realized R" },
|
||||
MetricSchema { id: "avg_win_r", doc: "mean realized R over winning trades" },
|
||||
MetricSchema { id: "avg_loss_r", doc: "mean realized R over losing trades" },
|
||||
MetricSchema { id: "profit_factor", doc: "gross winning R divided by absolute gross losing R" },
|
||||
MetricSchema { id: "max_r_drawdown", doc: "deepest peak-to-trough decline of the cumulative R curve" },
|
||||
MetricSchema { id: "sqn", doc: "System Quality Number: mean R over its std deviation, scaled by trade count" },
|
||||
MetricSchema { id: "sqn_normalized", doc: "SQN normalized to a 100-trade basis for n-independent comparison" },
|
||||
MetricSchema { id: "net_expectancy_r", doc: "expectancy net of the cost model's per-trade cost in R" },
|
||||
MetricSchema { id: "n_trades", doc: "count of closed trades in the run" },
|
||||
MetricSchema { id: "n_open_at_end", doc: "positions still open when the run ends" },
|
||||
MetricSchema { id: "total_pips", doc: "summed pip result over all closed trades" },
|
||||
MetricSchema { id: "max_drawdown", doc: "deepest peak-to-trough decline of the pip equity curve" },
|
||||
MetricSchema { id: "bias_sign_flips", doc: "count of sign changes in the strategy's bias stream" },
|
||||
MetricSchema { id: "deflated_score", doc: "selection score deflated for multiple testing against the bootstrap null" },
|
||||
MetricSchema { id: "overfit_probability", doc: "probability the selected edge is overfit, from the bootstrap null" },
|
||||
MetricSchema { id: "neighbourhood_score", doc: "plateau robustness: score aggregated over the param-space neighbourhood" },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -773,12 +781,23 @@ pub fn emit_vocabulary() -> &'static [&'static str] {
|
||||
&["family_table", "selection_report"]
|
||||
}
|
||||
|
||||
/// A closed-vocabulary persisted-tap slot with its one-line meaning (C29).
|
||||
pub struct TapSchema {
|
||||
pub id: &'static str,
|
||||
pub doc: &'static str,
|
||||
}
|
||||
|
||||
/// The wrap convention's persisted sink names — the closed tap vocabulary
|
||||
/// (#201 decision 1). A genuinely new observable becomes a new entry or an
|
||||
/// authored sink in the strategy blueprint (C22: the choice of sinks is part
|
||||
/// of the experiment) — never an open node-path namespace in the document.
|
||||
pub fn tap_vocabulary() -> &'static [&'static str] {
|
||||
&["equity", "exposure", "r_equity", "net_r_equity"]
|
||||
pub fn tap_vocabulary() -> &'static [TapSchema] {
|
||||
&[
|
||||
TapSchema { id: "equity", doc: "cumulative pip equity per cycle" },
|
||||
TapSchema { id: "exposure", doc: "signed position exposure per cycle" },
|
||||
TapSchema { id: "r_equity", doc: "cumulative gross R per cycle" },
|
||||
TapSchema { id: "net_r_equity", doc: "cumulative net R per cycle, after the cost model" },
|
||||
]
|
||||
}
|
||||
|
||||
/// The closed archive-column vocabulary a campaign `data.bindings` VALUE may
|
||||
@@ -821,16 +840,24 @@ pub enum DocFault {
|
||||
UnknownTap { index: usize, tap: String },
|
||||
UnknownBindingColumn { role: String, column: String },
|
||||
ProcessRefMustBeContentId,
|
||||
/// C29 (#316): a document's optional top-level `description` is present
|
||||
/// but fails the shape gate. Absent is never a fault.
|
||||
BadDescription { subject: String, fault: DocGateFault },
|
||||
}
|
||||
|
||||
fn is_known_metric(name: &str) -> bool {
|
||||
metric_vocabulary().contains(&name)
|
||||
metric_vocabulary().iter().any(|m| m.id == name)
|
||||
}
|
||||
|
||||
/// Intrinsic validation of a process document (P1 constraints included).
|
||||
/// Returns ALL findings, not the first.
|
||||
pub fn validate_process(doc: &ProcessDoc) -> Vec<DocFault> {
|
||||
let mut faults = Vec::new();
|
||||
if let Some(desc) = &doc.description
|
||||
&& let Err(fault) = doc_gate(&doc.name, desc)
|
||||
{
|
||||
faults.push(DocFault::BadDescription { subject: doc.name.clone(), fault });
|
||||
}
|
||||
if doc.pipeline.is_empty() {
|
||||
faults.push(DocFault::EmptyPipeline);
|
||||
}
|
||||
@@ -894,6 +921,11 @@ pub fn validate_process(doc: &ProcessDoc) -> Vec<DocFault> {
|
||||
/// Returns ALL findings, not the first.
|
||||
pub fn validate_campaign(doc: &CampaignDoc) -> Vec<DocFault> {
|
||||
let mut faults = Vec::new();
|
||||
if let Some(desc) = &doc.description
|
||||
&& let Err(fault) = doc_gate(&doc.name, desc)
|
||||
{
|
||||
faults.push(DocFault::BadDescription { subject: doc.name.clone(), fault });
|
||||
}
|
||||
if doc.data.instruments.is_empty() {
|
||||
faults.push(DocFault::EmptyInstruments);
|
||||
}
|
||||
@@ -1000,7 +1032,7 @@ pub fn validate_campaign(doc: &CampaignDoc) -> Vec<DocFault> {
|
||||
}
|
||||
}
|
||||
for (i, t) in doc.presentation.persist_taps.iter().enumerate() {
|
||||
if !tap_vocabulary().contains(&t.as_str()) {
|
||||
if !tap_vocabulary().iter().any(|k| k.id == t.as_str()) {
|
||||
faults.push(DocFault::UnknownTap { index: i, tap: t.clone() });
|
||||
}
|
||||
}
|
||||
@@ -1280,7 +1312,7 @@ pub fn open_slots_campaign(text: &str) -> Result<Vec<OpenSlot>, DocError> {
|
||||
"presentation",
|
||||
format!(
|
||||
"required section: persist_taps ({}) + emit",
|
||||
tap_vocabulary().join(" | ")
|
||||
tap_vocabulary().iter().map(|t| t.id).collect::<Vec<_>>().join(" | ")
|
||||
),
|
||||
));
|
||||
}
|
||||
@@ -1881,6 +1913,56 @@ mod tests {
|
||||
assert_eq!(validate_process(&doc), Vec::new());
|
||||
}
|
||||
|
||||
/// C29 (#316): a present document description passes the shape gate;
|
||||
/// absent is never a fault. Both fault arms fire and carry the doc name.
|
||||
#[test]
|
||||
fn document_description_gate_flags_empty_and_restatement() {
|
||||
let base = r#"{"format_version":1,"kind":"process","name":"p","pipeline":[{"block":"std::generalize","metric":"sqn"}]}"#;
|
||||
let mut doc: ProcessDoc = serde_json::from_str(base).expect("fixture parses");
|
||||
assert!(validate_process(&doc).is_empty(), "no description → no fault");
|
||||
doc.description = Some(" ".into());
|
||||
assert!(matches!(
|
||||
validate_process(&doc).as_slice(),
|
||||
[DocFault::BadDescription { subject, fault: DocGateFault::Empty }] if subject == "p"
|
||||
));
|
||||
doc.description = Some("P".into());
|
||||
assert!(matches!(
|
||||
validate_process(&doc).as_slice(),
|
||||
[DocFault::BadDescription { fault: DocGateFault::RestatesName, .. }]
|
||||
));
|
||||
doc.description = Some("walk-forward validation methodology".into());
|
||||
assert!(validate_process(&doc).is_empty(), "described → clean");
|
||||
}
|
||||
|
||||
/// The campaign side takes the identical gate, exercised over the
|
||||
/// crate's existing valid campaign fixture (`CAMPAIGN_FIXTURE`, which
|
||||
/// validates clean — pinned by `validate_campaign_accepts_a_valid_risk_section`).
|
||||
#[test]
|
||||
fn campaign_description_gate_mirrors_process() {
|
||||
let mut doc: CampaignDoc = serde_json::from_str(CAMPAIGN_FIXTURE).unwrap();
|
||||
assert!(validate_campaign(&doc).is_empty(), "fixture validates clean");
|
||||
doc.description = Some("".into());
|
||||
assert!(matches!(
|
||||
validate_campaign(&doc).as_slice(),
|
||||
[DocFault::BadDescription { fault: DocGateFault::Empty, .. }]
|
||||
));
|
||||
doc.description = Some("GER40 walk-forward over the momentum stack".into());
|
||||
assert!(validate_campaign(&doc).is_empty(), "described → clean");
|
||||
}
|
||||
|
||||
/// C29 (#316): a present description participates in the content id
|
||||
/// (absent ⇒ byte-identical is pinned by the existing round-trips).
|
||||
#[test]
|
||||
fn document_description_participates_in_the_content_id() {
|
||||
let base = r#"{"format_version":1,"kind":"process","name":"p","pipeline":[{"block":"std::generalize","metric":"sqn"}]}"#;
|
||||
let bare: ProcessDoc = serde_json::from_str(base).unwrap();
|
||||
let mut described: ProcessDoc = serde_json::from_str(base).unwrap();
|
||||
described.description = Some("walk-forward validation methodology".into());
|
||||
let bare_id = content_id_of(&process_to_json(&bare));
|
||||
let described_id = content_id_of(&process_to_json(&described));
|
||||
assert_ne!(bare_id, described_id, "description must move the content id");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_process_accepts_the_fixture_and_reports_each_fault() {
|
||||
let ok = parse_process(PROCESS_FIXTURE).unwrap();
|
||||
@@ -2072,7 +2154,8 @@ mod tests {
|
||||
/// never an open node-path namespace in the document.
|
||||
#[test]
|
||||
fn tap_vocabulary_is_the_wrap_conventions_four_sink_names() {
|
||||
assert_eq!(tap_vocabulary(), ["equity", "exposure", "r_equity", "net_r_equity"]);
|
||||
let ids: Vec<&str> = tap_vocabulary().iter().map(|t| t.id).collect();
|
||||
assert_eq!(ids, ["equity", "exposure", "r_equity", "net_r_equity"]);
|
||||
}
|
||||
|
||||
/// Each `presentation.persist_taps` entry outside `tap_vocabulary()` is an
|
||||
@@ -2092,7 +2175,7 @@ mod tests {
|
||||
);
|
||||
// every in-vocabulary name passes
|
||||
let mut all = ok.clone();
|
||||
all.presentation.persist_taps = tap_vocabulary().iter().map(|t| t.to_string()).collect();
|
||||
all.presentation.persist_taps = tap_vocabulary().iter().map(|t| t.id.to_string()).collect();
|
||||
assert_eq!(validate_campaign(&all), Vec::new());
|
||||
}
|
||||
|
||||
@@ -2157,7 +2240,10 @@ mod tests {
|
||||
assert_eq!(slot.kind, SlotKind::TapKinds);
|
||||
assert_eq!(
|
||||
slot_kind_label(slot.kind),
|
||||
format!("list of: {}", tap_vocabulary().join(" | "))
|
||||
format!(
|
||||
"list of: {}",
|
||||
tap_vocabulary().iter().map(|t| t.id).collect::<Vec<_>>().join(" | ")
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
slot_kind_label(slot.kind),
|
||||
@@ -2188,7 +2274,7 @@ mod tests {
|
||||
pres.hint,
|
||||
format!(
|
||||
"required section: persist_taps ({}) + emit",
|
||||
tap_vocabulary().join(" | ")
|
||||
tap_vocabulary().iter().map(|t| t.id).collect::<Vec<_>>().join(" | ")
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -782,6 +782,7 @@ pub fn reopen_all(signal: Composite, overrides: &[String]) -> Composite {
|
||||
fn r_breakout_signal(channel: Option<i64>) -> Composite {
|
||||
use aura_std::{Delay, Gt, Latch};
|
||||
let mut g = GraphBuilder::new("r_breakout_signal");
|
||||
g.doc("rolling-extreme breakout latched into a long/short signal");
|
||||
let delay = g.add(Delay::builder().bind("lag", Scalar::i64(1)));
|
||||
let mut mx_b = RollingMax::builder().named("channel_hi");
|
||||
let mut mn_b = RollingMin::builder().named("channel_lo");
|
||||
@@ -827,6 +828,7 @@ fn r_breakout_signal(channel: Option<i64>) -> Composite {
|
||||
fn r_meanrev_signal(window: Option<i64>, band_k: Option<f64>) -> Composite {
|
||||
use aura_std::{Add, Ema, Gt, Latch, Mul, Scale, Sqrt};
|
||||
let mut g = GraphBuilder::new("r_meanrev_signal");
|
||||
g.doc("EMA deviation against a volatility band, latched into a mean-reversion signal");
|
||||
let (mut mean_b, mut var_b) =
|
||||
(Ema::builder().named("mean_window"), Ema::builder().named("var_window"));
|
||||
if let Some(n) = window {
|
||||
@@ -891,6 +893,7 @@ fn r_meanrev_signal(window: Option<i64>, band_k: Option<f64>) -> Composite {
|
||||
fn r_channel_signal(channel: Option<i64>) -> Composite {
|
||||
use aura_std::{Delay, Gt, Latch};
|
||||
let mut g = GraphBuilder::new("hl_channel");
|
||||
g.doc("prior high/low channel breaks latched into a directional signal");
|
||||
let delay_hi = g.add(Delay::builder().named("prev_high").bind("lag", Scalar::i64(1)));
|
||||
let delay_lo = g.add(Delay::builder().named("prev_low").bind("lag", Scalar::i64(1)));
|
||||
let mut mx_b = RollingMax::builder().named("channel_hi");
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! vocabulary charter. `Env` is the per-invocation context every verb reads:
|
||||
//! merged resolver, runs root, data path, provenance.
|
||||
|
||||
use aura_core::PrimitiveBuilder;
|
||||
use aura_core::{DocGateFault, PrimitiveBuilder, doc_gate};
|
||||
use aura_core::project::{
|
||||
AURA_DESCRIPTOR_MAGIC, AURA_DESCRIPTOR_VERSION, AURA_PROJECT_SYMBOL,
|
||||
CORE_VERSION, ProjectDescriptor, RUSTC_VERSION, StrSlice,
|
||||
@@ -63,6 +63,11 @@ pub enum ProjectError {
|
||||
/// handing off to `load_crate`, so this never reaches the cargo-metadata
|
||||
/// probe (whose raw os-error text would otherwise leak through).
|
||||
PointerDirMissing(PathBuf),
|
||||
/// C29 load seam (#316): a resolved extension-vocabulary entry whose doc
|
||||
/// fails the shape gate — the load refuses rather than admit an
|
||||
/// undescribed node. Field presence is compile-enforced; shape is not,
|
||||
/// so the gate carries the failed rule for the refusal prose.
|
||||
UndescribedVocabularyEntry { type_id: String, fault: DocGateFault },
|
||||
}
|
||||
|
||||
impl fmt::Display for ProjectError {
|
||||
@@ -100,6 +105,19 @@ impl fmt::Display for ProjectError {
|
||||
"node crate at `{pointer}` (from [nodes] in Aura.toml): {inner}"
|
||||
),
|
||||
Self::PointerDirMissing(p) => write!(f, "{} does not exist", p.display()),
|
||||
Self::UndescribedVocabularyEntry { type_id, fault } => {
|
||||
let rule = match fault {
|
||||
DocGateFault::Empty => "has an empty doc",
|
||||
DocGateFault::RestatesName => {
|
||||
"has a doc that merely restates its name"
|
||||
}
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
"vocabulary entry `{type_id}` {rule} — every vocabulary \
|
||||
entry ships a one-line meaning (C29)"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -579,6 +597,18 @@ fn load_crate(crate_root: &Path, release: bool) -> Result<NativeEnv, ProjectErro
|
||||
let type_id_list = (desc.type_ids)();
|
||||
check_charter(&namespace, type_id_list, &|t| resolver(t))?;
|
||||
|
||||
// C29 load seam (#316): every resolved entry must describe itself. The
|
||||
// charter cross-check above guarantees each listed id resolves, so a
|
||||
// `None` here is unreachable — skipped rather than panicked on (a
|
||||
// hostile dylib must never panic the host).
|
||||
for &t in type_id_list {
|
||||
if let Some(builder) = resolver(t) {
|
||||
doc_gate(t, builder.schema().doc).map_err(|fault| {
|
||||
ProjectError::UndescribedVocabularyEntry { type_id: t.to_string(), fault }
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(NativeEnv { namespace, dylib_sha256, resolver, type_id_list })
|
||||
}
|
||||
|
||||
@@ -903,4 +933,32 @@ mod tests {
|
||||
"older source: not stale"
|
||||
);
|
||||
}
|
||||
|
||||
/// The C29 load-seam refusal prose (`UndescribedVocabularyEntry`) names
|
||||
/// the entry and the failed rule for BOTH `DocGateFault` arms. Only
|
||||
/// `Empty` is reachable end-to-end today (the fixture in
|
||||
/// `aura-cli/tests/project_load.rs` uses `doc: ""`); `RestatesName` has
|
||||
/// no such fixture, so this pins its rendered message directly against
|
||||
/// `Display` — a regression there would otherwise pass every test.
|
||||
#[test]
|
||||
fn undescribed_vocabulary_entry_display_names_the_entry_and_rule() {
|
||||
let empty = ProjectError::UndescribedVocabularyEntry {
|
||||
type_id: "ns::Foo".to_string(),
|
||||
fault: DocGateFault::Empty,
|
||||
};
|
||||
assert_eq!(
|
||||
empty.to_string(),
|
||||
"vocabulary entry `ns::Foo` has an empty doc — every vocabulary \
|
||||
entry ships a one-line meaning (C29)"
|
||||
);
|
||||
let restates = ProjectError::UndescribedVocabularyEntry {
|
||||
type_id: "ns::Foo".to_string(),
|
||||
fault: DocGateFault::RestatesName,
|
||||
};
|
||||
assert_eq!(
|
||||
restates.to_string(),
|
||||
"vocabulary entry `ns::Foo` has a doc that merely restates its \
|
||||
name — every vocabulary entry ships a one-line meaning (C29)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ pub fn tap_channel(tap: &str) -> Option<TapChannel> {
|
||||
debug_assert!(
|
||||
aura_research::tap_vocabulary()
|
||||
.iter()
|
||||
.all(|t| matches!(*t, "equity" | "exposure" | "r_equity" | "net_r_equity")),
|
||||
.all(|t| matches!(t.id, "equity" | "exposure" | "r_equity" | "net_r_equity")),
|
||||
"tap_vocabulary drifted from the channels persist_campaign_traces routes"
|
||||
);
|
||||
match tap {
|
||||
|
||||
@@ -324,6 +324,7 @@ fn tap_sink_schema(kind: ScalarKind) -> NodeSchema {
|
||||
inputs: vec![PortSpec { kind, firing: Firing::Any, name: "in".to_string() }],
|
||||
output: vec![],
|
||||
params: vec![],
|
||||
doc: "internal recording sink bound to a declared tap",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ impl Abs {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "value".into() }],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "absolute value of the input series",
|
||||
},
|
||||
|_| Box::new(Abs::new()),
|
||||
)
|
||||
|
||||
@@ -38,6 +38,7 @@ impl Add {
|
||||
],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "elementwise sum of two input series",
|
||||
},
|
||||
|_| Box::new(Add::new()),
|
||||
)
|
||||
|
||||
@@ -37,6 +37,7 @@ impl And {
|
||||
],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::Bool }],
|
||||
params: vec![],
|
||||
doc: "logical AND of two boolean series",
|
||||
},
|
||||
|_| Box::new(And::new()),
|
||||
)
|
||||
|
||||
@@ -33,6 +33,7 @@ impl Const {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "clock".into() }],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![ParamSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
doc: "constant-valued stream from a single param",
|
||||
},
|
||||
|p| Box::new(Const::new(p[0].f64())),
|
||||
)
|
||||
|
||||
@@ -53,6 +53,7 @@ impl CumSum {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "series".into() }],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "running cumulative sum of the input series",
|
||||
},
|
||||
|_| Box::new(CumSum::new()),
|
||||
)
|
||||
|
||||
@@ -65,6 +65,7 @@ impl Delay {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "series".into() }],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![ParamSpec { name: "lag".into(), kind: ScalarKind::I64 }],
|
||||
doc: "emits the input from `lag` cycles ago — the explicit delay register (C9)",
|
||||
},
|
||||
|p| Box::new(Delay::new(p[0].i64() as usize)),
|
||||
)
|
||||
|
||||
@@ -30,6 +30,7 @@ impl Div {
|
||||
],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "elementwise quotient of two input series",
|
||||
},
|
||||
|_| Box::new(Div::new()),
|
||||
)
|
||||
|
||||
@@ -63,6 +63,7 @@ impl Ema {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "series".into() }],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
doc: "exponential moving average over the input series",
|
||||
},
|
||||
|p| Box::new(Ema::new(p[0].i64() as usize)),
|
||||
)
|
||||
|
||||
@@ -40,6 +40,7 @@ impl EqConst {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::I64, firing: Firing::Any, name: "value".into() }],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::Bool }],
|
||||
params: vec![ParamSpec { name: "target".into(), kind: ScalarKind::I64 }],
|
||||
doc: "true where the input equals a constant param",
|
||||
},
|
||||
|p| Box::new(EqConst::new(p[0].i64())),
|
||||
)
|
||||
|
||||
@@ -41,7 +41,12 @@ impl GatedRecorder {
|
||||
let build_kinds = kinds.clone();
|
||||
PrimitiveBuilder::new(
|
||||
"GatedRecorder",
|
||||
NodeSchema { inputs, output: vec![], params: vec![] }, // sink: empty output (C8)
|
||||
NodeSchema {
|
||||
inputs,
|
||||
output: vec![],
|
||||
params: vec![],
|
||||
doc: "recording sink that persists its input while the gate is true, plus a final row at finalize",
|
||||
}, // sink: empty output (C8)
|
||||
move |_| Box::new(GatedRecorder::new(&build_kinds, gate_col, tx.clone())),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ impl Gt {
|
||||
],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::Bool }],
|
||||
params: vec![],
|
||||
doc: "true where the first input exceeds the second",
|
||||
},
|
||||
|_| Box::new(Gt::new()),
|
||||
)
|
||||
|
||||
@@ -46,6 +46,7 @@ impl Latch {
|
||||
],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "level-sensitive set/reset register holding 1.0 when latched, 0.0 when reset",
|
||||
},
|
||||
|_| Box::new(Latch::new()),
|
||||
)
|
||||
|
||||
@@ -52,7 +52,12 @@ impl LinComb {
|
||||
.collect();
|
||||
PrimitiveBuilder::new(
|
||||
"LinComb",
|
||||
NodeSchema { inputs, output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }], params },
|
||||
NodeSchema {
|
||||
inputs,
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params,
|
||||
doc: "linear combination of its inputs with constant weights",
|
||||
},
|
||||
|p| Box::new(LinComb::new(
|
||||
p.iter().map(|c| c.f64()).collect(),
|
||||
)),
|
||||
|
||||
@@ -26,6 +26,7 @@ impl Max {
|
||||
],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "elementwise maximum of two input series",
|
||||
},
|
||||
|_| Box::new(Max::new()),
|
||||
)
|
||||
|
||||
@@ -27,6 +27,7 @@ impl Min {
|
||||
],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "elementwise minimum of two input series",
|
||||
},
|
||||
|_| Box::new(Min::new()),
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@ impl Mul {
|
||||
],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "elementwise product of two input series",
|
||||
},
|
||||
|_| Box::new(Mul::new()),
|
||||
)
|
||||
|
||||
@@ -53,7 +53,12 @@ impl Recorder {
|
||||
let build_kinds = kinds.clone();
|
||||
PrimitiveBuilder::new(
|
||||
"Recorder",
|
||||
NodeSchema { inputs, output: vec![], params: vec![] }, // sink: empty output (C8)
|
||||
NodeSchema {
|
||||
inputs,
|
||||
output: vec![],
|
||||
params: vec![],
|
||||
doc: "recording sink persisting its input series into the trace store",
|
||||
}, // sink: empty output (C8)
|
||||
move |_| Box::new(Recorder::new(&build_kinds, firing, tx.clone())),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ impl RollingMax {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "series".into() }],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
doc: "rolling maximum over a fixed lookback window",
|
||||
},
|
||||
|p| Box::new(RollingMax::new(p[0].i64() as usize)),
|
||||
)
|
||||
|
||||
@@ -43,6 +43,7 @@ impl RollingMin {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "series".into() }],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
doc: "rolling minimum over a fixed lookback window",
|
||||
},
|
||||
|p| Box::new(RollingMin::new(p[0].i64() as usize)),
|
||||
)
|
||||
|
||||
@@ -28,6 +28,7 @@ impl Scale {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "signal".into() }],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![ParamSpec { name: "factor".into(), kind: ScalarKind::F64 }],
|
||||
doc: "input multiplied by a constant factor param",
|
||||
},
|
||||
|p| Box::new(Scale::new(p[0].f64())),
|
||||
)
|
||||
|
||||
@@ -38,6 +38,7 @@ impl Select {
|
||||
],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "chooses between two inputs by a boolean selector",
|
||||
},
|
||||
|_| Box::new(Select::new()),
|
||||
)
|
||||
|
||||
@@ -26,6 +26,7 @@ impl SeriesReducer {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing, name: "col[0]".to_string() }],
|
||||
output: vec![], // sink: empty output (C8)
|
||||
params: vec![],
|
||||
doc: "folds an input series into a single summary row emitted at finalize",
|
||||
},
|
||||
move |_| Box::new(SeriesReducer::new(tx.clone())),
|
||||
)
|
||||
|
||||
@@ -36,6 +36,7 @@ impl Sign {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "value".into() }],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "sign of the input: -1, 0 or +1",
|
||||
},
|
||||
|_| Box::new(Sign::new()),
|
||||
)
|
||||
|
||||
@@ -65,6 +65,7 @@ impl Sma {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "series".into() }],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
doc: "simple moving average over the input series",
|
||||
},
|
||||
|p| Box::new(Sma::new(p[0].i64() as usize)),
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ impl Sqrt {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "value".into() }],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "square root of the input series",
|
||||
},
|
||||
|_| Box::new(Sqrt::new()),
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ impl Sub {
|
||||
],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "elementwise difference of two input series",
|
||||
},
|
||||
|_| Box::new(Sub::new()),
|
||||
)
|
||||
|
||||
@@ -46,6 +46,7 @@ impl When {
|
||||
],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
doc: "emits the input only on cycles where the condition input is true",
|
||||
},
|
||||
|_| Box::new(When::new()),
|
||||
)
|
||||
|
||||
@@ -32,6 +32,7 @@ impl Bias {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "signal".into() }],
|
||||
output: vec![FieldSpec { name: "bias".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }],
|
||||
doc: "clamps the strategy signal into the bias contract [-1,+1]",
|
||||
},
|
||||
|p| Box::new(Bias::new(p[0].f64())),
|
||||
)
|
||||
|
||||
@@ -224,7 +224,16 @@ pub fn cost_node_builder(
|
||||
) -> PrimitiveBuilder {
|
||||
let mut inputs = geometry_input_ports();
|
||||
inputs.extend(extra_inputs);
|
||||
PrimitiveBuilder::new(name, NodeSchema { inputs, output: cost_output_fields(), params }, build)
|
||||
PrimitiveBuilder::new(
|
||||
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)]
|
||||
|
||||
@@ -58,6 +58,7 @@ impl CostSum {
|
||||
.map(|n| FieldSpec { name: (*n).into(), kind: ScalarKind::F64 })
|
||||
.collect(),
|
||||
params: vec![],
|
||||
doc: "sums cost-model contributions into one cost-in-R stream",
|
||||
},
|
||||
move |_| Box::new(CostSum::new(n_costs)),
|
||||
)
|
||||
|
||||
@@ -31,6 +31,7 @@ impl LongOnly {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "exposure".into() }],
|
||||
output: vec![FieldSpec { name: "exposure".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![ParamSpec { name: "enabled".into(), kind: ScalarKind::Bool }],
|
||||
doc: "when enabled, clamps negative exposure to flat (long-only); otherwise passes exposure through",
|
||||
},
|
||||
|p| Box::new(LongOnly::new(p[0].bool())),
|
||||
)
|
||||
|
||||
@@ -36,6 +36,7 @@ impl Sizer {
|
||||
],
|
||||
output: vec![FieldSpec { name: "size".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![ParamSpec { name: "risk_budget".into(), kind: ScalarKind::F64 }],
|
||||
doc: "size = risk_budget / stop_distance at the deploy edge; bias only gates firing, research runs flat-1R",
|
||||
},
|
||||
|p| Box::new(Sizer::new(p[0].f64())),
|
||||
)
|
||||
|
||||
@@ -34,6 +34,7 @@ impl FixedStop {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "price".into() }],
|
||||
output: vec![FieldSpec { name: "stop_distance".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![ParamSpec { name: "distance".into(), kind: ScalarKind::F64 }],
|
||||
doc: "derives the protective stop that defines the risk unit R",
|
||||
},
|
||||
|p| Box::new(FixedStop::new(p[0].f64())),
|
||||
)
|
||||
|
||||
@@ -72,6 +72,7 @@ impl VolTfStop {
|
||||
ParamSpec { name: "length".into(), kind: ScalarKind::I64 },
|
||||
ParamSpec { name: "k".into(), kind: ScalarKind::F64 },
|
||||
],
|
||||
doc: "volatility- and timeframe-scaled protective stop rule",
|
||||
},
|
||||
|p| Box::new(VolTfStop::new(p[0].i64(), p[1].i64(), p[2].f64())),
|
||||
)
|
||||
|
||||
@@ -134,6 +134,9 @@ impl Scale {
|
||||
}],
|
||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||
params: vec![ParamSpec { name: "factor".into(), kind: ScalarKind::F64 }],
|
||||
// C29: every vocabulary entry ships a one-line meaning — the
|
||||
// doc field is required (E0063 without it) and gated at load.
|
||||
doc: "scalar gain: emits the input times the factor param",
|
||||
},
|
||||
|p| Box::new(Scale::new(p[0].f64())),
|
||||
)
|
||||
@@ -217,10 +220,11 @@ $ aura graph build < smacross.json > blueprint.json
|
||||
Nodes are referenced by an **identifier** (given by `add`, see below); ports
|
||||
are dotted `<identifier>.<port>` on both sides of a wire.
|
||||
|
||||
### The eight ops
|
||||
### The nine ops
|
||||
|
||||
| op | JSON shape | does |
|
||||
|---|---|---|
|
||||
| `doc` | `{"op":"doc","text":<str>}` | declare the composite's one-line meaning (C29) — the op-script twin of the Rust builder's `.doc(...)`. Required before `aura graph register` accepts the product (the store refuses a doc-less composite); at most one per script (a second refuses: `a doc op may appear at most once`). The text is gated: empty or merely restating the composite's name refuses at register. |
|
||||
| `source` | `{"op":"source","role":<str>,"kind":<ScalarKind>}` | reserve a bound root **source** role of `kind` — a real input the harness feeds (e.g. `"price"`). |
|
||||
| `input` | `{"op":"input","role":<str>}` | reserve an open root **input** role (kind inferred from the slots it feeds) — for a fragment meant to be wired by an *enclosing* graph. A standalone document built with `aura graph build` finalizes as a closed root, so an `input` role that is never bound refuses at the end: `finalize: root input role <name> is unbound`. |
|
||||
| `add` | `{"op":"add","type":<TypeId>,"name":<str>?,"bind":{<param>:<Scalar>}?}` | instantiate a node of a type in the closed vocabulary (`aura graph introspect --vocabulary`) — see §0 below for how a type gets into that vocabulary in the first place. `name` becomes the node's identifier for later ops (default: the type's own lowercase label — two unnamed nodes of the same type then collide). `bind` sets zero or more of its params. |
|
||||
|
||||
@@ -194,6 +194,13 @@ the repo, not session memory, is the source of truth.
|
||||
measurement/strategy → backtest → execution) beside a process column, an
|
||||
assembly position, and an import-only shell — the import direction enforced
|
||||
structurally by the full-workspace `c28_layering` guard.
|
||||
- **[C29 — Self-description: every surface explains itself](contracts/c29-self-description.md)** —
|
||||
Every closed-vocabulary entry carries a one-line meaning behind one shared
|
||||
shape gate (`doc_gate`) at three seams — compile/unit (engine-shipped),
|
||||
load (native node crates), register (the content-addressed store, incl. an
|
||||
optional gated document `description`); descriptions never influence
|
||||
execution or identity ids, and registered artifacts are never
|
||||
retroactively invalidated.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# C29 — Self-description: every surface explains itself
|
||||
|
||||
**Guarantee.** Every closed-vocabulary entry the binary exposes carries a
|
||||
one-line meaning, enforced at the seam where the entry enters the system;
|
||||
everything entering the content-addressed store describes itself; a refusal
|
||||
for a missing description names the entry, the field, and the rule. One
|
||||
shared deterministic gate — `doc_gate` in aura-core, importable by every
|
||||
seam per [C28](c28-stratification.md); it fails on an empty/whitespace doc
|
||||
and on a doc that normalizes to the entry's own name — the full id or its
|
||||
bare display segment after the last `::`, so a namespaced extension entry
|
||||
cannot pass its short-name alibi
|
||||
(`DocGateFault::{Empty, RestatesName}`) — guards three seams:
|
||||
|
||||
1. **Compile/unit seam (engine-shipped).** `NodeSchema.doc` is a required
|
||||
field, so the compiler enumerates every construction site; metrics and
|
||||
tap slots are schema carriers (`MetricSchema` / `TapSchema`, id + doc);
|
||||
blocks (`BlockSchema.doc`, the pre-existing model) and tap folds (the
|
||||
registry roster, [C27](c27-declared-taps.md)) already carried theirs.
|
||||
The coverage walk (`aura-cli/tests/self_description.rs`) asserts the
|
||||
gate over all five vocabularies — the engine satisfies its own law.
|
||||
2. **Load seam (extension).** `load_crate` walks every charter-checked
|
||||
type id and gates the resolved schema's doc; a fault refuses the load
|
||||
naming type and rule (`ProjectError::UndescribedVocabularyEntry`).
|
||||
Field presence is compile-enforced for any crate built against the new
|
||||
core; the load gate catches the shape violations the compiler cannot
|
||||
see across the cdylib boundary — the empty-string alibi and the name
|
||||
restatement.
|
||||
3. **Register seam (data plane).** The registry's public `put_blueprint`
|
||||
parses the canonical bytes and requires a gate-passing doc on the root
|
||||
composite and on every named nested composite — the gate guards the
|
||||
store boundary itself, so the register verb, every register-then-run
|
||||
sugar path, and any future caller are gated by construction. The
|
||||
op-script vocabulary carries the `doc` op as the builder `.doc(...)`'s
|
||||
twin ([C25](c25-role-model.md): a closed, typed metadata construct).
|
||||
Process/campaign documents take an additive-optional top-level
|
||||
`description`, gated only when present (`DocFault::BadDescription`).
|
||||
|
||||
**Scope at this record's writing (#321).** The gate walks five
|
||||
vocabularies — process/campaign blocks, metrics, tap slots, tap folds, and
|
||||
the closed node roster — plus the document `description` and the three
|
||||
seams above. Recorded residue, tracked as #321 and not silently claimed:
|
||||
`emit_vocabulary()` and `binding_column_vocabulary()` remain bare name
|
||||
arrays, and builder schemas outside the closed node roster (e.g. the
|
||||
internal tap recording sink) carry compile-required but shape-unwalked
|
||||
docs. Closing #321 widens the walk and removes this paragraph.
|
||||
|
||||
**Id treatment.** Description fields that serialize participate in the
|
||||
content id and are blanked for the identity id, exactly as `Composite.doc`
|
||||
(`strip_debug_symbols`); `NodeSchema` is not serialized
|
||||
([C24](c24-blueprint-data.md)), so node docs move no id. Documents have no
|
||||
identity projection — their pair is: absent `description` is byte-identical
|
||||
to the field-less form (existing content ids stable), present participates.
|
||||
|
||||
**Forbids.** The engine evaluating description text — the gate is
|
||||
deterministic string shape, never content judgement (no freetext logic
|
||||
hole; C17 / domain invariant 10 untouched). A description influencing
|
||||
execution, identity ids, or determinism (C1). Retroactive invalidation of
|
||||
already-registered artifacts — the gate guards the write path only (the
|
||||
raw in-crate write survives as the store-mechanics and back-compat
|
||||
witness: pre-C29 doc-less entries stay readable). Machine-invented meaning
|
||||
lines — an authored text is corrected against the source, never weakened
|
||||
to an empty string or a name restatement.
|
||||
|
||||
**Why.** The audience is headless LLM agents and scripts (#319 ruling),
|
||||
for whom the binary is the only always-present teacher. Field evidence
|
||||
2026-07-22 (#314): an agent holding only the release binary had to recover
|
||||
execution semantics and document schemas by CAS forensics — the removed
|
||||
failure class is exactly that forensic recovery.
|
||||
|
||||
Rendering of the carried texts on the help/introspection surfaces is #315;
|
||||
the generated agent bootstrap card is #267; a fold introspection surface
|
||||
is blocked on #310.
|
||||
@@ -0,0 +1 @@
|
||||
{"format_version":1,"blueprint":{"name":"graph","nodes":[{"primitive":{"type":"lab316_nodes::Scale","name":"scaled","bound":[{"pos":0,"name":"factor","kind":"F64","value":{"F64":2.0}}]}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":1.0}}]}}],"edges":[{"from":0,"to":1,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0}],"source":"F64"}],"output":[{"node":1,"field":0,"name":"bias"}]}}
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{"op": "source", "role": "price", "kind": "F64"},
|
||||
{"op": "add", "type": "lab316_nodes::Scale", "name": "scaled", "bind": {"factor": {"F64": 2.0}}},
|
||||
{"op": "feed", "role": "price", "into": ["scaled.value"]},
|
||||
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 1.0}}},
|
||||
{"op": "connect", "from": "scaled.value", "to": "bias.signal"},
|
||||
{"op": "expose", "from": "bias.bias", "as": "bias"}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
{"format_version":1,"blueprint":{"name":"graph","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub","name":"sub"}},{"primitive":{"type":"Bias","name":"bias"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
|
||||
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{"op": "source", "role": "price", "kind": "F64"},
|
||||
{"op": "add", "type": "SMA", "name": "fast"},
|
||||
{"op": "add", "type": "SMA", "name": "slow"},
|
||||
{"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 @@
|
||||
{"format_version":1,"blueprint":{"name":"graph","doc":"fast/slow SMA crossover spread turned into a bounded directional bias","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub","name":"sub"}},{"primitive":{"type":"Bias","name":"bias"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user