feat(cli, std, research): self-describing surfaces — two-layer help, per-entry meanings, op reference
The binary is the only always-present teacher (C29); the 2026-07-22 external field test showed the help teaching nothing but the verb inventory. This cycle makes every closed vocabulary the binary ships listable with a one-line meaning, and opens the help with the concepts the field agent had to discover forensically. #315 — help + introspection: - `aura --help` opens with the two-layer concepts paragraph (research verbs as sugar over registered process/campaign documents; the directly authorable data plane; bias-as-target-position + protective-stop execution model in R; how traces come to exist). - Each document-bridged sugar verb's long help names the process shape it desugars to (sweep -> std::sweep; walkforward -> [std::grid, std::walk_forward]; mc -> + std::monte_carlo; generalize -> [std::sweep (selection), std::generalize]). `run` is not verb_sugar-bridged, so its help points at the canonical document-first form instead of claiming a desugaring it does not perform. - `graph introspect --vocabulary` lists each node type with its schema doc (bare names taught nothing); `--node <T>`'s head line carries the meaning. - `graph introspect --folds` (new): the tap-fold vocabulary with bind rule, output kind, and meaning — it existed only as source comments. The rows live in aura-std (`fold_vocabulary`), unit-pinned against `fold_binds_at`/`fold_output_kind` so the prose cannot drift from the executable rules, and doc_gate-checked (C29 entry seam). The discovery surface is graph introspect because folds bind at graph-declared taps (C27); the document layer still refuses a folds slot pending #310. - `process introspect --metrics` lines carry each metric's meaning (the doc column existed since #316 but was never printed). - `campaign introspect --block std::presentation` lists the four persisted taps' meanings under the persist_taps slot. - The scaffolded project CLAUDE.md teaches the execution semantics (bias as held target, protective stop, R) and the document data plane. #323 — C29 authoring surfaces: - `introspect --unwired` enumerates the optional `description` envelope slot for both document kinds (absent-only, like risk/cost); the campaign lib fixture gains a description, and the risk/cost-bound e2e binds description too so "no open slots" keeps meaning complete. - `graph build --help` carries the op-list reference: all nine op kinds with fields and a worked element each (OP_REFERENCE lives beside OpDoc so a new variant is one screen from its help line). - The authoring guide's S0 worked literal is byte-pinned to the scaffold's LIB_RS template (rendered with the guide's my_lab namespace); the scaffold e2es compile that template for real, so the pin is a transitive compile guard. The template gains the guide's C29 comment, the guide gains the template's rename note — one worked example, two surfaces, zero drift. Deliberately out of scope (ratified #319, 2026-07-24): no doc-strings for the quintet's 48 flag fields — the sugar surface retires; only the pointer lines above bridge toward the document layer. Verification: full workspace suite green, clippy -D warnings clean, aura-bench exit 0 with all fingerprints OK (help_ms +1.6% — the longer help text, in tolerance), cargo doc warning count unchanged (7 pre-existing). 9 new e2e pins in tests/help_self_description.rs; guide + CLAUDE.md pins in scaffold unit tests; fold-vocabulary rules + doc_gate unit-pinned in aura-std. closes #315 closes #323
This commit is contained in:
@@ -18,6 +18,31 @@ use serde::Deserialize;
|
||||
#[cfg(test)]
|
||||
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,
|
||||
/// internally tagged by `"op"`, mapped into the serde-free engine `Op`. Bind
|
||||
/// 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> {
|
||||
let builder = env.resolve(type_id).ok_or_else(|| format!("unknown node type {type_id:?}"))?;
|
||||
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 {
|
||||
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
|
||||
+ cmd.node.is_some() as usize
|
||||
+ cmd.unwired as usize
|
||||
+ cmd.folds as usize
|
||||
+ cmd.params.is_some() as usize
|
||||
+ (cmd.content_id.is_some() || cmd.identity_id) as usize;
|
||||
if count != 1 {
|
||||
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);
|
||||
}
|
||||
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?).
|
||||
for t in env.type_ids() {
|
||||
println!("{t}");
|
||||
match env.resolve(t) {
|
||||
Some(b) => println!("{t:<18} {}", b.schema().doc),
|
||||
None => println!("{t}"),
|
||||
}
|
||||
}
|
||||
} 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() {
|
||||
match introspect_node(type_id, env) {
|
||||
|
||||
@@ -1026,11 +1026,30 @@ fn version_string() -> &'static str {
|
||||
/// The `aura` root parser. `version` is built from `CARGO_PKG_VERSION` (the
|
||||
/// workspace `0.1.0`) plus the parenthesized `ENGINE_COMMIT`, so
|
||||
/// `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 (run, sweep, walkforward, mc,
|
||||
generalize) are sugar — each writes registered process/campaign documents and
|
||||
executes them. 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)]
|
||||
#[command(
|
||||
name = "aura",
|
||||
version = version_string(),
|
||||
about = "Author, backtest, and validate trading strategies — research CLI",
|
||||
long_about = CONCEPTS_HELP,
|
||||
infer_long_args = true
|
||||
)]
|
||||
struct Cli {
|
||||
@@ -1044,22 +1063,42 @@ struct Cli {
|
||||
#[derive(Subcommand)]
|
||||
enum Command {
|
||||
/// 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),
|
||||
/// Render a recorded run's trace to an HTML chart.
|
||||
Chart(ChartCmd),
|
||||
/// Emit / construct / introspect a graph.
|
||||
Graph(GraphCmd),
|
||||
/// Sweep a parameter grid over a strategy or a loaded blueprint.
|
||||
///
|
||||
/// Sugar over the document layer: desugars to a registered process
|
||||
/// (std::sweep) + campaign pair — author the documents directly via
|
||||
/// `aura process` / `aura campaign`.
|
||||
Sweep(SweepCmd),
|
||||
/// 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
|
||||
/// the 3:1 IS:OOS ratio, instead of refusing the window outright.
|
||||
///
|
||||
/// Sugar over the document layer: desugars to a registered process
|
||||
/// ([std::grid, std::walk_forward]) + campaign pair — author the documents
|
||||
/// directly via `aura process` / `aura campaign`.
|
||||
Walkforward(WalkforwardCmd),
|
||||
/// 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),
|
||||
/// 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
|
||||
/// shorter than it, preserving the 3:1 IS:OOS ratio, instead of refusing outright.
|
||||
///
|
||||
/// Sugar over the document layer: 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),
|
||||
/// List or inspect recorded run families.
|
||||
Runs(RunsCmd),
|
||||
@@ -1181,6 +1220,7 @@ struct GraphCmd {
|
||||
#[derive(Subcommand)]
|
||||
enum GraphSub {
|
||||
/// Construct a graph from a stdin op-list.
|
||||
#[command(after_help = crate::graph_construct::OP_REFERENCE)]
|
||||
Build,
|
||||
/// Introspect a graph.
|
||||
Introspect(GraphIntrospectCmd),
|
||||
@@ -1193,7 +1233,7 @@ enum GraphSub {
|
||||
|
||||
#[derive(Args)]
|
||||
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)]
|
||||
vocabulary: bool,
|
||||
/// Describe one node type's ports by name.
|
||||
@@ -1202,6 +1242,9 @@ struct GraphIntrospectCmd {
|
||||
/// List the graph's unwired (unbound) ports.
|
||||
#[arg(long)]
|
||||
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
|
||||
/// document from it (a blueprint envelope or an op-list, shape-
|
||||
/// discriminated, #196); without FILE, read a stdin op-list as before.
|
||||
|
||||
@@ -107,10 +107,12 @@ fn print_metric_roster() {
|
||||
(true, false) => "rankable",
|
||||
(false, false) => "annotation",
|
||||
};
|
||||
// C29 (#315): the roster carries each metric's one-line meaning, not
|
||||
// only where it may be used.
|
||||
if generalize {
|
||||
println!("{name:<24} {base} | generalize");
|
||||
println!("{name:<24} {base} | generalize — {}", m.doc);
|
||||
} 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 {
|
||||
let req = if s.required { "required" } else { "optional" };
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -165,6 +165,8 @@ 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())),
|
||||
@@ -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
|
||||
attaches it via `[nodes]` in `Aura.toml` (build it with `cargo build`).
|
||||
- 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__`
|
||||
@@ -455,6 +465,41 @@ mod tests {
|
||||
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
|
||||
/// 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
|
||||
|
||||
Reference in New Issue
Block a user