Compare commits
3 Commits
6fb7caf929
...
d26f0c84a4
| Author | SHA1 | Date | |
|---|---|---|---|
| d26f0c84a4 | |||
| 162bf849ce | |||
| 829a1984e6 |
@@ -18,6 +18,31 @@ use serde::Deserialize;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use aura_vocabulary::std_vocabulary;
|
use aura_vocabulary::std_vocabulary;
|
||||||
|
|
||||||
|
/// The op-list reference `aura graph build --help` appends (#323): the nine
|
||||||
|
/// op kinds with their fields and one worked element each. Lives beside
|
||||||
|
/// [`OpDoc`] so a new op variant is one screen away from its help line.
|
||||||
|
pub const OP_REFERENCE: &str = r#"Op-list reference (stdin: a JSON array of op objects, applied in order):
|
||||||
|
{"op":"source","role":"price","kind":"F64"}
|
||||||
|
declare a bound root input role of a scalar kind
|
||||||
|
{"op":"input","role":"price"}
|
||||||
|
declare an open input role (wired by an enclosing graph)
|
||||||
|
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":2}}}
|
||||||
|
instantiate a node ("name" optional; "bind" maps param -> typed scalar)
|
||||||
|
{"op":"feed","role":"price","into":["fast.series","slow.series"]}
|
||||||
|
wire a role into one or more input slots
|
||||||
|
{"op":"connect","from":"fast.value","to":"sub.lhs"}
|
||||||
|
wire a node output into an input slot
|
||||||
|
{"op":"expose","from":"sub.value","as":"bias"}
|
||||||
|
name a graph output field
|
||||||
|
{"op":"tap","from":"sub.value","as":"spread"}
|
||||||
|
declare a recordable tap on a wire (expose's output-side twin)
|
||||||
|
{"op":"gang","as":"length","into":["fast.length","slow.length"]}
|
||||||
|
fuse two or more sibling params into one public knob
|
||||||
|
{"op":"doc","text":"..."}
|
||||||
|
declare the composite's one-line meaning (C29)
|
||||||
|
|
||||||
|
Node types and their ports: aura graph introspect --vocabulary | --node <T>"#;
|
||||||
|
|
||||||
/// The wire DTO for one construction op — the document's by-identifier shape,
|
/// The wire DTO for one construction op — the document's by-identifier shape,
|
||||||
/// internally tagged by `"op"`, mapped into the serde-free engine `Op`. Bind
|
/// internally tagged by `"op"`, mapped into the serde-free engine `Op`. Bind
|
||||||
/// values are the typed `Scalar` form (`{"I64":2}`); `kind` the capitalized
|
/// values are the typed `Scalar` form (`{"I64":2}`); `kind` the capitalized
|
||||||
@@ -201,7 +226,8 @@ pub fn build_cmd(env: &aura_runner::project::Env) {
|
|||||||
pub fn introspect_node(type_id: &str, env: &aura_runner::project::Env) -> Result<String, String> {
|
pub fn introspect_node(type_id: &str, env: &aura_runner::project::Env) -> Result<String, String> {
|
||||||
let builder = env.resolve(type_id).ok_or_else(|| format!("unknown node type {type_id:?}"))?;
|
let builder = env.resolve(type_id).ok_or_else(|| format!("unknown node type {type_id:?}"))?;
|
||||||
let schema = builder.schema();
|
let schema = builder.schema();
|
||||||
let mut out = format!("{}\n", builder.label());
|
// C29 (#315): the head line carries the node's one-line meaning.
|
||||||
|
let mut out = format!("{} — {}\n", builder.label(), schema.doc);
|
||||||
for port in &schema.inputs {
|
for port in &schema.inputs {
|
||||||
out.push_str(&format!(" in {}:{:?}\n", port.name, port.kind));
|
out.push_str(&format!(" in {}:{:?}\n", port.name, port.kind));
|
||||||
}
|
}
|
||||||
@@ -239,17 +265,29 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd, env: &aura_runner::project
|
|||||||
let count = cmd.vocabulary as usize
|
let count = cmd.vocabulary as usize
|
||||||
+ cmd.node.is_some() as usize
|
+ cmd.node.is_some() as usize
|
||||||
+ cmd.unwired as usize
|
+ cmd.unwired as usize
|
||||||
|
+ cmd.folds as usize
|
||||||
+ cmd.params.is_some() as usize
|
+ cmd.params.is_some() as usize
|
||||||
+ (cmd.content_id.is_some() || cmd.identity_id) as usize;
|
+ (cmd.content_id.is_some() || cmd.identity_id) as usize;
|
||||||
if count != 1 {
|
if count != 1 {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"aura: Usage: aura graph introspect --vocabulary | --node <T> | --unwired | --params <FILE|ID> | --content-id [FILE] | --identity-id (the two id flags may be combined)"
|
"aura: Usage: aura graph introspect --vocabulary | --node <T> | --unwired | --folds | --params <FILE|ID> | --content-id [FILE] | --identity-id (the two id flags may be combined)"
|
||||||
);
|
);
|
||||||
std::process::exit(2);
|
std::process::exit(2);
|
||||||
}
|
}
|
||||||
if cmd.vocabulary {
|
if cmd.vocabulary {
|
||||||
|
// C29 (#315): each type id carries its schema's one-line meaning —
|
||||||
|
// bare names teach nothing (What do Latch, When, Select, Bias do?).
|
||||||
|
// A rostered id that fails to resolve is an invariant breach; better
|
||||||
|
// loud than a silent C29-incomplete bare row.
|
||||||
for t in env.type_ids() {
|
for t in env.type_ids() {
|
||||||
println!("{t}");
|
let b = env.resolve(t).expect("every rostered type id resolves to a builder");
|
||||||
|
println!("{t:<18} {}", b.schema().doc);
|
||||||
|
}
|
||||||
|
} else if cmd.folds {
|
||||||
|
// The fold vocabulary binds at graph-declared taps (C27), so the
|
||||||
|
// graph introspect namespace is its discovery surface (#315).
|
||||||
|
for f in aura_std::fold_vocabulary() {
|
||||||
|
println!("{:<7} binds {:<9} out {:<9} — {}", f.id, f.binds, f.out, f.doc);
|
||||||
}
|
}
|
||||||
} else if let Some(type_id) = cmd.node.as_deref() {
|
} else if let Some(type_id) = cmd.node.as_deref() {
|
||||||
match introspect_node(type_id, env) {
|
match introspect_node(type_id, env) {
|
||||||
|
|||||||
@@ -1026,11 +1026,32 @@ fn version_string() -> &'static str {
|
|||||||
/// The `aura` root parser. `version` is built from `CARGO_PKG_VERSION` (the
|
/// The `aura` root parser. `version` is built from `CARGO_PKG_VERSION` (the
|
||||||
/// workspace `0.1.0`) plus the parenthesized `ENGINE_COMMIT`, so
|
/// workspace `0.1.0`) plus the parenthesized `ENGINE_COMMIT`, so
|
||||||
/// `aura --version` prints `aura 0.1.0 (<commit>)`.
|
/// `aura --version` prints `aura 0.1.0 (<commit>)`.
|
||||||
|
/// The two-layer concepts paragraph `aura --help` opens with (#315): the
|
||||||
|
/// zero-setup self-description a reader without repo access gets.
|
||||||
|
const CONCEPTS_HELP: &str = "\
|
||||||
|
Author, backtest, and validate trading strategies — research CLI.
|
||||||
|
|
||||||
|
Two layers, one vocabulary: the research verbs are the convenience surface —
|
||||||
|
over --real data, sweep, walkforward, mc, and generalize desugar to
|
||||||
|
registered process/campaign documents and execute them (run is their
|
||||||
|
single-backtest sibling; synthetic runs execute in-process). That document
|
||||||
|
data plane is directly authorable: `aura process` / `aura campaign`
|
||||||
|
(validate | introspect | register | run), growing a document from a bare {}
|
||||||
|
via `introspect --unwired`.
|
||||||
|
|
||||||
|
Execution model: a strategy emits a bias in [-1,+1] per cycle, held as the
|
||||||
|
continuously-tracked target position; a protective stop defines the risk unit
|
||||||
|
R, and signal quality is measured in R.
|
||||||
|
|
||||||
|
Traces: `sweep --real --trace` / `walkforward --real --trace` record per-cycle
|
||||||
|
taps under runs/, consumed by `aura chart` and `aura measure`.";
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
name = "aura",
|
name = "aura",
|
||||||
version = version_string(),
|
version = version_string(),
|
||||||
about = "Author, backtest, and validate trading strategies — research CLI",
|
about = "Author, backtest, and validate trading strategies — research CLI",
|
||||||
|
long_about = CONCEPTS_HELP,
|
||||||
infer_long_args = true
|
infer_long_args = true
|
||||||
)]
|
)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
@@ -1044,22 +1065,43 @@ struct Cli {
|
|||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum Command {
|
enum Command {
|
||||||
/// Run a single backtest (built-in harness or a loaded blueprint).
|
/// Run a single backtest (built-in harness or a loaded blueprint).
|
||||||
|
///
|
||||||
|
/// Part of the research sugar surface; the canonical, document-first form
|
||||||
|
/// of any research run is a registered process + campaign pair — see
|
||||||
|
/// `aura process` / `aura campaign`.
|
||||||
Run(RunCmd),
|
Run(RunCmd),
|
||||||
/// Render a recorded run's trace to an HTML chart.
|
/// Render a recorded run's trace to an HTML chart.
|
||||||
Chart(ChartCmd),
|
Chart(ChartCmd),
|
||||||
/// Emit / construct / introspect a graph.
|
/// Emit / construct / introspect a graph.
|
||||||
Graph(GraphCmd),
|
Graph(GraphCmd),
|
||||||
/// Sweep a parameter grid over a strategy or a loaded blueprint.
|
/// Sweep a parameter grid over a strategy or a loaded blueprint.
|
||||||
|
///
|
||||||
|
/// Sugar over the document layer: over --real data this desugars to a
|
||||||
|
/// registered process (std::sweep) + campaign pair — author the documents
|
||||||
|
/// directly via `aura process` / `aura campaign`.
|
||||||
Sweep(SweepCmd),
|
Sweep(SweepCmd),
|
||||||
/// Walk-forward validation over a strategy or a loaded blueprint. Over --real, the
|
/// Walk-forward validation over a strategy or a loaded blueprint. Over --real, the
|
||||||
/// fixed 90/30-day roller fits to a --from/--to window shorter than it, preserving
|
/// fixed 90/30-day roller fits to a --from/--to window shorter than it, preserving
|
||||||
/// the 3:1 IS:OOS ratio, instead of refusing the window outright.
|
/// the 3:1 IS:OOS ratio, instead of refusing the window outright.
|
||||||
|
///
|
||||||
|
/// Sugar over the document layer: over --real data this desugars to a
|
||||||
|
/// registered process ([std::grid, std::walk_forward]) + campaign pair —
|
||||||
|
/// author the documents directly via `aura process` / `aura campaign`.
|
||||||
Walkforward(WalkforwardCmd),
|
Walkforward(WalkforwardCmd),
|
||||||
/// Grade one candidate across multiple instruments.
|
/// Grade one candidate across multiple instruments.
|
||||||
|
///
|
||||||
|
/// Sugar over the document layer: desugars to a registered process
|
||||||
|
/// ([std::sweep (selection), std::generalize]) + campaign pair — author
|
||||||
|
/// the documents directly via `aura process` / `aura campaign`.
|
||||||
Generalize(GeneralizeCmd),
|
Generalize(GeneralizeCmd),
|
||||||
/// Monte-Carlo over synthetic draws, an R-bootstrap, or a loaded blueprint. Over
|
/// Monte-Carlo over synthetic draws, an R-bootstrap, or a loaded blueprint. Over
|
||||||
/// --real, the fixed 90/30-day walk-forward roller fits to a --from/--to window
|
/// --real, the fixed 90/30-day walk-forward roller fits to a --from/--to window
|
||||||
/// shorter than it, preserving the 3:1 IS:OOS ratio, instead of refusing outright.
|
/// shorter than it, preserving the 3:1 IS:OOS ratio, instead of refusing outright.
|
||||||
|
///
|
||||||
|
/// Sugar over the document layer: over --real data this desugars to a
|
||||||
|
/// registered process ([std::grid, std::walk_forward, std::monte_carlo]) +
|
||||||
|
/// campaign pair — author the documents directly via `aura process` /
|
||||||
|
/// `aura campaign`.
|
||||||
Mc(McCmd),
|
Mc(McCmd),
|
||||||
/// List or inspect recorded run families.
|
/// List or inspect recorded run families.
|
||||||
Runs(RunsCmd),
|
Runs(RunsCmd),
|
||||||
@@ -1181,6 +1223,7 @@ struct GraphCmd {
|
|||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum GraphSub {
|
enum GraphSub {
|
||||||
/// Construct a graph from a stdin op-list.
|
/// Construct a graph from a stdin op-list.
|
||||||
|
#[command(after_help = crate::graph_construct::OP_REFERENCE)]
|
||||||
Build,
|
Build,
|
||||||
/// Introspect a graph.
|
/// Introspect a graph.
|
||||||
Introspect(GraphIntrospectCmd),
|
Introspect(GraphIntrospectCmd),
|
||||||
@@ -1193,7 +1236,7 @@ enum GraphSub {
|
|||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
struct GraphIntrospectCmd {
|
struct GraphIntrospectCmd {
|
||||||
/// List the closed node vocabulary (one node type per line).
|
/// List the closed node vocabulary (one node type + its meaning per line).
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
vocabulary: bool,
|
vocabulary: bool,
|
||||||
/// Describe one node type's ports by name.
|
/// Describe one node type's ports by name.
|
||||||
@@ -1202,6 +1245,9 @@ struct GraphIntrospectCmd {
|
|||||||
/// List the graph's unwired (unbound) ports.
|
/// List the graph's unwired (unbound) ports.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
unwired: bool,
|
unwired: bool,
|
||||||
|
/// List the closed tap-fold vocabulary (fold, bind rule, output kind, meaning).
|
||||||
|
#[arg(long)]
|
||||||
|
folds: bool,
|
||||||
/// Print the graph's content id (topology hash). With FILE, read the
|
/// Print the graph's content id (topology hash). With FILE, read the
|
||||||
/// document from it (a blueprint envelope or an op-list, shape-
|
/// document from it (a blueprint envelope or an op-list, shape-
|
||||||
/// discriminated, #196); without FILE, read a stdin op-list as before.
|
/// discriminated, #196); without FILE, read a stdin op-list as before.
|
||||||
|
|||||||
@@ -107,10 +107,12 @@ fn print_metric_roster() {
|
|||||||
(true, false) => "rankable",
|
(true, false) => "rankable",
|
||||||
(false, false) => "annotation",
|
(false, false) => "annotation",
|
||||||
};
|
};
|
||||||
|
// C29 (#315): the roster carries each metric's one-line meaning, not
|
||||||
|
// only where it may be used.
|
||||||
if generalize {
|
if generalize {
|
||||||
println!("{name:<24} {base} | generalize");
|
println!("{name:<24} {base} | generalize — {}", m.doc);
|
||||||
} else {
|
} else {
|
||||||
println!("{name:<24} {base}");
|
println!("{name:<24} {base} — {}", m.doc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -372,6 +374,13 @@ fn describe_one_block(id: &str) -> Result<(), String> {
|
|||||||
for s in schema.slots {
|
for s in schema.slots {
|
||||||
let req = if s.required { "required" } else { "optional" };
|
let req = if s.required { "required" } else { "optional" };
|
||||||
println!(" {:<20} {req}, {}", s.name, slot_kind_label(s.kind));
|
println!(" {:<20} {req}, {}", s.name, slot_kind_label(s.kind));
|
||||||
|
// C29 (#315): the tap slot's closed value vocabulary carries its
|
||||||
|
// per-entry meanings, indented under the slot line.
|
||||||
|
if matches!(s.kind, aura_research::SlotKind::TapKinds) {
|
||||||
|
for t in aura_research::tap_vocabulary() {
|
||||||
|
println!(" {:<14} {}", t.id, t.doc);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,6 +165,8 @@ impl Scale {
|
|||||||
}],
|
}],
|
||||||
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||||
params: vec![ParamSpec { name: "factor".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",
|
doc: "scalar gain: emits the input times the factor param",
|
||||||
},
|
},
|
||||||
|p| Box::new(Scale::new(p[0].f64())),
|
|p| Box::new(Scale::new(p[0].f64())),
|
||||||
@@ -238,6 +240,14 @@ std vocabulary, anchored by `Aura.toml`. There is no crate and no build step.
|
|||||||
`aura nodes new <name>` scaffolds a node crate beside this project and
|
`aura nodes new <name>` scaffolds a node crate beside this project and
|
||||||
attaches it via `[nodes]` in `Aura.toml` (build it with `cargo build`).
|
attaches it via `[nodes]` in `Aura.toml` (build it with `cargo build`).
|
||||||
- Topology is data (`blueprints/*.json`); results land in `runs/`.
|
- Topology is data (`blueprints/*.json`); results land in `runs/`.
|
||||||
|
- Execution model: a strategy emits a bias in [-1,+1] per cycle, held as the
|
||||||
|
continuously-tracked target position; a protective stop defines the risk
|
||||||
|
unit R, and quality metrics are R-based. Entry signals become held state
|
||||||
|
via the signal-side latch/edge-pulse idiom (see
|
||||||
|
`aura graph introspect --vocabulary`).
|
||||||
|
- Data plane: the research verbs are sugar over registered process/campaign
|
||||||
|
documents — author them directly with `aura process` / `aura campaign`,
|
||||||
|
growing one from a bare `{}` via `aura campaign introspect --unwired`.
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
/// Render a data-only project template: only `__NAME__`/`__NAME_SNAKE__`
|
/// Render a data-only project template: only `__NAME__`/`__NAME_SNAKE__`
|
||||||
@@ -455,6 +465,41 @@ mod tests {
|
|||||||
assert!(claude.contains("demo_lab_signal.fast.length"));
|
assert!(claude.contains("demo_lab_signal.fast.length"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// #315: the scaffolded project CLAUDE.md teaches the execution semantics
|
||||||
|
/// (bias as held target position, protective stop, R) and the document
|
||||||
|
/// data plane — the two things the 2026-07-22 field agent had to discover
|
||||||
|
/// forensically.
|
||||||
|
#[test]
|
||||||
|
fn project_claude_md_teaches_execution_semantics_and_the_data_plane() {
|
||||||
|
let claude = render_project(CLAUDE_MD_PROJECT, "demo-lab");
|
||||||
|
assert!(claude.contains("target position"), "names the held-target model: {claude}");
|
||||||
|
assert!(claude.contains("protective stop"), "names the stop that defines R: {claude}");
|
||||||
|
assert!(claude.contains("aura process"), "points at the document layer: {claude}");
|
||||||
|
assert!(claude.contains("introspect --unwired"), "names the bare-{{}} ramp: {claude}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// #323: the authoring guide's S0 worked literal is byte-identical to the
|
||||||
|
/// scaffold's `LIB_RS` template (rendered with the guide's `my_lab`
|
||||||
|
/// namespace). The scaffold e2e tests compile that template for real, so
|
||||||
|
/// this equality pin is a transitive compile guard — the guide literal
|
||||||
|
/// can no longer drift silently from the schema (it broke against the
|
||||||
|
/// required `NodeSchema.doc` once).
|
||||||
|
#[test]
|
||||||
|
fn guide_worked_example_matches_the_scaffold_template() {
|
||||||
|
let guide = include_str!("../../../docs/authoring-guide.md");
|
||||||
|
let anchor = "### Worked example: `Scale`";
|
||||||
|
let after = &guide[guide.find(anchor).expect("guide keeps the S0 worked example")..];
|
||||||
|
let start = after.find("```rust\n").expect("worked example is a rust fence") + 8;
|
||||||
|
let fence = &after[start..];
|
||||||
|
let fence = &fence[..fence.find("\n```").expect("fence closes") + 1];
|
||||||
|
let body = &LIB_RS[LIB_RS.find("use aura_core::{").expect("template body starts at the import")..];
|
||||||
|
let expected = body.replace("__NS__", "my_lab");
|
||||||
|
assert_eq!(
|
||||||
|
fence, expected,
|
||||||
|
"docs/authoring-guide.md S0 literal drifted from the compiled scaffold template"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Property (#183, fieldtest F2 gap): the scaffold's own starter node teaches
|
/// Property (#183, fieldtest F2 gap): the scaffold's own starter node teaches
|
||||||
/// the bound-param build closure. Its rendered `src/lib.rs` declares a param in
|
/// the bound-param build closure. Its rendered `src/lib.rs` declares a param in
|
||||||
/// the node schema and reads a bound value in the build closure (not the
|
/// the node schema and reads a bound value in the build closure (not the
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
//! End-to-end pins for the self-description surfaces (#315, #323): the help
|
||||||
|
//! opens with the two-layer concept, the sugar verbs name the document shape
|
||||||
|
//! they desugar to, every introspection roster carries per-entry meanings,
|
||||||
|
//! and `graph build --help` carries the op-list reference. Driven over the
|
||||||
|
//! built binary — the zero-setup surface a reader without repo access gets.
|
||||||
|
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
const BIN: &str = env!("CARGO_BIN_EXE_aura");
|
||||||
|
|
||||||
|
/// Run `aura <args>` (no stdin); return (stdout, stderr, success).
|
||||||
|
fn run(args: &[&str]) -> (String, String, bool) {
|
||||||
|
let out = Command::new(BIN).args(args).output().expect("spawn aura");
|
||||||
|
(
|
||||||
|
String::from_utf8_lossy(&out.stdout).into_owned(),
|
||||||
|
String::from_utf8_lossy(&out.stderr).into_owned(),
|
||||||
|
out.status.success(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// #315: `aura --help` opens with the concepts paragraph — the two layers,
|
||||||
|
/// the bias-as-target-position + protective-stop execution model, and how
|
||||||
|
/// traces come to exist. Each pin sits on one authored line of the explicit
|
||||||
|
/// `long_about` string, so clap's wrapping cannot split it.
|
||||||
|
#[test]
|
||||||
|
fn top_level_help_opens_with_the_two_layer_concept() {
|
||||||
|
let (out, err, ok) = run(&["--help"]);
|
||||||
|
assert!(ok, "aura --help failed: {err}");
|
||||||
|
assert!(out.contains("research verbs"), "names the sugar layer: {out}");
|
||||||
|
assert!(out.contains("directly authorable"), "names the document data plane: {out}");
|
||||||
|
assert!(out.contains("bias in [-1,+1]"), "names the bias output: {out}");
|
||||||
|
assert!(out.contains("defines the risk unit"), "names the protective stop / R: {out}");
|
||||||
|
assert!(out.contains("introspect --unwired"), "names the bare-{{}} ramp: {out}");
|
||||||
|
assert!(out.contains("aura chart"), "names the trace consumers: {out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// #315: each document-bridged sugar verb's long help names the process
|
||||||
|
/// shape it desugars to and points at the document layer. `run` (not
|
||||||
|
/// document-bridged) points at the canonical document-first form instead.
|
||||||
|
#[test]
|
||||||
|
fn sugar_verbs_name_their_document_shape() {
|
||||||
|
for (verb, needle) in [
|
||||||
|
("sweep", "std::sweep"),
|
||||||
|
("walkforward", "std::walk_forward"),
|
||||||
|
("mc", "std::monte_carlo"),
|
||||||
|
("generalize", "std::generalize"),
|
||||||
|
] {
|
||||||
|
let (out, err, ok) = run(&[verb, "--help"]);
|
||||||
|
assert!(ok, "aura {verb} --help failed: {err}");
|
||||||
|
assert!(out.contains("Sugar"), "{verb} --help names the sugar relation: {out}");
|
||||||
|
assert!(out.contains(needle), "{verb} --help names {needle}: {out}");
|
||||||
|
assert!(out.contains("aura campaign"), "{verb} --help points at the documents: {out}");
|
||||||
|
}
|
||||||
|
let (out, err, ok) = run(&["run", "--help"]);
|
||||||
|
assert!(ok, "aura run --help failed: {err}");
|
||||||
|
assert!(out.contains("document-first"), "run --help names the canonical form: {out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// #323: `graph build --help` carries the op-list reference — the op kinds
|
||||||
|
/// and fields are learnable from the binary, not only from serde refusals.
|
||||||
|
#[test]
|
||||||
|
fn graph_build_help_carries_the_op_reference() {
|
||||||
|
let (out, err, ok) = run(&["graph", "build", "--help"]);
|
||||||
|
assert!(ok, "aura graph build --help failed: {err}");
|
||||||
|
for op in [
|
||||||
|
r#"{"op":"source""#,
|
||||||
|
r#"{"op":"input""#,
|
||||||
|
r#"{"op":"add""#,
|
||||||
|
r#"{"op":"feed""#,
|
||||||
|
r#"{"op":"connect""#,
|
||||||
|
r#"{"op":"expose""#,
|
||||||
|
r#"{"op":"tap""#,
|
||||||
|
r#"{"op":"gang""#,
|
||||||
|
r#"{"op":"doc""#,
|
||||||
|
] {
|
||||||
|
assert!(out.contains(op), "op reference carries {op}: {out}");
|
||||||
|
}
|
||||||
|
assert!(out.contains("graph introspect --vocabulary"), "points at the node roster: {out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// #315: the node vocabulary listing carries each type's one-line meaning —
|
||||||
|
/// no more bare names that teach nothing.
|
||||||
|
#[test]
|
||||||
|
fn graph_introspect_vocabulary_carries_meanings() {
|
||||||
|
let (out, err, ok) = run(&["graph", "introspect", "--vocabulary"]);
|
||||||
|
assert!(ok, "vocabulary listing failed: {err}");
|
||||||
|
let sma = out
|
||||||
|
.lines()
|
||||||
|
.find(|l| l.split_whitespace().next() == Some("SMA"))
|
||||||
|
.unwrap_or_else(|| panic!("no SMA line: {out}"));
|
||||||
|
assert!(sma.contains("moving average"), "SMA line carries its meaning: {sma}");
|
||||||
|
for l in out.lines().filter(|l| !l.trim().is_empty()) {
|
||||||
|
assert!(l.split_whitespace().count() >= 2, "bare name without meaning: {l}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// #315: `--node <T>`'s head line carries the type's meaning beside its label.
|
||||||
|
#[test]
|
||||||
|
fn graph_introspect_node_head_line_carries_the_meaning() {
|
||||||
|
let (out, err, ok) = run(&["graph", "introspect", "--node", "SMA"]);
|
||||||
|
assert!(ok, "node introspect failed: {err}");
|
||||||
|
let head = out.lines().next().unwrap_or("");
|
||||||
|
assert!(head.contains("moving average"), "head line carries the meaning: {out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// #315: the fold vocabulary is introspectable from the binary — name, bind
|
||||||
|
/// rule, output kind, and meaning per fold (it lived only in source comments).
|
||||||
|
#[test]
|
||||||
|
fn graph_introspect_folds_lists_the_fold_vocabulary() {
|
||||||
|
let (out, err, ok) = run(&["graph", "introspect", "--folds"]);
|
||||||
|
assert!(ok, "folds listing failed: {err}");
|
||||||
|
let lines: Vec<&str> = out.lines().filter(|l| !l.trim().is_empty()).collect();
|
||||||
|
assert_eq!(lines.len(), 7, "one line per fold kind: {out}");
|
||||||
|
let mean = lines.iter().find(|l| l.starts_with("Mean")).expect("Mean row");
|
||||||
|
assert!(mean.contains("f64") && mean.contains("mean"), "Mean row rules + meaning: {mean}");
|
||||||
|
let count = lines.iter().find(|l| l.starts_with("Count")).expect("Count row");
|
||||||
|
assert!(count.contains("i64") && count.contains("any"), "Count row rules: {count}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// #315: the metric roster carries each metric's one-line meaning beside its
|
||||||
|
/// applicability tags.
|
||||||
|
#[test]
|
||||||
|
fn process_introspect_metrics_carries_meanings() {
|
||||||
|
let (out, err, ok) = run(&["process", "introspect", "--metrics"]);
|
||||||
|
assert!(ok, "metric roster failed: {err}");
|
||||||
|
let er = out
|
||||||
|
.lines()
|
||||||
|
.find(|l| l.split_whitespace().next() == Some("expectancy_r"))
|
||||||
|
.unwrap_or_else(|| panic!("no expectancy_r line: {out}"));
|
||||||
|
assert!(er.contains("mean realized R"), "meaning text on the roster line: {er}");
|
||||||
|
for l in out.lines().filter(|l| !l.trim().is_empty()) {
|
||||||
|
assert!(l.contains(" — "), "roster line without meaning: {l}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// #315: the persisted-tap vocabulary's meanings are readable where the taps
|
||||||
|
/// are advertised — under the `std::presentation` block's slot listing.
|
||||||
|
#[test]
|
||||||
|
fn campaign_introspect_block_presentation_lists_tap_meanings() {
|
||||||
|
let (out, err, ok) = run(&["campaign", "introspect", "--block", "std::presentation"]);
|
||||||
|
assert!(ok, "presentation describe failed: {err}");
|
||||||
|
assert!(out.contains("cumulative pip equity"), "equity tap meaning: {out}");
|
||||||
|
assert!(out.contains("after the cost model"), "net_r_equity tap meaning: {out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// #323: `introspect --unwired` enumerates the optional C29 `description`
|
||||||
|
/// slot for both document kinds — the authoring side of the gate is
|
||||||
|
/// advertised where every other slot is.
|
||||||
|
#[test]
|
||||||
|
fn introspect_unwired_lists_the_description_slot() {
|
||||||
|
let dir = std::env::temp_dir().join(format!("aura-selfdesc-{}", std::process::id()));
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let bare = dir.join("bare.json");
|
||||||
|
std::fs::write(&bare, "{}").unwrap();
|
||||||
|
for family in ["process", "campaign"] {
|
||||||
|
let (out, err, ok) = run(&[family, "introspect", "--unwired", bare.to_str().unwrap()]);
|
||||||
|
assert!(ok, "{family} --unwired failed: {err}");
|
||||||
|
assert!(
|
||||||
|
out.contains("open slot: description"),
|
||||||
|
"{family} --unwired lists the description slot: {out}"
|
||||||
|
);
|
||||||
|
assert!(out.contains("C29-gated"), "{family} hint names the gate: {out}");
|
||||||
|
}
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
@@ -871,13 +871,14 @@ fn campaign_introspect_unwired_lists_the_optional_risk_slot_on_bare_envelope() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// #216/#234: bound (non-empty) risk AND cost lists close both optional
|
/// #216/#234: bound (non-empty) risk AND cost lists close both optional
|
||||||
/// slots — a complete document with both reports no open slots at all.
|
/// slots — with the optional C29 description also present (#323), a complete
|
||||||
|
/// document reports no open slots at all.
|
||||||
#[test]
|
#[test]
|
||||||
fn campaign_introspect_unwired_omits_bound_risk_and_cost_slots() {
|
fn campaign_introspect_unwired_omits_bound_risk_and_cost_slots() {
|
||||||
let dir = temp_cwd("campaign-introspect-unwired-risk-bound");
|
let dir = temp_cwd("campaign-introspect-unwired-risk-bound");
|
||||||
let with_both = CAMPAIGN_DOC.replacen(
|
let with_both = CAMPAIGN_DOC.replacen(
|
||||||
"\"seed\": 1,",
|
"\"seed\": 1,",
|
||||||
"\"seed\": 1,\n \"risk\": [ { \"vol\": { \"length\": 3, \"k\": 2.0 } } ],\n \"cost\": [ { \"constant\": { \"cost_per_trade\": 2.0 } } ],",
|
"\"seed\": 1,\n \"description\": \"a screen with bound risk and cost\",\n \"risk\": [ { \"vol\": { \"length\": 3, \"k\": 2.0 } } ],\n \"cost\": [ { \"constant\": { \"cost_per_trade\": 2.0 } } ],",
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
assert_ne!(with_both, CAMPAIGN_DOC, "replacen must actually match the fixture's seed field");
|
assert_ne!(with_both, CAMPAIGN_DOC, "replacen must actually match the fixture's seed field");
|
||||||
|
|||||||
@@ -1326,6 +1326,14 @@ fn envelope_open_slots(v: &serde_json::Value, kind: &str, slots: &mut Vec<OpenSl
|
|||||||
if v.get("kind").and_then(|k| k.as_str()) != Some(kind) {
|
if v.get("kind").and_then(|k| k.as_str()) != Some(kind) {
|
||||||
slots.push(open("kind", format!("required, must be \"{kind}\"")));
|
slots.push(open("kind", format!("required, must be \"{kind}\"")));
|
||||||
}
|
}
|
||||||
|
// C29 (#323): the description slot is enumerable like every other
|
||||||
|
// envelope slot — optional, so only listed while absent.
|
||||||
|
if v.get("description").is_none() {
|
||||||
|
slots.push(open(
|
||||||
|
"description",
|
||||||
|
"optional, string — a one-line meaning (C29-gated when present)",
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -1470,6 +1478,7 @@ mod tests {
|
|||||||
"format_version": 1,
|
"format_version": 1,
|
||||||
"kind": "campaign",
|
"kind": "campaign",
|
||||||
"name": "ger40-momentum-screen",
|
"name": "ger40-momentum-screen",
|
||||||
|
"description": "GER40 momentum screen over the fast/slow SMA grid.",
|
||||||
"data": {
|
"data": {
|
||||||
"instruments": ["GER40"],
|
"instruments": ["GER40"],
|
||||||
"windows": [ { "from_ms": 1704067200000, "to_ms": 1719792000000 } ]
|
"windows": [ { "from_ms": 1704067200000, "to_ms": 1719792000000 } ]
|
||||||
|
|||||||
@@ -74,6 +74,6 @@ pub use sma::Sma;
|
|||||||
pub use sqrt::Sqrt;
|
pub use sqrt::Sqrt;
|
||||||
pub use sub::Sub;
|
pub use sub::Sub;
|
||||||
pub use tap_cell::newest_cell;
|
pub use tap_cell::newest_cell;
|
||||||
pub use tap_fold::{fold_binds_at, fold_output_kind, FoldKind, TapFold};
|
pub use tap_fold::{fold_binds_at, fold_output_kind, fold_vocabulary, FoldKind, FoldSchema, TapFold};
|
||||||
pub use tap_live::TapLive;
|
pub use tap_live::TapLive;
|
||||||
pub use when::When;
|
pub use when::When;
|
||||||
|
|||||||
@@ -45,6 +45,32 @@ pub fn fold_output_kind(fold: FoldKind, kind: ScalarKind) -> ScalarKind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One fold entry's introspection row (C29): id, bind/output rules in prose,
|
||||||
|
/// one-line meaning. `fold_vocabulary` is the binary's discovery surface for
|
||||||
|
/// [`FoldKind`] (`aura graph introspect --folds`, #315); the unit tests pin
|
||||||
|
/// each row's rule prose against [`fold_binds_at`] / [`fold_output_kind`] so
|
||||||
|
/// the table cannot drift from the executable rules.
|
||||||
|
pub struct FoldSchema {
|
||||||
|
pub id: &'static str,
|
||||||
|
pub kind: FoldKind,
|
||||||
|
pub binds: &'static str,
|
||||||
|
pub out: &'static str,
|
||||||
|
pub doc: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The closed fold vocabulary as introspection rows, in [`FoldKind`] order.
|
||||||
|
pub fn fold_vocabulary() -> &'static [FoldSchema] {
|
||||||
|
&[
|
||||||
|
FoldSchema { id: "Count", kind: FoldKind::Count, binds: "any tap", out: "i64", doc: "number of rows the tap recorded" },
|
||||||
|
FoldSchema { id: "Sum", kind: FoldKind::Sum, binds: "f64 taps", out: "f64", doc: "sum of the tap's recorded values" },
|
||||||
|
FoldSchema { id: "Mean", kind: FoldKind::Mean, binds: "f64 taps", out: "f64", doc: "arithmetic mean of the tap's recorded values" },
|
||||||
|
FoldSchema { id: "Min", kind: FoldKind::Min, binds: "f64 taps", out: "f64", doc: "smallest recorded value" },
|
||||||
|
FoldSchema { id: "Max", kind: FoldKind::Max, binds: "f64 taps", out: "f64", doc: "largest recorded value" },
|
||||||
|
FoldSchema { id: "First", kind: FoldKind::First, binds: "any tap", out: "tap kind", doc: "earliest recorded value, kind-preserving" },
|
||||||
|
FoldSchema { id: "Last", kind: FoldKind::Last, binds: "any tap", out: "tap kind", doc: "latest recorded value, kind-preserving" },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
/// Owned accumulator for every fold kind at once (a handful of words; keeping
|
/// Owned accumulator for every fold kind at once (a handful of words; keeping
|
||||||
/// it total avoids a per-kind state enum). Sequential `sum += v` matches a
|
/// it total avoids a per-kind state enum). Sequential `sum += v` matches a
|
||||||
/// slice-driven mean's operation order, so streamed and slice `Mean` agree
|
/// slice-driven mean's operation order, so streamed and slice `Mean` agree
|
||||||
@@ -164,6 +190,47 @@ mod tests {
|
|||||||
use aura_core::AnyColumn;
|
use aura_core::AnyColumn;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
/// The vocabulary rows are prose renderings of the executable rules —
|
||||||
|
/// pin them against `fold_binds_at` / `fold_output_kind` so the
|
||||||
|
/// introspection surface cannot drift from what the engine enforces.
|
||||||
|
#[test]
|
||||||
|
fn fold_vocabulary_rows_match_the_executable_rules() {
|
||||||
|
let rows = fold_vocabulary();
|
||||||
|
assert_eq!(rows.len(), 7, "one row per FoldKind");
|
||||||
|
for row in rows {
|
||||||
|
let f64_only = fold_binds_at(row.kind, ScalarKind::F64)
|
||||||
|
&& !fold_binds_at(row.kind, ScalarKind::I64);
|
||||||
|
match row.binds {
|
||||||
|
"f64 taps" => assert!(f64_only, "{} claims f64-only but binds wider", row.id),
|
||||||
|
"any tap" => assert!(
|
||||||
|
fold_binds_at(row.kind, ScalarKind::I64)
|
||||||
|
&& fold_binds_at(row.kind, ScalarKind::Bool),
|
||||||
|
"{} claims any-tap but refuses a kind",
|
||||||
|
row.id
|
||||||
|
),
|
||||||
|
other => panic!("unknown binds prose {other:?} on {}", row.id),
|
||||||
|
}
|
||||||
|
match row.out {
|
||||||
|
"i64" => assert_eq!(fold_output_kind(row.kind, ScalarKind::F64), ScalarKind::I64),
|
||||||
|
"f64" => assert_eq!(fold_output_kind(row.kind, ScalarKind::F64), ScalarKind::F64),
|
||||||
|
"tap kind" => {
|
||||||
|
assert_eq!(fold_output_kind(row.kind, ScalarKind::F64), ScalarKind::F64);
|
||||||
|
assert_eq!(fold_output_kind(row.kind, ScalarKind::I64), ScalarKind::I64);
|
||||||
|
}
|
||||||
|
other => panic!("unknown out prose {other:?} on {}", row.id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// C29 entry seam: every fold row ships a gate-clean one-line meaning.
|
||||||
|
#[test]
|
||||||
|
fn fold_vocabulary_docs_pass_the_doc_gate() {
|
||||||
|
for row in fold_vocabulary() {
|
||||||
|
aura_core::doc_gate(row.id, row.doc)
|
||||||
|
.unwrap_or_else(|f| panic!("fold {} doc fails the gate: {f:?}", row.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Drive a fold over an f64 series and return the emitted rows.
|
/// Drive a fold over an f64 series and return the emitted rows.
|
||||||
fn run_f64_fold(fold: FoldKind, values: &[f64]) -> Vec<(Timestamp, Vec<Scalar>)> {
|
fn run_f64_fold(fold: FoldKind, values: &[f64]) -> Vec<(Timestamp, Vec<Scalar>)> {
|
||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|||||||
@@ -122,7 +122,8 @@ impl Scale {
|
|||||||
pub fn new(factor: f64) -> Self {
|
pub fn new(factor: f64) -> Self {
|
||||||
Self { factor, out: [Cell::from_f64(0.0)] }
|
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 {
|
pub fn builder() -> PrimitiveBuilder {
|
||||||
PrimitiveBuilder::new(
|
PrimitiveBuilder::new(
|
||||||
"my_lab::Scale",
|
"my_lab::Scale",
|
||||||
|
|||||||
Reference in New Issue
Block a user