From 750ee93180888644212111bf7255094502fccb07 Mon Sep 17 00:00:00 2001 From: claude Date: Thu, 23 Jul 2026 19:47:46 +0200 Subject: [PATCH] feat(research, registry, cli, runner): C29 register seam -- description gate, store-boundary doc gate, authored root docs Iteration 3 of the self-description plan, tasks 1-3 (checkpoint commit mid-iteration; the register e2e, the op-script doc slot, and the full gates follow): - aura-research: DocFault::BadDescription { subject, fault }; a present document description passes doc_gate in validate_process / validate_campaign (absent is never a fault and stays byte-identical); prose arm in doc_fault_prose; tests incl. content-id participation. - aura-registry: the store boundary gates admission -- the public put_blueprint parses the canonical bytes and walks root + named nested composite docs (None refuses like ""), refusing via UndescribedComposite { name, fault } / MalformedBlueprint; the raw write survives as private put_blueprint_unchecked (store-mechanics tests + the never-retroactive witness: doc-less pre-C29 entries stay readable). Registry-internal seeders triaged junk-vs-real. - authored root docs: the 4 example blueprints + their 4 open-fixture twins, the scaffold starter template, the scaled_signal and hl_signal inline fixtures, and the three programmatic member.rs builders (breakout, meanrev, hl_channel) via GraphBuilder::doc -- mechanical propagation of the authored lines. Recovery note: the implement run's task 3 discarded tasks 1+2's edits in three files via a file-level checkout (the issue-#23 guard shape); the loop's boundary snapshot caught it and the content was restored from recovery snapshot 11a6ae2, then re-verified (research 81 green, registry 51 green, cli 58/59). Known-open at this checkpoint: the op-script register test is red -- op-built composites have no doc surface (#125 scope cut); the op vocabulary grows the anticipated doc slot in the next task of this iteration (decision logged on the issue). refs #316 --- crates/aura-cli/examples/r_breakout.json | 2 +- crates/aura-cli/examples/r_channel.json | 2 +- crates/aura-cli/examples/r_meanrev.json | 2 +- crates/aura-cli/examples/r_sma.json | 2 +- crates/aura-cli/src/research_docs.rs | 39 ++++++ crates/aura-cli/src/scaffold.rs | 2 +- crates/aura-cli/tests/cli_run.rs | 2 +- .../tests/fixtures/r_breakout_open.json | 2 +- .../tests/fixtures/r_channel_open.json | 2 +- .../tests/fixtures/r_meanrev_open.json | 2 +- .../aura-cli/tests/fixtures/r_sma_open.json | 2 +- .../aura-cli/tests/project_sweep_campaign.rs | 2 +- crates/aura-registry/src/lib.rs | 118 ++++++++++++++++-- crates/aura-research/src/lib.rs | 65 +++++++++- crates/aura-runner/src/member.rs | 3 + 15 files changed, 226 insertions(+), 21 deletions(-) diff --git a/crates/aura-cli/examples/r_breakout.json b/crates/aura-cli/examples/r_breakout.json index e8556b2..e60f719 100644 --- a/crates/aura-cli/examples/r_breakout.json +++ b/crates/aura-cli/examples/r_breakout.json @@ -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"}]}} \ No newline at end of file +{"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"}]}} \ No newline at end of file diff --git a/crates/aura-cli/examples/r_channel.json b/crates/aura-cli/examples/r_channel.json index 9579508..8c481a4 100644 --- a/crates/aura-cli/examples/r_channel.json +++ b/crates/aura-cli/examples/r_channel.json @@ -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"}]}} \ No newline at end of file +{"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"}]}} \ No newline at end of file diff --git a/crates/aura-cli/examples/r_meanrev.json b/crates/aura-cli/examples/r_meanrev.json index df795a6..d385361 100644 --- a/crates/aura-cli/examples/r_meanrev.json +++ b/crates/aura-cli/examples/r_meanrev.json @@ -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"}]}} \ No newline at end of file +{"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"}]}} \ No newline at end of file diff --git a/crates/aura-cli/examples/r_sma.json b/crates/aura-cli/examples/r_sma.json index cbdc8c7..1fb51c7 100644 --- a/crates/aura-cli/examples/r_sma.json +++ b/crates/aura-cli/examples/r_sma.json @@ -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"}]}} \ No newline at end of file +{"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"}]}} \ No newline at end of file diff --git a/crates/aura-cli/src/research_docs.rs b/crates/aura-cli/src/research_docs.rs index a70f9eb..cc306d1 100644 --- a/crates/aura-cli/src/research_docs.rs +++ b/crates/aura-cli/src/research_docs.rs @@ -191,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)" + ), + }, } } @@ -817,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 diff --git a/crates/aura-cli/src/scaffold.rs b/crates/aura-cli/src/scaffold.rs index a224f61..7cedbda 100644 --- a/crates/aura-cli/src/scaffold.rs +++ b/crates/aura-cli/src/scaffold.rs @@ -222,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) diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index a3fec65..97c93fd 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -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 diff --git a/crates/aura-cli/tests/fixtures/r_breakout_open.json b/crates/aura-cli/tests/fixtures/r_breakout_open.json index 6bb36e4..d725a31 100644 --- a/crates/aura-cli/tests/fixtures/r_breakout_open.json +++ b/crates/aura-cli/tests/fixtures/r_breakout_open.json @@ -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"}]}]}} \ No newline at end of file +{"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"}]}]}} \ No newline at end of file diff --git a/crates/aura-cli/tests/fixtures/r_channel_open.json b/crates/aura-cli/tests/fixtures/r_channel_open.json index 52942b1..2acb269 100644 --- a/crates/aura-cli/tests/fixtures/r_channel_open.json +++ b/crates/aura-cli/tests/fixtures/r_channel_open.json @@ -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"}]}]}} \ No newline at end of file +{"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"}]}]}} \ No newline at end of file diff --git a/crates/aura-cli/tests/fixtures/r_meanrev_open.json b/crates/aura-cli/tests/fixtures/r_meanrev_open.json index 05bf383..1976c78 100644 --- a/crates/aura-cli/tests/fixtures/r_meanrev_open.json +++ b/crates/aura-cli/tests/fixtures/r_meanrev_open.json @@ -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"}]}]}} \ No newline at end of file +{"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"}]}]}} \ No newline at end of file diff --git a/crates/aura-cli/tests/fixtures/r_sma_open.json b/crates/aura-cli/tests/fixtures/r_sma_open.json index 52ec036..f3b213e 100644 --- a/crates/aura-cli/tests/fixtures/r_sma_open.json +++ b/crates/aura-cli/tests/fixtures/r_sma_open.json @@ -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"}]}} \ No newline at end of file +{"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"}]}} \ No newline at end of file diff --git a/crates/aura-cli/tests/project_sweep_campaign.rs b/crates/aura-cli/tests/project_sweep_campaign.rs index a7d1dcd..5d55488 100644 --- a/crates/aura-cli/tests/project_sweep_campaign.rs +++ b/crates/aura-cli/tests/project_sweep_campaign.rs @@ -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. diff --git a/crates/aura-registry/src/lib.rs b/crates/aura-registry/src/lib.rs index 94c594e..76d38e8 100644 --- a/crates/aura-registry/src/lib.rs +++ b/crates/aura-registry/src/lib.rs @@ -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, 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,21 @@ 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); set .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 +1126,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 +1903,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 +2035,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 +2050,7 @@ mod tests { vec![], vec![], ) + .with_doc("fixture composite for the registry seeder test") } #[test] @@ -2095,7 +2191,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 +2247,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 +2341,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 +2415,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"); diff --git a/crates/aura-research/src/lib.rs b/crates/aura-research/src/lib.rs index a44e718..7079696 100644 --- a/crates/aura-research/src/lib.rs +++ b/crates/aura-research/src/lib.rs @@ -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. @@ -840,6 +840,9 @@ 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 { @@ -850,6 +853,11 @@ fn is_known_metric(name: &str) -> bool { /// Returns ALL findings, not the first. pub fn validate_process(doc: &ProcessDoc) -> Vec { 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); } @@ -913,6 +921,11 @@ pub fn validate_process(doc: &ProcessDoc) -> Vec { /// Returns ALL findings, not the first. pub fn validate_campaign(doc: &CampaignDoc) -> Vec { 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); } @@ -1900,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(); diff --git a/crates/aura-runner/src/member.rs b/crates/aura-runner/src/member.rs index b17b2a1..5bcc8ed 100644 --- a/crates/aura-runner/src/member.rs +++ b/crates/aura-runner/src/member.rs @@ -782,6 +782,7 @@ pub fn reopen_all(signal: Composite, overrides: &[String]) -> Composite { fn r_breakout_signal(channel: Option) -> 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) -> Composite { fn r_meanrev_signal(window: Option, band_k: Option) -> 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, band_k: Option) -> Composite { fn r_channel_signal(channel: Option) -> 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");