Compare commits
5 Commits
bbac29db2d
...
32eb5a6a9e
| Author | SHA1 | Date | |
|---|---|---|---|
| 32eb5a6a9e | |||
| 564a767974 | |||
| 4ff85b94e5 | |||
| 10570b75f8 | |||
| a851af993a |
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"surface": "cli_fixed_cost",
|
"surface": "cli_fixed_cost",
|
||||||
"metrics": {
|
"metrics": {
|
||||||
"help_ms": 1.5046899999999999,
|
"help_ms": 1.487171,
|
||||||
"run_ms": 3.63305
|
"run_ms": 3.376192
|
||||||
},
|
},
|
||||||
"fingerprint": "run_line_fnv=6bb0d796f760d140",
|
"fingerprint": "run_line_fnv=9bdbc3acf7b2926a",
|
||||||
"reps": 3,
|
"reps": 3,
|
||||||
"host": {
|
"host": {
|
||||||
"hostname": "Raki",
|
"hostname": "Raki",
|
||||||
"nproc": 24
|
"nproc": 24
|
||||||
},
|
},
|
||||||
"profile": "release",
|
"profile": "release",
|
||||||
"commit": "9c7f60b",
|
"commit": "10570b7",
|
||||||
"date": "2026-07-23"
|
"date": "2026-07-25"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ use std::collections::BTreeMap;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use aura_engine::{
|
use aura_engine::{
|
||||||
blueprint_from_json, blueprint_identity_json, blueprint_to_json, replay, ArgOpError,
|
blueprint_from_json, blueprint_identity_json, blueprint_to_json, name_gate, replay, ArgOpError,
|
||||||
BindOpError, BlueprintDoc, CompileError, Composite, GraphSession, LoadError, Op, OpError,
|
BindOpError, BlueprintDoc, CompileError, Composite, GraphSession, LoadError, NameGateFault, Op,
|
||||||
Scalar, ScalarKind,
|
OpError, Scalar, ScalarKind,
|
||||||
};
|
};
|
||||||
use aura_runner::runner::render_value;
|
use aura_runner::runner::render_value;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -22,9 +22,10 @@ use crate::research_docs::resolve_id_prefix;
|
|||||||
#[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 ten
|
/// The op-list reference `aura graph build --help` appends (#323): the eleven
|
||||||
/// op kinds with their fields and one worked element each. Lives beside
|
/// op kinds with their fields and one worked element each (#331: `name` joins
|
||||||
/// [`OpDoc`] so a new op variant is one screen away from its help line.
|
/// the roster, ten -> eleven). 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):
|
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"}
|
{"op":"source","role":"price","kind":"F64"}
|
||||||
declare a bound root input role of a scalar kind
|
declare a bound root input role of a scalar kind
|
||||||
@@ -50,6 +51,9 @@ pub const OP_REFERENCE: &str = r#"Op-list reference (stdin: a JSON array of op o
|
|||||||
{"op":"use","ref":{"name":"agree"},"name":"gate","bind":{"sma.length":{"I64":9}}}
|
{"op":"use","ref":{"name":"agree"},"name":"gate","bind":{"sma.length":{"I64":9}}}
|
||||||
splice a registered blueprint (by "content_id" or "name") under an
|
splice a registered blueprint (by "content_id" or "name") under an
|
||||||
instance name ("bind" path-qualifies the spliced instance's params)
|
instance name ("bind" path-qualifies the spliced instance's params)
|
||||||
|
{"op":"name","name":"ny_momentum"}
|
||||||
|
set the composite's render name, at most once per script (default
|
||||||
|
"graph" if omitted)
|
||||||
|
|
||||||
Node types and their ports: aura graph introspect --vocabulary | --node <T>"#;
|
Node types and their ports: aura graph introspect --vocabulary | --node <T>"#;
|
||||||
|
|
||||||
@@ -111,6 +115,9 @@ enum OpDoc {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
bind: BTreeMap<String, Scalar>,
|
bind: BTreeMap<String, Scalar>,
|
||||||
},
|
},
|
||||||
|
/// Set the composite's render name (#331) — script-level, at most once;
|
||||||
|
/// the op-script twin of `replay`'s seeded default name (`"graph"`).
|
||||||
|
Name { name: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A `use` op's reference (#317) — exactly one of a store content id (full
|
/// A `use` op's reference (#317) — exactly one of a store content id (full
|
||||||
@@ -143,6 +150,7 @@ impl OpDoc {
|
|||||||
OpDoc::Doc { .. } => "doc".to_string(),
|
OpDoc::Doc { .. } => "doc".to_string(),
|
||||||
OpDoc::Use { name: Some(n), .. } => format!("use {n:?}"),
|
OpDoc::Use { name: Some(n), .. } => format!("use {n:?}"),
|
||||||
OpDoc::Use { name: None, .. } => "use".to_string(),
|
OpDoc::Use { name: None, .. } => "use".to_string(),
|
||||||
|
OpDoc::Name { .. } => "name".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,6 +190,7 @@ impl From<OpDoc> for Op {
|
|||||||
name,
|
name,
|
||||||
bind: bind.into_iter().collect(),
|
bind: bind.into_iter().collect(),
|
||||||
},
|
},
|
||||||
|
OpDoc::Name { name } => Op::Name { name },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,9 +266,25 @@ fn format_op_error(e: &OpError) -> String {
|
|||||||
OpError::UnknownSubgraph { ref_id } => {
|
OpError::UnknownSubgraph { ref_id } => {
|
||||||
format!("use: no subgraph for content id {ref_id:?}")
|
format!("use: no subgraph for content id {ref_id:?}")
|
||||||
}
|
}
|
||||||
|
OpError::DuplicateName => "a script names its blueprint at most once".to_string(),
|
||||||
|
OpError::BadName { name, fault } => name_gate_fault_prose(name, fault),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Phrase a `name_gate` shape violation as prose (#331): shared by this
|
||||||
|
/// module's op-intake `format_op_error` `BadName` arm and the
|
||||||
|
/// blueprint-envelope intake's root-name gate (`composite_from_authored_text`
|
||||||
|
/// below) — the shape rule has exactly one seam-independent cause per fault,
|
||||||
|
/// so both data-borne birth routes for a name read identically.
|
||||||
|
fn name_gate_fault_prose(name: &str, fault: &NameGateFault) -> String {
|
||||||
|
let cause = match fault {
|
||||||
|
NameGateFault::Empty => "must be non-empty",
|
||||||
|
NameGateFault::ContainsSeparator => "must not contain '/' or '\\'",
|
||||||
|
NameGateFault::DotSegment => "must not be \".\" or \"..\"",
|
||||||
|
};
|
||||||
|
format!("blueprint name {name:?} is invalid: {cause} (a single path segment)")
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve one `use` op's [`UseRef`] to the full store content id (#317):
|
/// Resolve one `use` op's [`UseRef`] to the full store content id (#317):
|
||||||
/// verbatim if it already IS a 64-hex content id, else a unique content-id
|
/// verbatim if it already IS a 64-hex content id, else a unique content-id
|
||||||
/// prefix (#302 semantics, reusing [`resolve_id_prefix`]) or a registry name
|
/// prefix (#302 semantics, reusing [`resolve_id_prefix`]) or a registry name
|
||||||
@@ -649,7 +674,7 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd, env: &aura_runner::project
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match composite_from_any(&text, env) {
|
match composite_from_authored_text(&text, env) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(m) => {
|
Err(m) => {
|
||||||
eprintln!("aura: {m}");
|
eprintln!("aura: {m}");
|
||||||
@@ -819,6 +844,18 @@ pub(crate) fn blueprint_slot_prose(doc: &str, env: &aura_runner::project::Env) -
|
|||||||
/// envelope (a JSON object: `format_version` + `blueprint`) OR a construction
|
/// envelope (a JSON object: `format_version` + `blueprint`) OR a construction
|
||||||
/// op-list (a JSON array) — shape-discriminated on the top-level JSON type,
|
/// op-list (a JSON array) — shape-discriminated on the top-level JSON type,
|
||||||
/// each canonicalized by its own rules.
|
/// each canonicalized by its own rules.
|
||||||
|
///
|
||||||
|
/// **Ungated by design (#331 review finding).** This is the STORE READ-BACK
|
||||||
|
/// shape: `params_lines`'s content-id fetch (`resolve_blueprint_text`'s
|
||||||
|
/// non-file branch) — `introspect --params <ID>` and, by the same
|
||||||
|
/// convention, `validate_campaign_refs`'s already-ungated `blueprint_from_json`
|
||||||
|
/// call — reads whatever is already sitting in the store, and C29 says a
|
||||||
|
/// registered artifact is never retroactively invalidated. Freshly authored
|
||||||
|
/// FILE text (a hand-edited document that has not yet passed through a gated
|
||||||
|
/// intake) goes through [`composite_from_authored_text`] instead, never here
|
||||||
|
/// — `params_lines`'s OWN file branch is such a case (#331 delta re-review:
|
||||||
|
/// it used to call straight through to this fn, missing the gate; fixed by
|
||||||
|
/// branching on `resolve_blueprint_text`'s file-vs-store flag).
|
||||||
pub(crate) fn composite_from_any(text: &str, env: &aura_runner::project::Env) -> Result<Composite, String> {
|
pub(crate) fn composite_from_any(text: &str, env: &aura_runner::project::Env) -> Result<Composite, String> {
|
||||||
let value: serde_json::Value =
|
let value: serde_json::Value =
|
||||||
serde_json::from_str(text).map_err(|e| format!("invalid document: {e}"))?;
|
serde_json::from_str(text).map_err(|e| format!("invalid document: {e}"))?;
|
||||||
@@ -836,12 +873,86 @@ pub(crate) fn composite_from_any(text: &str, env: &aura_runner::project::Env) ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The FILE-intake counterpart of [`composite_from_any`] (#331 review
|
||||||
|
/// finding): identical parse, plus the blueprint-envelope root-name shape
|
||||||
|
/// gate. Every call site that builds a `Composite` from text freshly read
|
||||||
|
/// off disk (`graph register`, `graph introspect --content-id <FILE>`, the
|
||||||
|
/// bare `aura graph <FILE>` viewer, and `graph introspect --params <FILE>`'s
|
||||||
|
/// file branch) goes through this wrapper — never the ungated
|
||||||
|
/// `composite_from_any` — because a hand-edited envelope with
|
||||||
|
/// `"name":"../x"` would otherwise register/build cleanly and write
|
||||||
|
/// `traces/../x/` at run time (`trace_store.rs` joins the name unsanitized).
|
||||||
|
/// Gating the composite's name unconditionally (not just on the Object
|
||||||
|
/// branch) is harmless: an op-script-built composite's name already passed
|
||||||
|
/// the op-intake gate (`GraphSession::set_name`), so re-checking it here is
|
||||||
|
/// redundant, not restrictive — it keeps this wrapper a single shared choke
|
||||||
|
/// point rather than one that has to re-discriminate the JSON shape. Only
|
||||||
|
/// the ROOT name is checked: the filesystem seam consumes exclusively the
|
||||||
|
/// root name. Store read-back (reproduce, `use`-splice resolution from the
|
||||||
|
/// registry, and `params_lines`'s content-id branch) never reaches this
|
||||||
|
/// function — C29's "registered artifacts are never retroactively
|
||||||
|
/// invalidated" stays intact.
|
||||||
|
///
|
||||||
|
/// **Deliberate exceptions — direct `gate_authored_root_name` callers
|
||||||
|
/// (main.rs).** A handful of fresh-FILE intakes reach the same unsanitized
|
||||||
|
/// `traces/<name>/` seam (or `put_blueprint` the loaded envelope straight
|
||||||
|
/// into the registry) without going through this wrapper, because each
|
||||||
|
/// already parses the document its own way and only needs the shared root-
|
||||||
|
/// name gate bolted on, not the shape-discrimination `composite_from_any`
|
||||||
|
/// does:
|
||||||
|
/// - `aura run <blueprint.json>` (`dispatch_run`): envelope-only grammar (no
|
||||||
|
/// op-script fallback), feeds `signal.name()` into
|
||||||
|
/// `run_signal_r`/`run_measurement` -> `bind_tap_plan` ->
|
||||||
|
/// `TraceStore::begin_run`.
|
||||||
|
/// - `validate_and_register_axes` (shared by `generalize`,
|
||||||
|
/// `sweep --real`, `walkforward --real`, `mc --real`): `put_blueprint`s
|
||||||
|
/// the loaded envelope by topology hash before any of those four verbs
|
||||||
|
/// touches an archive.
|
||||||
|
/// - `run_blueprint_sweep` / `run_blueprint_walkforward` / `run_blueprint_mc`
|
||||||
|
/// (the synthetic, no-`--real` family builders): same `put_blueprint`
|
||||||
|
/// reason, on the routes that bypass `validate_and_register_axes`
|
||||||
|
/// entirely (#331 cycle-close — these used to plant an ungated envelope
|
||||||
|
/// in the store; see each fn's own comment).
|
||||||
|
/// - `list_blueprint_axes` (`aura sweep <FILE> --list-axes`, main.rs): no
|
||||||
|
/// registry write and no trace directory here, but a shape-violating root
|
||||||
|
/// name otherwise mangles through `wrapped_to_raw_axis` into a printed,
|
||||||
|
/// non-bindable axis name instead of refusing (#331 fieldtest finding
|
||||||
|
/// c331_2e) — the class rule is every CLI intake reading an authored
|
||||||
|
/// envelope from a file gates the root name, not only the writing ones.
|
||||||
|
///
|
||||||
|
/// All of these share the identical `name_gate` + `name_gate_fault_prose`
|
||||||
|
/// primitives this wrapper uses, so the refusal wording is byte-identical
|
||||||
|
/// across every authored-file-intake route even though the shape-
|
||||||
|
/// discrimination step is not shared by them.
|
||||||
|
pub(crate) fn composite_from_authored_text(
|
||||||
|
text: &str,
|
||||||
|
env: &aura_runner::project::Env,
|
||||||
|
) -> Result<Composite, String> {
|
||||||
|
let composite = composite_from_any(text, env)?;
|
||||||
|
gate_authored_root_name(composite.name())?;
|
||||||
|
Ok(composite)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The root-name shape gate itself (#331 delta re-review), factored out of
|
||||||
|
/// [`composite_from_authored_text`] so `dispatch_run`'s narrower-grammar file
|
||||||
|
/// intake (see that fn's doc comment) can share the exact `name_gate` call and
|
||||||
|
/// `name_gate_fault_prose` wording without going through the shape-discriminating
|
||||||
|
/// wrapper.
|
||||||
|
pub(crate) fn gate_authored_root_name(name: &str) -> Result<(), String> {
|
||||||
|
name_gate(name).map_err(|fault| name_gate_fault_prose(name, &fault))
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve a blueprint document's bytes from a file path or a 64-hex content
|
/// Resolve a blueprint document's bytes from a file path or a 64-hex content
|
||||||
/// id in the project store (the campaign-run target-addressing convention).
|
/// id in the project store (the campaign-run target-addressing convention).
|
||||||
/// The id shape is `aura_runner::axes::is_content_id` — the same predicate
|
/// The id shape is `aura_runner::axes::is_content_id` — the same predicate
|
||||||
/// `campaign run`'s target resolution uses, so the two FILE-or-id surfaces
|
/// `campaign run`'s target resolution uses, so the two FILE-or-id surfaces
|
||||||
/// cannot drift apart on what counts as a store address.
|
/// cannot drift apart on what counts as a store address. The `bool` names
|
||||||
fn resolve_blueprint_text(target: &str, env: &aura_runner::project::Env) -> Result<String, String> {
|
/// which branch fired: `true` for a FRESH FILE read, `false` for a STORE
|
||||||
|
/// (content-id) fetch — `params_lines` (#331 delta re-review) uses it to
|
||||||
|
/// decide whether the root-name gate applies (a fresh file must gate; a
|
||||||
|
/// store read-back must not, C29) without re-deriving the FILE-vs-id
|
||||||
|
/// distinction a second time.
|
||||||
|
fn resolve_blueprint_text(target: &str, env: &aura_runner::project::Env) -> Result<(String, bool), String> {
|
||||||
// A CLI arg tolerates the `content:` display prefix (#194); doc ref
|
// A CLI arg tolerates the `content:` display prefix (#194); doc ref
|
||||||
// fields stay bare-only.
|
// fields stay bare-only.
|
||||||
let target = target
|
let target = target
|
||||||
@@ -851,11 +962,12 @@ fn resolve_blueprint_text(target: &str, env: &aura_runner::project::Env) -> Resu
|
|||||||
let path = Path::new(target);
|
let path = Path::new(target);
|
||||||
if path.is_file() {
|
if path.is_file() {
|
||||||
return std::fs::read_to_string(path)
|
return std::fs::read_to_string(path)
|
||||||
|
.map(|text| (text, true))
|
||||||
.map_err(|e| format!("cannot read {}: {e}", path.display()));
|
.map_err(|e| format!("cannot read {}: {e}", path.display()));
|
||||||
}
|
}
|
||||||
if aura_runner::axes::is_content_id(target) {
|
if aura_runner::axes::is_content_id(target) {
|
||||||
return match env.registry().get_blueprint(target) {
|
return match env.registry().get_blueprint(target) {
|
||||||
Ok(Some(json)) => Ok(json),
|
Ok(Some(json)) => Ok((json, false)),
|
||||||
Ok(None) => Err(format!("no blueprint {target} in the project store")),
|
Ok(None) => Err(format!("no blueprint {target} in the project store")),
|
||||||
Err(e) => Err(e.to_string()),
|
Err(e) => Err(e.to_string()),
|
||||||
};
|
};
|
||||||
@@ -872,12 +984,16 @@ fn resolve_blueprint_text(target: &str, env: &aura_runner::project::Env) -> Resu
|
|||||||
/// `bound_param_space()`'s names are already RAW (#203), so no blueprint-name
|
/// `bound_param_space()`'s names are already RAW (#203), so no blueprint-name
|
||||||
/// concatenation is needed — line-identical to the reconciled `--list-axes`
|
/// concatenation is needed — line-identical to the reconciled `--list-axes`
|
||||||
/// bound pass (`list_blueprint_axes`, main.rs), same `render_value` lexicon.
|
/// bound pass (`list_blueprint_axes`, main.rs), same `render_value` lexicon.
|
||||||
|
/// #331 delta re-review: the FILE target is a FOURTH freshly-authored-file
|
||||||
|
/// intake this fn's own `composite_from_any` call had missed gating — a FILE
|
||||||
|
/// routes through [`composite_from_authored_text`] instead; a content id
|
||||||
|
/// (STORE read-back) keeps the ungated `composite_from_any` (C29).
|
||||||
fn params_lines(target: &str, env: &aura_runner::project::Env) -> Result<String, String> {
|
fn params_lines(target: &str, env: &aura_runner::project::Env) -> Result<String, String> {
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
let text = resolve_blueprint_text(target, env)?;
|
let (text, is_file) = resolve_blueprint_text(target, env)?;
|
||||||
// Shape-discriminated like file-mode --content-id: an op-script (array)
|
// Shape-discriminated like file-mode --content-id: an op-script (array)
|
||||||
// builds through the one-build tail, an envelope (object) loads (#202).
|
// builds through the one-build tail, an envelope (object) loads (#202).
|
||||||
let composite = composite_from_any(&text, env)?;
|
let composite = if is_file { composite_from_authored_text(&text, env)? } else { composite_from_any(&text, env)? };
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
for p in composite.param_space() {
|
for p in composite.param_space() {
|
||||||
let _ = writeln!(out, "{}:{:?}", p.name, p.kind);
|
let _ = writeln!(out, "{}:{:?}", p.name, p.kind);
|
||||||
@@ -918,7 +1034,9 @@ fn register_blueprint(
|
|||||||
.map_err(|e| format!("cannot read {}: {e}", file.display()))?;
|
.map_err(|e| format!("cannot read {}: {e}", file.display()))?;
|
||||||
// Shape-discriminated like file-mode --content-id (#202): either shape
|
// Shape-discriminated like file-mode --content-id (#202): either shape
|
||||||
// canonicalizes to the envelope form, so the stored id is shape-invariant.
|
// canonicalizes to the envelope form, so the stored id is shape-invariant.
|
||||||
let composite = composite_from_any(&text, env)?;
|
// Gated (#331 review finding): this text is freshly authored FILE bytes,
|
||||||
|
// not a store read-back, so `composite_from_authored_text` applies.
|
||||||
|
let composite = composite_from_authored_text(&text, env)?;
|
||||||
let canonical = blueprint_to_json(&composite).map_err(|e| format!("serialize error: {e:?}"))?;
|
let canonical = blueprint_to_json(&composite).map_err(|e| format!("serialize error: {e:?}"))?;
|
||||||
let id = crate::content_id(&canonical);
|
let id = crate::content_id(&canonical);
|
||||||
let registry = env.registry();
|
let registry = env.registry();
|
||||||
|
|||||||
+90
-26
@@ -815,15 +815,29 @@ impl IcReport {
|
|||||||
/// alongside it, so the discovery surface must list it regardless too. Names
|
/// alongside it, so the discovery surface must list it regardless too. Names
|
||||||
/// are RAW `<node>.<param>` paths (#328: the wrapped `<blueprint>.<node>.<param>`
|
/// are RAW `<node>.<param>` paths (#328: the wrapped `<blueprint>.<node>.<param>`
|
||||||
/// form is retired from the surface) — exactly what `--axis` binds.
|
/// form is retired from the surface) — exactly what `--axis` binds.
|
||||||
|
///
|
||||||
|
/// **Gated (#331 fieldtest finding).** This is a FRESH-FILE intake exactly like
|
||||||
|
/// `register` / `introspect --content-id <FILE>` / `introspect --params <FILE>`
|
||||||
|
/// (`composite_from_authored_text`'s doc comment lists the class): a hand-edited
|
||||||
|
/// envelope with a shape-violating root name (`"../x"`) must be refused before
|
||||||
|
/// any axis line prints, not mangled through `wrapped_to_raw_axis` into a
|
||||||
|
/// non-bindable name. Gated immediately after load, before any axis derivation
|
||||||
|
/// — same placement discipline as the other sites — at exit 1, matching the
|
||||||
|
/// register-route convention (this route never `put_blueprint`s anything,
|
||||||
|
/// unlike the exit-2 sweep-EXECUTION route `run_blueprint_sweep` gates).
|
||||||
fn list_blueprint_axes(doc: &str, env: &aura_runner::project::Env) {
|
fn list_blueprint_axes(doc: &str, env: &aura_runner::project::Env) {
|
||||||
|
let signal = blueprint_from_json(doc, &|t| env.resolve(t))
|
||||||
|
.expect("doc parse-validated at the dispatch boundary");
|
||||||
|
if let Err(msg) = graph_construct::gate_authored_root_name(signal.name()) {
|
||||||
|
eprintln!("aura: {msg}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
let space = blueprint_axis_probe(doc, env).param_space();
|
let space = blueprint_axis_probe(doc, env).param_space();
|
||||||
for p in &space {
|
for p in &space {
|
||||||
// Strip the wrapper's one leading node segment (#328): open names print
|
// Strip the wrapper's one leading node segment (#328): open names print
|
||||||
// RAW, matching `--axis`'s own accepted namespace.
|
// RAW, matching `--axis`'s own accepted namespace.
|
||||||
println!("{}:{:?}", aura_runner::axes::wrapped_to_raw_axis(&p.name), p.kind); // ScalarKind Debug -> I64/F64/Bool/Timestamp
|
println!("{}:{:?}", aura_runner::axes::wrapped_to_raw_axis(&p.name), p.kind); // ScalarKind Debug -> I64/F64/Bool/Timestamp
|
||||||
}
|
}
|
||||||
let signal = blueprint_from_json(doc, &|t| env.resolve(t))
|
|
||||||
.expect("doc parse-validated at the dispatch boundary");
|
|
||||||
for b in signal.bound_param_space() {
|
for b in signal.bound_param_space() {
|
||||||
// `bound_param_space()`'s `.name` is already RAW (#203) — no blueprint-name
|
// `bound_param_space()`'s `.name` is already RAW (#203) — no blueprint-name
|
||||||
// concatenation (#328: the blueprint name stays out of axis paths, C23).
|
// concatenation (#328: the blueprint name stays out of axis paths, C23).
|
||||||
@@ -901,6 +915,18 @@ fn run_blueprint_sweep(
|
|||||||
env: &aura_runner::project::Env,
|
env: &aura_runner::project::Env,
|
||||||
) {
|
) {
|
||||||
let _ = persist; // reserved for the deferred per-member trace path; the family record below is unconditional
|
let _ = persist; // reserved for the deferred per-member trace path; the family record below is unconditional
|
||||||
|
// #331 cycle-close: this fn `put_blueprint`s the loaded fresh-FILE
|
||||||
|
// envelope into the registry below — gate the root name up front,
|
||||||
|
// before the family ever builds (a shape-violating name can otherwise
|
||||||
|
// confuse the wrapped/raw axis resolution the family build performs,
|
||||||
|
// producing a misleading unrelated error instead of this refusal; see
|
||||||
|
// `gate_authored_root_name`'s doc comment for the full route list).
|
||||||
|
let blueprint = blueprint_from_json(doc, &|t| env.resolve(t))
|
||||||
|
.expect("doc parse-validated at the dispatch boundary");
|
||||||
|
if let Err(msg) = graph_construct::gate_authored_root_name(blueprint.name()) {
|
||||||
|
eprintln!("aura: {msg}");
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
let family = blueprint_sweep_family(doc, axes, &data, env).unwrap_or_else(|e| {
|
let family = blueprint_sweep_family(doc, axes, &data, env).unwrap_or_else(|e| {
|
||||||
eprintln!("aura: {e}");
|
eprintln!("aura: {e}");
|
||||||
std::process::exit(2);
|
std::process::exit(2);
|
||||||
@@ -915,11 +941,7 @@ fn run_blueprint_sweep(
|
|||||||
.topology_hash
|
.topology_hash
|
||||||
.clone()
|
.clone()
|
||||||
.expect("a blueprint sweep stamps every member's topology_hash");
|
.expect("a blueprint sweep stamps every member's topology_hash");
|
||||||
let canonical = blueprint_to_json(
|
let canonical = blueprint_to_json(&blueprint).expect("a loaded blueprint re-serializes");
|
||||||
&blueprint_from_json(doc, &|t| env.resolve(t))
|
|
||||||
.expect("doc parse-validated at the dispatch boundary"),
|
|
||||||
)
|
|
||||||
.expect("a loaded blueprint re-serializes");
|
|
||||||
reg.put_blueprint(&topo, &canonical).unwrap_or_else(|e| {
|
reg.put_blueprint(&topo, &canonical).unwrap_or_else(|e| {
|
||||||
eprintln!("aura: {e}");
|
eprintln!("aura: {e}");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
@@ -947,6 +969,16 @@ fn run_blueprint_walkforward(
|
|||||||
doc: &str, axes: &[(String, Vec<Scalar>)], name: &str, data: DataSource, select: Selection,
|
doc: &str, axes: &[(String, Vec<Scalar>)], name: &str, data: DataSource, select: Selection,
|
||||||
env: &aura_runner::project::Env,
|
env: &aura_runner::project::Env,
|
||||||
) {
|
) {
|
||||||
|
// #331 cycle-close: this fn `put_blueprint`s the loaded fresh-FILE
|
||||||
|
// envelope into the registry below — gate the root name up front,
|
||||||
|
// before the family ever builds (see `run_blueprint_sweep`'s identical
|
||||||
|
// comment on why this must precede the family build, not just the
|
||||||
|
// registry write).
|
||||||
|
let blueprint = blueprint_from_json(doc, &|t| env.resolve(t)).expect("doc parse-validated at the dispatch boundary");
|
||||||
|
if let Err(msg) = graph_construct::gate_authored_root_name(blueprint.name()) {
|
||||||
|
eprintln!("aura: {msg}");
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
let result = blueprint_walkforward_family(doc, axes, &data, select, env);
|
let result = blueprint_walkforward_family(doc, axes, &data, select, env);
|
||||||
let reg = env.registry();
|
let reg = env.registry();
|
||||||
let topo = result.windows[0]
|
let topo = result.windows[0]
|
||||||
@@ -956,10 +988,7 @@ fn run_blueprint_walkforward(
|
|||||||
.topology_hash
|
.topology_hash
|
||||||
.clone()
|
.clone()
|
||||||
.expect("a blueprint walk-forward stamps every member's topology_hash");
|
.expect("a blueprint walk-forward stamps every member's topology_hash");
|
||||||
let canonical = blueprint_to_json(
|
let canonical = blueprint_to_json(&blueprint).expect("a loaded blueprint re-serializes");
|
||||||
&blueprint_from_json(doc, &|t| env.resolve(t)).expect("doc parse-validated at the dispatch boundary"),
|
|
||||||
)
|
|
||||||
.expect("a loaded blueprint re-serializes");
|
|
||||||
reg.put_blueprint(&topo, &canonical).unwrap_or_else(|e| { eprintln!("aura: {e}"); std::process::exit(1); });
|
reg.put_blueprint(&topo, &canonical).unwrap_or_else(|e| { eprintln!("aura: {e}"); std::process::exit(1); });
|
||||||
let id = match reg.append_family(name, FamilyKind::WalkForward, &walkforward_member_reports(&result)) {
|
let id = match reg.append_family(name, FamilyKind::WalkForward, &walkforward_member_reports(&result)) {
|
||||||
Ok(id) => id,
|
Ok(id) => id,
|
||||||
@@ -983,6 +1012,17 @@ fn run_blueprint_walkforward(
|
|||||||
/// `FamilyKind::MonteCarlo` family (C18/C21 lineage), and print each draw's member line
|
/// `FamilyKind::MonteCarlo` family (C18/C21 lineage), and print each draw's member line
|
||||||
/// (carrying the seed) plus the aggregate — mirroring `run_blueprint_sweep`.
|
/// (carrying the seed) plus the aggregate — mirroring `run_blueprint_sweep`.
|
||||||
fn run_blueprint_mc(doc: &str, n_seeds: u64, name: &str, data: DataSource, env: &aura_runner::project::Env) {
|
fn run_blueprint_mc(doc: &str, n_seeds: u64, name: &str, data: DataSource, env: &aura_runner::project::Env) {
|
||||||
|
// #331 cycle-close: this fn `put_blueprint`s the loaded fresh-FILE
|
||||||
|
// envelope into the registry below — gate the root name up front,
|
||||||
|
// before the family ever builds (see `run_blueprint_sweep`'s identical
|
||||||
|
// comment on why this must precede the family build, not just the
|
||||||
|
// registry write).
|
||||||
|
let blueprint = blueprint_from_json(doc, &|t| env.resolve(t))
|
||||||
|
.expect("doc parse-validated at the dispatch boundary");
|
||||||
|
if let Err(msg) = graph_construct::gate_authored_root_name(blueprint.name()) {
|
||||||
|
eprintln!("aura: {msg}");
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
let family = blueprint_mc_family(doc, n_seeds, &data, env).unwrap_or_else(|e| {
|
let family = blueprint_mc_family(doc, n_seeds, &data, env).unwrap_or_else(|e| {
|
||||||
eprintln!("aura: {e}");
|
eprintln!("aura: {e}");
|
||||||
std::process::exit(2);
|
std::process::exit(2);
|
||||||
@@ -995,11 +1035,7 @@ fn run_blueprint_mc(doc: &str, n_seeds: u64, name: &str, data: DataSource, env:
|
|||||||
.topology_hash
|
.topology_hash
|
||||||
.clone()
|
.clone()
|
||||||
.expect("a blueprint mc stamps every member's topology_hash");
|
.expect("a blueprint mc stamps every member's topology_hash");
|
||||||
let canonical = blueprint_to_json(
|
let canonical = blueprint_to_json(&blueprint).expect("a loaded blueprint re-serializes");
|
||||||
&blueprint_from_json(doc, &|t| env.resolve(t))
|
|
||||||
.expect("doc parse-validated at the dispatch boundary"),
|
|
||||||
)
|
|
||||||
.expect("a loaded blueprint re-serializes");
|
|
||||||
reg.put_blueprint(&topo, &canonical).unwrap_or_else(|e| {
|
reg.put_blueprint(&topo, &canonical).unwrap_or_else(|e| {
|
||||||
eprintln!("aura: {e}");
|
eprintln!("aura: {e}");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
@@ -1699,13 +1735,19 @@ fn parse_axes(
|
|||||||
Ok(axes)
|
Ok(axes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The three distinguishable ways `validate_and_register_axes` can fail: an
|
/// The four distinguishable ways `validate_and_register_axes` can fail: a
|
||||||
/// unknown axis name is a usage error (exit 2, echoed before the archive is
|
/// shape-violating root name is a usage error (exit 2, echoed before the
|
||||||
/// touched); a registry write failure is a runtime error (exit 1); running
|
/// blueprint ever reaches the registry — #331 cycle-close: this fn
|
||||||
/// outside a project (no `Aura.toml` found up from cwd, #218's gate) is also
|
/// `put_blueprint`s a fresh-FILE envelope and so must gate it exactly like
|
||||||
/// a runtime error (exit 1) — the strategy document cannot resolve against a
|
/// `register`/`run`/the other authored-file intakes, see
|
||||||
/// project store/vocabulary that doesn't exist.
|
/// `gate_authored_root_name`'s doc comment); an unknown axis name is a usage
|
||||||
|
/// error (exit 2, echoed before the archive is touched); a registry write
|
||||||
|
/// failure is a runtime error (exit 1); running outside a project (no
|
||||||
|
/// `Aura.toml` found up from cwd, #218's gate) is also a runtime error —
|
||||||
|
/// exit 1, since the strategy document cannot resolve against a project
|
||||||
|
/// store/vocabulary that doesn't exist.
|
||||||
enum AxisRegisterError {
|
enum AxisRegisterError {
|
||||||
|
BadName(String),
|
||||||
UnknownAxis(String),
|
UnknownAxis(String),
|
||||||
Registry(String),
|
Registry(String),
|
||||||
NoProject(String),
|
NoProject(String),
|
||||||
@@ -1738,6 +1780,10 @@ fn validate_and_register_axes(
|
|||||||
let wrapped_open = blueprint_axis_probe(doc, env).param_space();
|
let wrapped_open = blueprint_axis_probe(doc, env).param_space();
|
||||||
let blueprint = blueprint_from_json(doc, &|t| env.resolve(t))
|
let blueprint = blueprint_from_json(doc, &|t| env.resolve(t))
|
||||||
.expect("doc parse-validated at the dispatch boundary");
|
.expect("doc parse-validated at the dispatch boundary");
|
||||||
|
// #331 cycle-close: this fn `put_blueprint`s a fresh-FILE envelope below —
|
||||||
|
// gate the root name before it lands in the registry (shared choke point
|
||||||
|
// with every other authored-file intake).
|
||||||
|
graph_construct::gate_authored_root_name(blueprint.name()).map_err(AxisRegisterError::BadName)?;
|
||||||
let raw_bound: HashSet<String> =
|
let raw_bound: HashSet<String> =
|
||||||
blueprint.bound_param_space().into_iter().map(|b| b.name).collect();
|
blueprint.bound_param_space().into_iter().map(|b| b.name).collect();
|
||||||
for (n, _) in axes {
|
for (n, _) in axes {
|
||||||
@@ -1774,11 +1820,16 @@ fn validate_and_register_axes(
|
|||||||
|
|
||||||
/// Exit-map for a `validate_and_register_axes` failure — the stderr line and
|
/// Exit-map for a `validate_and_register_axes` failure — the stderr line and
|
||||||
/// exit code every campaign dispatcher (sweep/generalize/walkforward/mc)
|
/// exit code every campaign dispatcher (sweep/generalize/walkforward/mc)
|
||||||
/// preserves: an unknown axis is a usage error (exit 2, echoed before the
|
/// preserves: a shape-violating root name (#331) or an unknown axis is a
|
||||||
/// archive is touched); a registry write failure or a missing project (no
|
/// usage error (exit 2, echoed before the archive is touched); a registry
|
||||||
/// `Aura.toml` found up from cwd, #218's gate) is a runtime error (exit 1).
|
/// write failure or a missing project (no `Aura.toml` found up from cwd,
|
||||||
|
/// #218's gate) is a runtime error (exit 1).
|
||||||
fn exit_axis_register_error(e: AxisRegisterError) -> ! {
|
fn exit_axis_register_error(e: AxisRegisterError) -> ! {
|
||||||
match e {
|
match e {
|
||||||
|
AxisRegisterError::BadName(m) => {
|
||||||
|
eprintln!("aura: {m}");
|
||||||
|
std::process::exit(2)
|
||||||
|
}
|
||||||
AxisRegisterError::UnknownAxis(m) => {
|
AxisRegisterError::UnknownAxis(m) => {
|
||||||
eprintln!("aura: {m}");
|
eprintln!("aura: {m}");
|
||||||
std::process::exit(2)
|
std::process::exit(2)
|
||||||
@@ -1884,6 +1935,19 @@ fn dispatch_run(a: RunCmd, env: &aura_runner::project::Env) {
|
|||||||
eprintln!("aura: {path}: {msg}");
|
eprintln!("aura: {path}: {msg}");
|
||||||
std::process::exit(2);
|
std::process::exit(2);
|
||||||
});
|
});
|
||||||
|
// #331 delta re-review: this is the third freshly-authored-FILE
|
||||||
|
// intake (`register`, `introspect --content-id <FILE>`, and this
|
||||||
|
// one) that must gate the root name before it ever reaches the
|
||||||
|
// filesystem — both run arms below feed `signal.name()` straight
|
||||||
|
// into `bind_tap_plan`'s `TraceStore::begin_run`, which joins it
|
||||||
|
// unsanitized (`trace_store.rs`). Not routed through
|
||||||
|
// `composite_from_authored_text` itself (see that fn's doc
|
||||||
|
// comment on why); same `name_gate`/`name_gate_fault_prose`
|
||||||
|
// primitives, this arm's own load-error exit/prose convention.
|
||||||
|
if let Err(msg) = graph_construct::gate_authored_root_name(signal.name()) {
|
||||||
|
eprintln!("aura: {path}: {msg}");
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
// Shape dispatch (C28 phase 3): a `bias` output → the strategy path
|
// Shape dispatch (C28 phase 3): a `bias` output → the strategy path
|
||||||
// (wrap_r + R evaluation), byte-identical; else ≥1 declared tap → a
|
// (wrap_r + R evaluation), byte-identical; else ≥1 declared tap → a
|
||||||
// bare measurement run (no wrap_r); else an inert blueprint → refuse.
|
// bare measurement run (no wrap_r); else an inert blueprint → refuse.
|
||||||
@@ -1981,7 +2045,7 @@ fn dispatch_graph(a: GraphCmd, env: &aura_runner::project::Env) {
|
|||||||
eprintln!("aura: {path}: {e}");
|
eprintln!("aura: {path}: {e}");
|
||||||
std::process::exit(2);
|
std::process::exit(2);
|
||||||
});
|
});
|
||||||
let bp = graph_construct::composite_from_any(&doc, env).unwrap_or_else(|msg| {
|
let bp = graph_construct::composite_from_authored_text(&doc, env).unwrap_or_else(|msg| {
|
||||||
eprintln!("aura: {path}: {msg}");
|
eprintln!("aura: {path}: {msg}");
|
||||||
std::process::exit(2);
|
std::process::exit(2);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2449,6 +2449,53 @@ fn aura_walkforward_synthetic_blueprint_refuses_a_wrapped_form_axis_name() {
|
|||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Property (#331 cycle-close, high finding): the SYNTHETIC (no `--real`)
|
||||||
|
/// blueprint sweep reads a fresh-FILE envelope and `put_blueprint`s it into
|
||||||
|
/// the registry unconditionally (`run_blueprint_sweep`, keyed by the
|
||||||
|
/// family's shared topology hash) — before this fix that write was UNGATED,
|
||||||
|
/// so `aura sweep bad.json` could plant a shape-violating root name
|
||||||
|
/// (`"../x"`) in the store, breaking the C29 exemption's premise that "the
|
||||||
|
/// store is populated through the gated intakes" (`composite_from_any`'s
|
||||||
|
/// doc comment). A hand-edited envelope whose root name violates
|
||||||
|
/// `name_gate` must now refuse with the shared `name_gate_fault_prose`
|
||||||
|
/// wording (byte-identical to `register`/`run`/etc.) at the site's usual
|
||||||
|
/// exit code (2, matching every other blueprint-load refusal in this
|
||||||
|
/// function), and nothing must land in `runs/blueprints`.
|
||||||
|
#[test]
|
||||||
|
fn aura_sweep_synthetic_blueprint_refuses_a_hand_crafted_envelope_with_a_bad_root_name() {
|
||||||
|
let dir = temp_cwd("sweep_synthetic_bad_root_name_refusal");
|
||||||
|
let fixture_text = std::fs::read_to_string(format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")))
|
||||||
|
.expect("read r_sma.json fixture");
|
||||||
|
let bad = fixture_text.replacen("\"name\":\"sma_signal\"", "\"name\":\"../x\"", 1);
|
||||||
|
assert!(bad.contains("\"name\":\"../x\""), "the root-name replace landed: {bad}");
|
||||||
|
let file = dir.join("bad-root-name.json");
|
||||||
|
std::fs::write(&file, &bad).expect("write envelope fixture");
|
||||||
|
|
||||||
|
let out = Command::new(BIN)
|
||||||
|
.args(["sweep", file.to_str().unwrap(), "--axis", "fast.length=2,4"])
|
||||||
|
.current_dir(&dir)
|
||||||
|
.output()
|
||||||
|
.expect("spawn aura sweep (synthetic, bad root name)");
|
||||||
|
assert_eq!(
|
||||||
|
out.status.code(),
|
||||||
|
Some(2),
|
||||||
|
"a shape-violating root name must be refused, not registered: status={:?} stderr={}",
|
||||||
|
out.status,
|
||||||
|
String::from_utf8_lossy(&out.stderr)
|
||||||
|
);
|
||||||
|
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
|
||||||
|
assert!(
|
||||||
|
stderr.contains("blueprint name \"../x\" is invalid") && stderr.contains("must not contain '/' or '\\'"),
|
||||||
|
"names the offending root name with the shared gate prose: {stderr}"
|
||||||
|
);
|
||||||
|
assert!(out.stdout.is_empty(), "a refused root name must not print a member line: {out:?}");
|
||||||
|
assert!(
|
||||||
|
!dir.join("runs").exists(),
|
||||||
|
"a refused root name must leave no store — nothing may land in runs/blueprints"
|
||||||
|
);
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
|
||||||
/// Property (#247, #328 render-seam echo): every sweep-verb boundary
|
/// Property (#247, #328 render-seam echo): every sweep-verb boundary
|
||||||
/// `BindError` renders as prose in the same register as its unknown-axis
|
/// `BindError` renders as prose in the same register as its unknown-axis
|
||||||
/// sibling (`aura: axis <name>: names no param of this blueprint … — see
|
/// sibling (`aura: axis <name>: names no param of this blueprint … — see
|
||||||
@@ -5463,6 +5510,48 @@ fn aura_sweep_list_axes_rejects_malformed_blueprint() {
|
|||||||
assert!(!stderr.contains("panicked"), "must reject cleanly, not panic: {stderr}");
|
assert!(!stderr.contains("panicked"), "must reject cleanly, not panic: {stderr}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Property (#331 fieldtest finding, cycle-331-blueprint-name c331_2e): `--list-axes`
|
||||||
|
/// is a FRESH-FILE intake exactly like `register` / `introspect --content-id` /
|
||||||
|
/// `introspect --params`, but its own root-name gate was missing — a hand-edited
|
||||||
|
/// envelope with a shape-violating root name (`"../x"`) printed mangled,
|
||||||
|
/// non-bindable axis names instead of being refused. The route now shares
|
||||||
|
/// `gate_authored_root_name` + `name_gate_fault_prose` and exits 1 — the
|
||||||
|
/// register-route convention (this route never `put_blueprint`s anything, unlike
|
||||||
|
/// the exit-2 sweep-EXECUTION route pinned by
|
||||||
|
/// `aura_sweep_synthetic_blueprint_refuses_a_hand_crafted_envelope_with_a_bad_root_name`)
|
||||||
|
/// — printing no axis line at all.
|
||||||
|
#[test]
|
||||||
|
fn aura_sweep_list_axes_refuses_a_hand_crafted_envelope_with_a_bad_root_name() {
|
||||||
|
let dir = temp_cwd("sweep_list_axes_bad_root_name_refusal");
|
||||||
|
let fixture_text =
|
||||||
|
std::fs::read_to_string(format!("{}/tests/fixtures/r_sma_open.json", env!("CARGO_MANIFEST_DIR")))
|
||||||
|
.expect("read r_sma_open.json fixture");
|
||||||
|
let bad = fixture_text.replacen("\"name\":\"sma_signal\"", "\"name\":\"../x\"", 1);
|
||||||
|
assert!(bad.contains("\"name\":\"../x\""), "the root-name replace landed: {bad}");
|
||||||
|
let file = dir.join("bad-root-name.json");
|
||||||
|
std::fs::write(&file, &bad).expect("write envelope fixture");
|
||||||
|
|
||||||
|
let out = Command::new(BIN)
|
||||||
|
.args(["sweep", file.to_str().unwrap(), "--list-axes"])
|
||||||
|
.current_dir(&dir)
|
||||||
|
.output()
|
||||||
|
.expect("spawn aura sweep --list-axes (bad root name)");
|
||||||
|
assert_eq!(
|
||||||
|
out.status.code(),
|
||||||
|
Some(1),
|
||||||
|
"a shape-violating root name must be refused (register-route convention), not listed: status={:?} stderr={}",
|
||||||
|
out.status,
|
||||||
|
String::from_utf8_lossy(&out.stderr)
|
||||||
|
);
|
||||||
|
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
|
||||||
|
assert!(
|
||||||
|
stderr.contains("blueprint name \"../x\" is invalid") && stderr.contains("must not contain '/' or '\\'"),
|
||||||
|
"names the offending root name with the shared gate prose: {stderr}"
|
||||||
|
);
|
||||||
|
assert!(out.stdout.is_empty(), "a refused root name must print no axis line: {out:?}");
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
|
||||||
/// E2E (#210 c0110 fieldtest, "aura sweep <op-script> rejects the op-script
|
/// E2E (#210 c0110 fieldtest, "aura sweep <op-script> rejects the op-script
|
||||||
/// array with a raw serde error"): `aura sweep`'s blueprint slot accepts only
|
/// array with a raw serde error"): `aura sweep`'s blueprint slot accepts only
|
||||||
/// the built blueprint envelope; an un-built op-script (a JSON array) is
|
/// the built blueprint envelope; an un-built op-script (a JSON array) is
|
||||||
|
|||||||
@@ -88,6 +88,43 @@ const SIGNAL_DOC_NAME_RESTATED: &str = r#"[
|
|||||||
{"op":"expose","from":"bias.bias","as":"bias"}
|
{"op":"expose","from":"bias.bias","as":"bias"}
|
||||||
]"#;
|
]"#;
|
||||||
|
|
||||||
|
/// SIGNAL_DOC with a leading `{"op":"name",...}` (#331) — authors a render
|
||||||
|
/// name distinct from the CLI's own default seed ("graph"), otherwise
|
||||||
|
/// structurally identical to SIGNAL_DOC.
|
||||||
|
const SIGNAL_DOC_NAMED: &str = r#"[
|
||||||
|
{"op":"name","name":"ny_momentum"},
|
||||||
|
{"op":"source","role":"price","kind":"F64"},
|
||||||
|
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":2}}},
|
||||||
|
{"op":"add","type":"SMA","name":"slow","bind":{"length":{"I64":4}}},
|
||||||
|
{"op":"add","type":"Sub"},
|
||||||
|
{"op":"add","type":"Bias"},
|
||||||
|
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
|
||||||
|
{"op":"connect","from":"fast.value","to":"sub.lhs"},
|
||||||
|
{"op":"connect","from":"slow.value","to":"sub.rhs"},
|
||||||
|
{"op":"connect","from":"sub.value","to":"bias.signal"},
|
||||||
|
{"op":"expose","from":"bias.bias","as":"bias"}
|
||||||
|
]"#;
|
||||||
|
|
||||||
|
/// SIGNAL_DOC_NAME_RESTATED with an authored name (#331 spec test 7) instead
|
||||||
|
/// of the CLI's default seed: the `doc` op's text restates the *authored*
|
||||||
|
/// name ("ny_momentum") rather than the literal "graph" — the same
|
||||||
|
/// RestatesName arm of the C29 shape gate must fire against whichever name
|
||||||
|
/// the composite actually carries, not a hardcoded "graph" comparison.
|
||||||
|
const SIGNAL_DOC_NAMED_NAME_RESTATED: &str = r#"[
|
||||||
|
{"op":"name","name":"ny_momentum"},
|
||||||
|
{"op":"doc","text":"Ny Momentum"},
|
||||||
|
{"op":"source","role":"price","kind":"F64"},
|
||||||
|
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":2}}},
|
||||||
|
{"op":"add","type":"SMA","name":"slow","bind":{"length":{"I64":4}}},
|
||||||
|
{"op":"add","type":"Sub"},
|
||||||
|
{"op":"add","type":"Bias"},
|
||||||
|
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
|
||||||
|
{"op":"connect","from":"fast.value","to":"sub.lhs"},
|
||||||
|
{"op":"connect","from":"slow.value","to":"sub.rhs"},
|
||||||
|
{"op":"connect","from":"sub.value","to":"bias.signal"},
|
||||||
|
{"op":"expose","from":"bias.bias","as":"bias"}
|
||||||
|
]"#;
|
||||||
|
|
||||||
/// Every op below applies cleanly (all eager checks pass), but `sub.rhs` is
|
/// Every op below applies cleanly (all eager checks pass), but `sub.rhs` is
|
||||||
/// never wired — a 0-cover input slot only the holistic finalize gate can
|
/// never wired — a 0-cover input slot only the holistic finalize gate can
|
||||||
/// reject. The smallest document that is op-valid yet whole-document-invalid.
|
/// reject. The smallest document that is op-valid yet whole-document-invalid.
|
||||||
@@ -929,6 +966,120 @@ fn graph_register_refuses_an_op_script_doc_that_restates_the_composite_name() {
|
|||||||
assert!(err.contains("C29"), "stderr cites the contract: {err}");
|
assert!(err.contains("C29"), "stderr cites the contract: {err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// (#331 spec test 7) The C29 restates-name gate checks against the
|
||||||
|
/// *authored* name, not a hardcoded default: a `doc` op restating the name
|
||||||
|
/// set by a `name` op ("ny_momentum") refuses at register exactly like the
|
||||||
|
/// default-name case above (`graph_register_refuses_an_op_script_doc_that_
|
||||||
|
/// restates_the_composite_name`, which must stay green byte-identical).
|
||||||
|
#[test]
|
||||||
|
fn graph_register_refuses_a_doc_that_restates_an_authored_name() {
|
||||||
|
let dir = temp_cwd("register-op-script-restated-authored-name-doc");
|
||||||
|
let restated_script = dir.join("restated-authored-name-doc-script.json");
|
||||||
|
std::fs::write(&restated_script, SIGNAL_DOC_NAMED_NAME_RESTATED)
|
||||||
|
.expect("write op-script fixture");
|
||||||
|
let (out, err, code) = run_in(&dir, &["graph", "register", restated_script.to_str().unwrap()]);
|
||||||
|
assert_eq!(code, Some(1), "authored-name-restating doc refuses: {out} {err}");
|
||||||
|
assert!(err.contains("merely restates"), "stderr names the rule: {err}");
|
||||||
|
assert!(err.contains("C29"), "stderr cites the contract: {err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (#331 spec test 6) `graph build` on an ops doc with a `name` op emits
|
||||||
|
/// blueprint JSON carrying the authored name, not the CLI's own seeded
|
||||||
|
/// default (`"graph"`, `replay("graph", ...)` in `composite_from_str`).
|
||||||
|
#[test]
|
||||||
|
fn graph_build_emits_the_authored_name_from_a_name_op() {
|
||||||
|
let (stdout, stderr, ok) = run(&["graph", "build"], SIGNAL_DOC_NAMED);
|
||||||
|
assert!(ok, "build succeeds: {stderr}");
|
||||||
|
assert!(stdout.contains("\"name\":\"ny_momentum\""), "authored name carries into the blueprint: {stdout}");
|
||||||
|
assert!(!stdout.contains("\"name\":\"graph\""), "the default seed name must not leak in: {stdout}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (#331 spec test 8) Refusal prose for a duplicate `name` op and a
|
||||||
|
/// shape-violating name is op-indexed (`op N (name): ...`) — the same
|
||||||
|
/// by-identifier labeling every other op fault gets.
|
||||||
|
#[test]
|
||||||
|
fn graph_build_name_op_refusals_are_op_indexed() {
|
||||||
|
let twice_named = r#"[
|
||||||
|
{"op":"source","role":"price","kind":"F64"},
|
||||||
|
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":2}}},
|
||||||
|
{"op":"name","name":"a"},
|
||||||
|
{"op":"name","name":"b"},
|
||||||
|
{"op":"feed","role":"price","into":["fast.series"]},
|
||||||
|
{"op":"expose","from":"fast.value","as":"out"}
|
||||||
|
]"#;
|
||||||
|
let (_out, stderr, ok) = run(&["graph", "build"], twice_named);
|
||||||
|
assert!(!ok, "a second name op refuses");
|
||||||
|
assert!(stderr.contains("op 3 (name):"), "op-indexed at the second name op: {stderr}");
|
||||||
|
|
||||||
|
let slash_named = r#"[{"op":"name","name":"a/b"}]"#;
|
||||||
|
let (_out2, stderr2, ok2) = run(&["graph", "build"], slash_named);
|
||||||
|
assert!(!ok2, "a shape-violating name refuses");
|
||||||
|
assert!(stderr2.contains("op 0 (name):"), "op-indexed at the offending op: {stderr2}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (#331 spec test 9, at the binary seam — mirrors
|
||||||
|
/// `graph_introspect_identity_id_bridges_renamed_op_scripts`'s pattern for
|
||||||
|
/// the debug-name field): a named and an unnamed build of the SAME
|
||||||
|
/// structural script share the identity id (the name is stripped as a
|
||||||
|
/// debug symbol before identity hashing, C23) and differ in the content id
|
||||||
|
/// (the name is part of the serialized bytes).
|
||||||
|
#[test]
|
||||||
|
fn graph_named_and_unnamed_twins_share_identity_id_and_differ_content_id() {
|
||||||
|
let (ia, _e, ok) = run(&["graph", "introspect", "--identity-id"], SIGNAL_DOC);
|
||||||
|
assert!(ok, "exit success");
|
||||||
|
let (ib, _e2, ok2) = run(&["graph", "introspect", "--identity-id"], SIGNAL_DOC_NAMED);
|
||||||
|
assert!(ok2, "exit success");
|
||||||
|
assert_eq!(ia.trim(), ib.trim(), "the name is stripped before identity hashing (C23)");
|
||||||
|
|
||||||
|
let (ca, _e3, ok3) = run(&["graph", "introspect", "--content-id"], SIGNAL_DOC);
|
||||||
|
let (cb, _e4, ok4) = run(&["graph", "introspect", "--content-id"], SIGNAL_DOC_NAMED);
|
||||||
|
assert!(ok3 && ok4, "exit success");
|
||||||
|
assert_ne!(ca.trim(), cb.trim(), "the name is part of the serialized bytes -> different content id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (#331 spec test 10, the skeptic's bypass route): the op intake
|
||||||
|
/// (`GraphSession::set_name`) is not the name's only data-borne birth
|
||||||
|
/// route — a hand-edited blueprint ENVELOPE (not an op-script) can carry a
|
||||||
|
/// shape-violating root name directly. `aura graph register` on such a file
|
||||||
|
/// is refused naming the offending name (exit 1, the refusal convention
|
||||||
|
/// every sibling test in this module asserts), not silently stored under a
|
||||||
|
/// name that would write `traces/../x/` at run time. Shipped envelope
|
||||||
|
/// fixtures with plain root names (`doc_blueprint.json`'s "sig",
|
||||||
|
/// `nested_doc_blueprint.json`'s "root", every op-script's default "graph")
|
||||||
|
/// stay accepted — the surrounding suite is that regression gate.
|
||||||
|
///
|
||||||
|
/// Spec test 10 also names `build` as a gated envelope-intake route
|
||||||
|
/// alongside `register`. The `graph build` SUBCOMMAND takes only a stdin
|
||||||
|
/// op-script (`Vec<OpDoc>`, never a JSON object) — it cannot ingest this
|
||||||
|
/// envelope fixture at all (a shape mismatch fires before any name is ever
|
||||||
|
/// read). The file-consuming envelope-intake sibling `build` route is
|
||||||
|
/// `graph introspect --content-id <FILE>` (its own doc comment: "one
|
||||||
|
/// build, then each requested id…") — the other call site this iteration
|
||||||
|
/// moved onto the same `composite_from_authored_text` choke point as
|
||||||
|
/// `register`, so it is the leg exercised here, sharing the identical
|
||||||
|
/// `name_gate_fault_prose` text.
|
||||||
|
#[test]
|
||||||
|
fn graph_build_and_register_refuse_a_hand_crafted_envelope_with_a_bad_root_name() {
|
||||||
|
let dir = temp_cwd("register-bad-root-name");
|
||||||
|
let (envelope_bytes, _e, built) = run(&["graph", "build"], SIGNAL_DOC_DESCRIBED);
|
||||||
|
assert!(built, "graph build produces the envelope");
|
||||||
|
let bad = envelope_bytes.replacen("\"name\":\"graph\"", "\"name\":\"../x\"", 1);
|
||||||
|
assert!(bad.contains("\"name\":\"../x\""), "the root-name replace landed: {bad}");
|
||||||
|
let file = dir.join("bad-root-name.json");
|
||||||
|
std::fs::write(&file, &bad).expect("write envelope fixture");
|
||||||
|
|
||||||
|
let (out, err, code) = run_in(&dir, &["graph", "register", file.to_str().unwrap()]);
|
||||||
|
assert_eq!(code, Some(1), "a shape-violating root name refuses (not a panic): {out} {err}");
|
||||||
|
assert!(err.contains("../x"), "names the offending root name: {err}");
|
||||||
|
|
||||||
|
// The other file-consuming envelope intake (same fixture, same choke
|
||||||
|
// point) refuses identically.
|
||||||
|
let (out2, err2, code2) =
|
||||||
|
run_in(&dir, &["graph", "introspect", "--content-id", file.to_str().unwrap()]);
|
||||||
|
assert_eq!(code2, Some(1), "the sibling envelope intake refuses too: {out2} {err2}");
|
||||||
|
assert_eq!(err2, err, "both file-consuming envelope intakes share the identical fault prose");
|
||||||
|
}
|
||||||
|
|
||||||
/// Property (#202, the same on-ramp seam at the sibling verb): `aura graph
|
/// Property (#202, the same on-ramp seam at the sibling verb): `aura graph
|
||||||
/// introspect --params` accepts an op-script exactly as it accepts a blueprint
|
/// introspect --params` accepts an op-script exactly as it accepts a blueprint
|
||||||
/// envelope — the raw axis namespace it prints for the op-script equals the one it
|
/// envelope — the raw axis namespace it prints for the op-script equals the one it
|
||||||
@@ -954,6 +1105,35 @@ fn graph_params_accepts_an_op_script_matching_its_envelope() {
|
|||||||
assert_eq!(op_out, env_out, "op-script and envelope agree on the raw axis namespace");
|
assert_eq!(op_out, env_out, "op-script and envelope agree on the raw axis namespace");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// (#331 review finding, the C29 exemption's ratifying pin): the root-name
|
||||||
|
/// shape gate lives at the CLI's file-consuming envelope intakes (`register`,
|
||||||
|
/// `introspect --content-id <FILE>`) — never at STORE read-back. A blueprint
|
||||||
|
/// with a shape-violating root name registered DIRECTLY through the
|
||||||
|
/// `aura-registry` store API (bypassing the CLI's gate entirely, the way a
|
||||||
|
/// pre-#331 artifact would already sit in a project's store) must still be
|
||||||
|
/// introspectable: `introspect --params <ID>` reads the store, not a file, so
|
||||||
|
/// it must never re-punish an artifact the store already accepted — C29,
|
||||||
|
/// "registered artifacts are never retroactively invalidated".
|
||||||
|
#[test]
|
||||||
|
fn graph_introspect_params_accepts_a_pre_331_store_artifact_with_a_bad_root_name() {
|
||||||
|
let dir = temp_cwd("introspect-store-readback-bad-root-name");
|
||||||
|
let (envelope_bytes, _e, built) = run(&["graph", "build"], SIGNAL_DOC_DESCRIBED);
|
||||||
|
assert!(built, "graph build produces the envelope");
|
||||||
|
let bad = envelope_bytes.replacen("\"name\":\"graph\"", "\"name\":\"../x\"", 1);
|
||||||
|
assert!(bad.contains("\"name\":\"../x\""), "the root-name replace landed: {bad}");
|
||||||
|
|
||||||
|
// Bypass the CLI's gate entirely: the registry's own store API is the
|
||||||
|
// simulated pre-#331 write path.
|
||||||
|
let id = aura_research::content_id_of(&bad);
|
||||||
|
let registry = aura_registry::Registry::open(dir.join("runs/runs.jsonl"));
|
||||||
|
registry
|
||||||
|
.put_blueprint(&id, &bad)
|
||||||
|
.expect("the registry's own store API has no root-name gate to bypass");
|
||||||
|
|
||||||
|
let (out, err, code) = run_in(&dir, &["graph", "introspect", "--params", &id]);
|
||||||
|
assert_eq!(code, Some(0), "store read-back stays ungated (C29): {out} {err}");
|
||||||
|
}
|
||||||
|
|
||||||
/// Property (#226, the render positional closes the #202 on-ramp family):
|
/// Property (#226, the render positional closes the #202 on-ramp family):
|
||||||
/// `aura graph <op-script.json>` (the render positional) accepts an op-script
|
/// `aura graph <op-script.json>` (the render positional) accepts an op-script
|
||||||
/// document (a JSON array) exactly as it accepts a #155 blueprint envelope (a
|
/// document (a JSON array) exactly as it accepts a #155 blueprint envelope (a
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ fn sugar_verbs_name_their_document_shape() {
|
|||||||
|
|
||||||
/// #323: `graph build --help` carries the op-list reference — the op kinds
|
/// #323: `graph build --help` carries the op-list reference — the op kinds
|
||||||
/// and fields are learnable from the binary, not only from serde refusals.
|
/// and fields are learnable from the binary, not only from serde refusals.
|
||||||
/// #317: the `use` op joins the roster (nine -> ten).
|
/// #317: the `use` op joins the roster (nine -> ten). #331: the `name` op
|
||||||
|
/// joins the roster (ten -> eleven).
|
||||||
#[test]
|
#[test]
|
||||||
fn graph_build_help_carries_the_op_reference() {
|
fn graph_build_help_carries_the_op_reference() {
|
||||||
let (out, err, ok) = run(&["graph", "build", "--help"]);
|
let (out, err, ok) = run(&["graph", "build", "--help"]);
|
||||||
@@ -74,6 +75,7 @@ fn graph_build_help_carries_the_op_reference() {
|
|||||||
r#"{"op":"gang""#,
|
r#"{"op":"gang""#,
|
||||||
r#"{"op":"doc""#,
|
r#"{"op":"doc""#,
|
||||||
r#"{"op":"use""#,
|
r#"{"op":"use""#,
|
||||||
|
r#"{"op":"name""#,
|
||||||
] {
|
] {
|
||||||
assert!(out.contains(op), "op reference carries {op}: {out}");
|
assert!(out.contains(op), "op reference carries {op}: {out}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,3 +81,60 @@ fn measurement_run_honours_the_tap_selector() {
|
|||||||
let expected = (1.0097_f64 + 1.0092) / 2.0;
|
let expected = (1.0097_f64 + 1.0092) / 2.0;
|
||||||
assert!((got - expected).abs() < 1e-9, "last row: got {got}, expected {expected}");
|
assert!((got - expected).abs() < 1e-9, "last row: got {got}, expected {expected}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// #331 delta re-review, the third authored-file intake route: `dispatch_run`
|
||||||
|
/// loads a FRESH FILE via `blueprint_from_json` and, on this measurement arm,
|
||||||
|
/// feeds `signal.name()` straight into `bind_tap_plan` -> `TraceStore::begin_run`
|
||||||
|
/// (`trace_store.rs` joins the name unsanitized) — a hand-crafted envelope with
|
||||||
|
/// `"name":"../x"` used to reach `--tap fast_tap=record` and escape
|
||||||
|
/// `runs/traces/<name>/` for `runs/x/` (one level up, since `traces/../x`
|
||||||
|
/// normalizes there) instead of refusing at the same root-name choke point
|
||||||
|
/// `register`/`introspect --content-id <FILE>` already gate through. This pins
|
||||||
|
/// the closed route: exit nonzero (dispatch_run's own bad-input-file
|
||||||
|
/// convention, exit 2), the shared `name_gate_fault_prose` wording, and — the
|
||||||
|
/// property that actually matters — the escaped directory is never created.
|
||||||
|
#[test]
|
||||||
|
fn run_refuses_a_hand_crafted_envelope_with_a_bad_root_name_before_any_trace_write() {
|
||||||
|
let cwd = temp_cwd("bad-root-name");
|
||||||
|
let mut v: serde_json::Value =
|
||||||
|
serde_json::from_str(&measurement_blueprint_json()).expect("parse measurement blueprint");
|
||||||
|
v["blueprint"]["name"] = serde_json::json!("../x");
|
||||||
|
let bad = serde_json::to_string(&v).expect("re-serialize with a shape-violating root name");
|
||||||
|
let bp_path = cwd.join("bad-root-name.json");
|
||||||
|
std::fs::write(&bp_path, &bad).expect("write bad-root-name blueprint");
|
||||||
|
|
||||||
|
let out = Command::new(BIN)
|
||||||
|
.args(["run", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=record"])
|
||||||
|
.current_dir(&cwd)
|
||||||
|
.output()
|
||||||
|
.expect("spawn aura run");
|
||||||
|
assert_eq!(
|
||||||
|
out.status.code(),
|
||||||
|
Some(2),
|
||||||
|
"a shape-violating root name refuses at dispatch_run's own bad-file exit code: {:?} stderr={}",
|
||||||
|
out.status,
|
||||||
|
String::from_utf8_lossy(&out.stderr)
|
||||||
|
);
|
||||||
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||||
|
assert!(
|
||||||
|
stderr.contains(r#"blueprint name "../x" is invalid"#),
|
||||||
|
"the shared name_gate_fault_prose wording names the offending root name: {stderr}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
out.stdout.is_empty(),
|
||||||
|
"a refused run must not print a report: {:?}",
|
||||||
|
String::from_utf8_lossy(&out.stdout)
|
||||||
|
);
|
||||||
|
|
||||||
|
// The property that actually matters: the escaped write target (one level
|
||||||
|
// up from `traces/`, since `trace_store.rs` joins the name unsanitized)
|
||||||
|
// was never created — the gate fires before `begin_run` ever touches disk.
|
||||||
|
assert!(
|
||||||
|
!cwd.join("runs/x").exists(),
|
||||||
|
"the escaped trace directory must never be created"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!cwd.join("runs/traces").exists(),
|
||||||
|
"no trace directory of any shape is written on a refused run"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -195,6 +195,38 @@ pub struct GangMember {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deterministic shape gate for authored composite render names (#331) —
|
||||||
|
/// shape only, never content judgement (C29 discipline): non-empty, no path
|
||||||
|
/// separator (`/` or `\`), and not the special segments `.` or `..`. The rule
|
||||||
|
/// exists because the name keys a trace directory on disk unsanitized
|
||||||
|
/// (`traces/<name>/`, `crates/aura-registry/src/trace_store.rs`). Applied at
|
||||||
|
/// both data-borne birth routes for a name (the skeptic's two-seam finding):
|
||||||
|
/// the `Op::Name` op intake (`construction.rs`) and the CLI's
|
||||||
|
/// blueprint-envelope root-name intake — never at store read-back (C29:
|
||||||
|
/// registered artifacts are never retroactively invalidated), and never on
|
||||||
|
/// the Rust builder API (`GraphBuilder::new`, role-2 native authoring).
|
||||||
|
pub fn name_gate(name: &str) -> Result<(), NameGateFault> {
|
||||||
|
if name.is_empty() {
|
||||||
|
return Err(NameGateFault::Empty);
|
||||||
|
}
|
||||||
|
if name.contains('/') || name.contains('\\') {
|
||||||
|
return Err(NameGateFault::ContainsSeparator);
|
||||||
|
}
|
||||||
|
if name == "." || name == ".." {
|
||||||
|
return Err(NameGateFault::DotSegment);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The [`name_gate`] shape violation — WHY the name was refused; the caller
|
||||||
|
/// already holds the offending name (WHAT) to build a message from.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum NameGateFault {
|
||||||
|
Empty,
|
||||||
|
ContainsSeparator,
|
||||||
|
DotSegment,
|
||||||
|
}
|
||||||
|
|
||||||
/// A reusable sub-graph fragment compiled away by inlining (C9/C23). It is **not**
|
/// A reusable sub-graph fragment compiled away by inlining (C9/C23). It is **not**
|
||||||
/// a [`Node`]: it is never `eval`'d. It holds interior items (local indices),
|
/// a [`Node`]: it is never `eval`'d. It holds interior items (local indices),
|
||||||
/// interior edges (local indices), input roles (role `r` fans into the interior
|
/// interior edges (local indices), input roles (role `r` fans into the interior
|
||||||
@@ -1519,6 +1551,19 @@ mod tests {
|
|||||||
use aura_strategy::Bias;
|
use aura_strategy::Bias;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
/// `name_gate` (#331) unit table: an ordinary name passes; an empty
|
||||||
|
/// name, one containing either path separator, and the two dot segments
|
||||||
|
/// each fault with their own fault variant.
|
||||||
|
#[test]
|
||||||
|
fn name_gate_accepts_ordinary_names_and_refuses_shape_violations() {
|
||||||
|
assert_eq!(name_gate("ny_momentum"), Ok(()));
|
||||||
|
assert_eq!(name_gate(""), Err(NameGateFault::Empty));
|
||||||
|
assert_eq!(name_gate("a/b"), Err(NameGateFault::ContainsSeparator));
|
||||||
|
assert_eq!(name_gate("a\\b"), Err(NameGateFault::ContainsSeparator));
|
||||||
|
assert_eq!(name_gate("."), Err(NameGateFault::DotSegment));
|
||||||
|
assert_eq!(name_gate(".."), Err(NameGateFault::DotSegment));
|
||||||
|
}
|
||||||
|
|
||||||
/// One knob fanning into two sibling open params passes the gate; the
|
/// One knob fanning into two sibling open params passes the gate; the
|
||||||
/// value carries the gang table.
|
/// value carries the gang table.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ use std::collections::{HashMap, HashSet};
|
|||||||
use aura_core::{ArgOpError, BindOpError, NodeSchema, PrimitiveBuilder, Scalar, ScalarKind};
|
use aura_core::{ArgOpError, BindOpError, NodeSchema, PrimitiveBuilder, Scalar, ScalarKind};
|
||||||
|
|
||||||
use crate::blueprint::{
|
use crate::blueprint::{
|
||||||
check_param_namespace_injective, edge_kind_check, validate_wiring, BlueprintNode, Composite,
|
check_param_namespace_injective, edge_kind_check, name_gate, validate_wiring, BlueprintNode,
|
||||||
Gang, GangMember, OutField, Role, Tap, TapWire,
|
Composite, Gang, GangMember, NameGateFault, OutField, Role, Tap, TapWire,
|
||||||
};
|
};
|
||||||
use crate::builder::{resolve_input_slot, resolve_output_field, PortResolveError};
|
use crate::builder::{resolve_input_slot, resolve_output_field, PortResolveError};
|
||||||
use crate::harness::{Edge, Target};
|
use crate::harness::{Edge, Target};
|
||||||
@@ -64,6 +64,8 @@ pub enum Op {
|
|||||||
/// `bind` is applied path-qualified (e.g. `"sma.length"`), after the
|
/// `bind` is applied path-qualified (e.g. `"sma.length"`), after the
|
||||||
/// splice.
|
/// splice.
|
||||||
Use { ref_id: String, name: Option<String>, bind: Vec<(String, Scalar)> },
|
Use { ref_id: String, name: Option<String>, bind: Vec<(String, Scalar)> },
|
||||||
|
/// Set the composite's render name (#331). At most once per script.
|
||||||
|
Name { name: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A per-op construction fault, by-identifier so the cause names the op.
|
/// A per-op construction fault, by-identifier so the cause names the op.
|
||||||
@@ -124,6 +126,13 @@ pub enum OpError {
|
|||||||
/// resolves and fetches before replay, refusing there on a miss), but
|
/// resolves and fetches before replay, refusing there on a miss), but
|
||||||
/// total: a bare `replay`/`GraphSession` caller can still hit it.
|
/// total: a bare `replay`/`GraphSession` caller can still hit it.
|
||||||
UnknownSubgraph { ref_id: String },
|
UnknownSubgraph { ref_id: String },
|
||||||
|
/// A second `name` op — the render name is declared at most once (mirrors
|
||||||
|
/// `DuplicateDoc`).
|
||||||
|
DuplicateName,
|
||||||
|
/// A `name` op's `name` (or, at the CLI's blueprint-envelope intake, an
|
||||||
|
/// envelope's root name) failed the deterministic `name_gate` shape check
|
||||||
|
/// (#331). Carries the offending name and the shape fault detail.
|
||||||
|
BadName { name: String, fault: NameGateFault },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A per-op-fallible blueprint accumulator. Holds the same interior data a
|
/// A per-op-fallible blueprint accumulator. Holds the same interior data a
|
||||||
@@ -153,6 +162,11 @@ pub struct GraphSession<'v> {
|
|||||||
coverage: HashMap<(usize, usize), usize>,
|
coverage: HashMap<(usize, usize), usize>,
|
||||||
gangs: Vec<Gang>,
|
gangs: Vec<Gang>,
|
||||||
doc: Option<String>,
|
doc: Option<String>,
|
||||||
|
/// At-most-once guard for `Op::Name` (#331), mirroring `doc`'s own
|
||||||
|
/// uniqueness posture but tracked separately (`name` itself is always
|
||||||
|
/// populated, seeded from `::new`'s `name` argument, so it cannot double
|
||||||
|
/// as its own "was it authored" flag).
|
||||||
|
name_authored: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'v> GraphSession<'v> {
|
impl<'v> GraphSession<'v> {
|
||||||
@@ -182,6 +196,7 @@ impl<'v> GraphSession<'v> {
|
|||||||
coverage: HashMap::new(),
|
coverage: HashMap::new(),
|
||||||
gangs: Vec::new(),
|
gangs: Vec::new(),
|
||||||
doc: None,
|
doc: None,
|
||||||
|
name_authored: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,9 +220,29 @@ impl<'v> GraphSession<'v> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Op::Use { ref_id, name, bind } => self.use_subgraph(ref_id, name, bind),
|
Op::Use { ref_id, name, bind } => self.use_subgraph(ref_id, name, bind),
|
||||||
|
Op::Name { name } => self.set_name(name),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `Op::Name` (#331): set the composite's render name — the op-script's
|
||||||
|
/// data-borne twin of `replay`'s seeded default name. Guarded by (a)
|
||||||
|
/// at-most-once (mirrors the `doc` op's uniqueness guard just above) and
|
||||||
|
/// (b) the shared deterministic shape gate (`name_gate`) — the SAME gate
|
||||||
|
/// the CLI's blueprint-envelope intake applies to a hand-edited
|
||||||
|
/// envelope's root name (the other data-borne birth route, skeptic
|
||||||
|
/// finding minuted on #331).
|
||||||
|
fn set_name(&mut self, name: String) -> Result<(), OpError> {
|
||||||
|
if self.name_authored {
|
||||||
|
return Err(OpError::DuplicateName);
|
||||||
|
}
|
||||||
|
if let Err(fault) = name_gate(&name) {
|
||||||
|
return Err(OpError::BadName { name, fault });
|
||||||
|
}
|
||||||
|
self.name = name;
|
||||||
|
self.name_authored = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// `Op::Use` (#317): fetch a registered blueprint through the injected
|
/// `Op::Use` (#317): fetch a registered blueprint through the injected
|
||||||
/// `subgraph` resolver, rename it to the instance identifier, apply its
|
/// `subgraph` resolver, rename it to the instance identifier, apply its
|
||||||
/// path-qualified `bind` entries, and push it as an ordinary
|
/// path-qualified `bind` entries, and push it as an ordinary
|
||||||
@@ -1002,6 +1037,72 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Op::Name (#331) -----------------------------------------------
|
||||||
|
|
||||||
|
/// A `name` op sets the finished composite's render name — the property
|
||||||
|
/// the whole op exists for.
|
||||||
|
#[test]
|
||||||
|
fn name_op_sets_the_finished_composite_name() {
|
||||||
|
let mut s = scaffold();
|
||||||
|
s.apply(Op::Name { name: "x".into() }).unwrap();
|
||||||
|
s.apply(Op::Feed { role: "price".into(), into: vec!["fast.series".into(), "slow.series".into()] })
|
||||||
|
.unwrap();
|
||||||
|
s.apply(Op::Connect { from: "fast.value".into(), to: "sub.lhs".into() }).unwrap();
|
||||||
|
s.apply(Op::Connect { from: "slow.value".into(), to: "sub.rhs".into() }).unwrap();
|
||||||
|
s.apply(Op::Expose { from: "sub.value".into(), as_name: "out".into() }).unwrap();
|
||||||
|
let c = s.finish().expect("finishes");
|
||||||
|
assert_eq!(c.name(), "x");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Byte-compat (#331 AC1): a script that never authors a `name` op keeps
|
||||||
|
/// the seed name `replay`/`GraphSession::new` was given — the CLI's own
|
||||||
|
/// seed stays `"graph"`, unchanged by this op's addition.
|
||||||
|
#[test]
|
||||||
|
fn no_name_op_keeps_the_seeded_replay_name() {
|
||||||
|
let ops = vec![
|
||||||
|
Op::Source { role: "price".into(), kind: ScalarKind::F64 },
|
||||||
|
Op::Add { type_id: "SMA".into(), as_name: Some("a".into()), args: vec![], bind: vec![] },
|
||||||
|
Op::Feed { role: "price".into(), into: vec!["a.series".into()] },
|
||||||
|
Op::Expose { from: "a.value".into(), as_name: "out".into() },
|
||||||
|
];
|
||||||
|
let c = super::replay("graph", ops, &std_vocabulary, &|_: &str| None).expect("replays");
|
||||||
|
assert_eq!(c.name(), "graph");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A second `name` op is a fault, not a silent last-wins overwrite —
|
||||||
|
/// mirrors the `doc` op's own at-most-once guard.
|
||||||
|
#[test]
|
||||||
|
fn duplicate_name_op_is_refused() {
|
||||||
|
let mut s = session("g");
|
||||||
|
s.apply(Op::Name { name: "x".into() }).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
s.apply(Op::Name { name: "y".into() }),
|
||||||
|
Err(OpError::DuplicateName)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shape violations (#331): empty, containing a path separator, and the
|
||||||
|
/// dot segments all fault at the op via the shared `name_gate`.
|
||||||
|
#[test]
|
||||||
|
fn name_op_shape_violations_are_refused() {
|
||||||
|
use crate::blueprint::NameGateFault;
|
||||||
|
let mut s = session("g");
|
||||||
|
assert_eq!(
|
||||||
|
s.apply(Op::Name { name: "".into() }),
|
||||||
|
Err(OpError::BadName { name: "".into(), fault: NameGateFault::Empty })
|
||||||
|
);
|
||||||
|
let mut s = session("g");
|
||||||
|
assert_eq!(
|
||||||
|
s.apply(Op::Name { name: "a/b".into() }),
|
||||||
|
Err(OpError::BadName { name: "a/b".into(), fault: NameGateFault::ContainsSeparator })
|
||||||
|
);
|
||||||
|
let mut s = session("g");
|
||||||
|
assert_eq!(
|
||||||
|
s.apply(Op::Name { name: "..".into() }),
|
||||||
|
Err(OpError::BadName { name: "..".into(), fault: NameGateFault::DotSegment })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn finish_reports_incomplete_on_unwired_slot() {
|
fn finish_reports_incomplete_on_unwired_slot() {
|
||||||
// a fully-named scaffold missing the sub.rhs connection -> holistic 0-arm
|
// a fully-named scaffold missing the sub.rhs connection -> holistic 0-arm
|
||||||
@@ -1501,6 +1602,30 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// #331 ratifying pin (spec test 5): `use_subgraph`'s instance-identifier
|
||||||
|
/// default (`name: None` -> the fetched composite's own `name()`,
|
||||||
|
/// `construction.rs:221-230` — code-inspected, previously untested since
|
||||||
|
/// every other test in this section passes an explicit instance name)
|
||||||
|
/// actually reaches the session's identifier namespace: `use_fixture()`'s
|
||||||
|
/// own name is "fixture", so with no instance name the spliced ports
|
||||||
|
/// resolve under "fixture.*", not some other placeholder.
|
||||||
|
#[test]
|
||||||
|
fn use_op_with_no_name_defaults_instance_identifier_to_the_fetched_composites_name() {
|
||||||
|
let subgraph = |_: &str| Some(use_fixture());
|
||||||
|
let mut s = GraphSession::new("g", &std_vocabulary, &subgraph);
|
||||||
|
s.apply(Op::Source { role: "price".into(), kind: ScalarKind::F64 }).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
s.apply(Op::Use { ref_id: "fixture-id".into(), name: None, bind: vec![] }),
|
||||||
|
Ok(())
|
||||||
|
);
|
||||||
|
s.apply(Op::Feed { role: "price".into(), into: vec!["fixture.price".into()] }).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
s.apply(Op::Expose { from: "fixture.out".into(), as_name: "bias".into() }),
|
||||||
|
Ok(()),
|
||||||
|
"the spliced instance resolves under \"fixture\" — the fetched composite's own name"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// A post-splice `bind` path with no matching open param anywhere in the
|
/// A post-splice `bind` path with no matching open param anywhere in the
|
||||||
/// instance faults through the existing bind-fault shape (`BadParam`),
|
/// instance faults through the existing bind-fault shape (`BadParam`),
|
||||||
/// naming the instance and the qualified within-instance path (#317).
|
/// naming the instance and the qualified within-instance path (#317).
|
||||||
|
|||||||
@@ -52,8 +52,9 @@ mod sweep;
|
|||||||
mod walkforward;
|
mod walkforward;
|
||||||
|
|
||||||
pub use blueprint::{
|
pub use blueprint::{
|
||||||
BindError, Binder, BlueprintNode, BoundSpec, CompileError, Composite, Gang, GangFault,
|
name_gate, BindError, Binder, BlueprintNode, BoundSpec, CompileError, Composite, Gang,
|
||||||
GangMember, OutField, RandomBinder, ReopenError, Role, SweepBinder, Tap, TapWire,
|
GangFault, GangMember, NameGateFault, OutField, RandomBinder, ReopenError, Role, SweepBinder,
|
||||||
|
Tap, TapWire,
|
||||||
};
|
};
|
||||||
pub use blueprint_serde::{
|
pub use blueprint_serde::{
|
||||||
blueprint_from_json, blueprint_identity_json, blueprint_to_json, ArgData, BlueprintDoc,
|
blueprint_from_json, blueprint_identity_json, blueprint_to_json, ArgData, BlueprintDoc,
|
||||||
|
|||||||
+10
-1
@@ -227,10 +227,11 @@ $ aura graph build < smacross.json > blueprint.json
|
|||||||
Nodes are referenced by an **identifier** (given by `add`, see below); ports
|
Nodes are referenced by an **identifier** (given by `add`, see below); ports
|
||||||
are dotted `<identifier>.<port>` on both sides of a wire.
|
are dotted `<identifier>.<port>` on both sides of a wire.
|
||||||
|
|
||||||
### The ten ops
|
### The eleven ops
|
||||||
|
|
||||||
| op | JSON shape | does |
|
| op | JSON shape | does |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| `name` | `{"op":"name","name":<str>}` | set the composite's **render name**, script-level and at-most-once (a second `name` op refuses: `a script names its blueprint at most once`). The name must be a single path segment (non-empty, no `/`, `\`, `.` or `..`) since it keys a trace directory (`traces/<name>/`) at run time. Omitting the op keeps the CLI's own default, `"graph"`. |
|
||||||
| `doc` | `{"op":"doc","text":<str>}` | declare the composite's one-line meaning (C29) — the op-script twin of the Rust builder's `.doc(...)`. Required before `aura graph register` accepts the product (the store refuses a doc-less composite); at most one per script (a second refuses: `a doc op may appear at most once`). The text is gated: empty or merely restating the composite's name refuses at register. |
|
| `doc` | `{"op":"doc","text":<str>}` | declare the composite's one-line meaning (C29) — the op-script twin of the Rust builder's `.doc(...)`. Required before `aura graph register` accepts the product (the store refuses a doc-less composite); at most one per script (a second refuses: `a doc op may appear at most once`). The text is gated: empty or merely restating the composite's name refuses at register. |
|
||||||
| `source` | `{"op":"source","role":<str>,"kind":<ScalarKind>}` | reserve a bound root **source** role of `kind` — a real input the harness feeds (e.g. `"price"`). |
|
| `source` | `{"op":"source","role":<str>,"kind":<ScalarKind>}` | reserve a bound root **source** role of `kind` — a real input the harness feeds (e.g. `"price"`). |
|
||||||
| `input` | `{"op":"input","role":<str>}` | reserve an open root **input** role (kind inferred from the slots it feeds) — the formal parameter of an **open pattern**, a fragment meant to be wired by an *enclosing* graph. An open pattern builds and registers like any blueprint. Running it standalone is governed by the ordinary run gates: the harness binds input roles to archive columns **by name** (C26), so a pattern whose roles match the data runs as-is; what refuses, by name, is a role the harness cannot bind — or the run surface's other gates (a signal without a bias output or tap; free knobs). |
|
| `input` | `{"op":"input","role":<str>}` | reserve an open root **input** role (kind inferred from the slots it feeds) — the formal parameter of an **open pattern**, a fragment meant to be wired by an *enclosing* graph. An open pattern builds and registers like any blueprint. Running it standalone is governed by the ordinary run gates: the harness binds input roles to archive columns **by name** (C26), so a pattern whose roles match the data runs as-is; what refuses, by name, is a role the harness cannot bind — or the run surface's other gates (a signal without a bias output or tap; free knobs). |
|
||||||
@@ -242,6 +243,14 @@ are dotted `<identifier>.<port>` on both sides of a wire.
|
|||||||
| `tap` | `{"op":"tap","from":<port>,"as":<str>}` | declare a **measurement tap** on an interior output field under the name `as` — the output-side twin of `expose` (a recorded observation point, not a boundary output; a `Composite.taps` entry, C27). A single `aura run` constructs a recorder at each declared tap and persists its per-cycle series as a `ColumnarTrace`; a sweep leaves it inert. Tap names are their own namespace and must be unique (a second `tap` under one name refuses: `duplicate tap name`). |
|
| `tap` | `{"op":"tap","from":<port>,"as":<str>}` | declare a **measurement tap** on an interior output field under the name `as` — the output-side twin of `expose` (a recorded observation point, not a boundary output; a `Composite.taps` entry, C27). A single `aura run` constructs a recorder at each declared tap and persists its per-cycle series as a `ColumnarTrace`; a sweep leaves it inert. Tap names are their own namespace and must be unique (a second `tap` under one name refuses: `duplicate tap name`). |
|
||||||
| `gang` | `{"op":"gang","as":"channel_length","into":["channel_hi.length","channel_lo.length"]}` | Fuse two or more sibling params into ONE public knob: the member addresses leave the sweepable param space and `as` replaces them; the bound or swept value fans out to every member at bootstrap. Members must share one scalar kind and stay open (un-bound). |
|
| `gang` | `{"op":"gang","as":"channel_length","into":["channel_hi.length","channel_lo.length"]}` | Fuse two or more sibling params into ONE public knob: the member addresses leave the sweepable param space and `as` replaces them; the bound or swept value fans out to every member at bootstrap. Members must share one scalar kind and stay open (un-bound). |
|
||||||
|
|
||||||
|
Omitting `name` is fine for a single strategy, but it has three live
|
||||||
|
consequences once a project has more than one: every unnamed store document
|
||||||
|
is indistinguishable by name (`"graph"` again), every unnamed strategy's
|
||||||
|
default tap recording lands in the same shared `traces/graph/` directory
|
||||||
|
(later runs overwrite earlier ones), and two `use`-splices of unnamed
|
||||||
|
blueprints collide on the same default instance identifier (`graph`) inside
|
||||||
|
the same composing script. A one-line `name` op dissolves all three at once.
|
||||||
|
|
||||||
Value forms are the typed-tag representations used everywhere in this
|
Value forms are the typed-tag representations used everywhere in this
|
||||||
family of artifacts:
|
family of artifacts:
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ entirely (there is no composite in the flat graph). The blueprint's field /
|
|||||||
input-role *names* are **non-load-bearing** — the wiring resolves by index, and names
|
input-role *names* are **non-load-bearing** — the wiring resolves by index, and names
|
||||||
survive at most as **informative debug symbols** (exactly as `FieldSpec.name` already
|
survive at most as **informative debug symbols** (exactly as `FieldSpec.name` already
|
||||||
is, C8), kept for tracing / rendering (C9 graph-as-data) but carrying no run
|
is, C8), kept for tracing / rendering (C9 graph-as-data) but carrying no run
|
||||||
semantics. The flat graph is then the target of **behaviour-preserving optimisation**
|
semantics. (Reconciling with the root composite's own render name, #331: that
|
||||||
|
claim stands unweakened — the name never influences compilation, identity, or
|
||||||
|
execution semantics — but at *run* time, outside compilation, it keys the trace
|
||||||
|
directory (`traces/<name>/`); that operational role, not any load-bearing
|
||||||
|
weight inside the flat graph, is exactly why the authored-blueprint intakes
|
||||||
|
shape-gate it.) The flat graph is then the target of **behaviour-preserving optimisation**
|
||||||
— any transform that leaves every observable sink trace bit-identical (C1 is the
|
— any transform that leaves every observable sink trace bit-identical (C1 is the
|
||||||
correctness invariant) — on two levels:
|
correctness invariant) — on two levels:
|
||||||
|
|
||||||
|
|||||||
@@ -196,8 +196,25 @@ rejected at construction.
|
|||||||
|
|
||||||
The op-script is a JSON **array of ops**, each object internally tagged by `"op"`,
|
The op-script is a JSON **array of ops**, each object internally tagged by `"op"`,
|
||||||
replayed in order; nodes are referenced **by identifier**, ports as dotted
|
replayed in order; nodes are referenced **by identifier**, ports as dotted
|
||||||
`<identifier>.<port>`. The ten verbs:
|
`<identifier>.<port>`. The eleven verbs:
|
||||||
|
|
||||||
|
- `name` — `{"op":"name","name":<str>}` — set the composite's render name
|
||||||
|
(#331), script-level and at-most-once (a second refuses); omitted, the
|
||||||
|
built composite keeps the CLI's own seed default (`"graph"`). Gated by a
|
||||||
|
shared shape check (non-empty, single path segment, no `/`, `\`, `.` or
|
||||||
|
`..`) applied at this op intake **plus every CLI intake that reads an
|
||||||
|
authored blueprint envelope from a file** — `register`, `introspect
|
||||||
|
--content-id <FILE>`, the bare graph-file viewer, `run`, `introspect
|
||||||
|
--params <FILE>`, `sweep --list-axes`, and each of
|
||||||
|
`sweep`/`walkforward`/`mc`/`generalize`'s file-reading entry points
|
||||||
|
(synthetic and `--real` alike) — one class of intake, one gate. The
|
||||||
|
refusal's core sentence (`name_gate_fault_prose`) is byte-uniform across
|
||||||
|
every one of these sites; only the leading context prefix varies —
|
||||||
|
`run` and the bare graph-file viewer prepend `aura: <path>:`, while
|
||||||
|
`register`, the sweep-axis family (`validate_and_register_axes`), and
|
||||||
|
`introspect` prepend bare `aura: `. Store read-back (`reproduce`, `use`
|
||||||
|
resolution, `introspect`/`--params` by content id) stays deliberately
|
||||||
|
ungated — C29: a registered artifact is never retroactively invalidated.
|
||||||
- `source` — `{"op":"source","role":<str>,"kind":<ScalarKind>}` — declare a root
|
- `source` — `{"op":"source","role":<str>,"kind":<ScalarKind>}` — declare a root
|
||||||
source role producing a base column of `kind`.
|
source role producing a base column of `kind`.
|
||||||
- `input` — `{"op":"input","role":<str>}` — declare a root input role (kind inferred
|
- `input` — `{"op":"input","role":<str>}` — declare a root input role (kind inferred
|
||||||
|
|||||||
+1
-1
@@ -37,7 +37,7 @@ The param-generic, input-role-generic graph-as-data produced by running a Rust b
|
|||||||
|
|
||||||
### blueprint label
|
### blueprint label
|
||||||
**Avoid:** —
|
**Avoid:** —
|
||||||
A registry-level name pointing at a registered blueprint's content id (`graph register --name <label>`, #317) — a mutable, latest-wins pointer over the immutable content-addressed store, not a second identity: re-registering under an existing label *repoints* it (the earlier content id stays reachable by its own id, never invalidated). `graph introspect --registered` lists every label with its content-id prefix and root `doc` line — the discovery surface a `use (op)` reference by name resolves against.
|
A registry-level name pointing at a registered blueprint's content id (`graph register --name <label>`, #317) — a mutable, latest-wins pointer over the immutable content-addressed store, not a second identity: re-registering under an existing label *repoints* it (the earlier content id stays reachable by its own id, never invalidated). `graph introspect --registered` lists every label with its content-id prefix and root `doc` line — the discovery surface a `use (op)` reference by name resolves against. Orthogonal to the composite's own **render name** (the `name` field carried in the blueprint bytes, op-settable via `{"op":"name",...}`, #331; defaults to `"graph"` if omitted): the render name keys the run's trace directory (`traces/<name>/`) and a `use`-splice's default instance identifier, while the label is an explicit, separately-set store pointer.
|
||||||
|
|
||||||
### bootstrap
|
### bootstrap
|
||||||
**Avoid:** —
|
**Avoid:** —
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
# Fieldtest transcript — cycle #331 (blueprint name op)
|
||||||
|
|
||||||
|
Source-blind consumer field test of the eleventh op-script op
|
||||||
|
`{"op":"name","name":"<name>"}` — the composite render name that flows to store
|
||||||
|
documents, `traces/<name>/`, and `use`-splice instance defaults, gated by a
|
||||||
|
deterministic shape check at every authored intake.
|
||||||
|
|
||||||
|
Binary: `target/release/aura`, built from HEAD `4ff85b9` via
|
||||||
|
`cargo build --release -p aura-cli` (Finished in 5.48s — HEAD builds clean).
|
||||||
|
Scratch project `aura new nm_lab` in `/tmp/ftwork/nm_lab`; real data from the
|
||||||
|
built-in archive (US500). Public interface only (README, authoring-guide,
|
||||||
|
glossary, design ledger C23/C24/C29, `aura … --help`) — no crate source read.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example 1 — authoring loop end to end (`c331_1_named_crossover.ops.json`)
|
||||||
|
|
||||||
|
A named SMA-crossover with a `spread` tap, all knobs bound (closed → runnable).
|
||||||
|
|
||||||
|
```
|
||||||
|
$ aura graph build < c331_1_named_crossover.ops.json > c331_1_named_crossover.bp.json # exit 0
|
||||||
|
# blueprint.name == "ny_momentum", taps == [{"name":"spread",...}]
|
||||||
|
$ aura graph register c331_1_named_crossover.bp.json
|
||||||
|
registered blueprint 347a361997e8a65d4122b5ab50367031f78df24a1ff6ad14477ed2c04ec3b603 (…)
|
||||||
|
# store doc runs/blueprints/347a…json -> name: ny_momentum
|
||||||
|
$ aura run c331_1_named_crossover.bp.json --real US500 --tap spread=mean # exit 0
|
||||||
|
$ find runs/traces
|
||||||
|
runs/traces/ny_momentum
|
||||||
|
runs/traces/ny_momentum/index.json
|
||||||
|
runs/traces/ny_momentum/spread.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The authored name `ny_momentum` shows up in all three documented places — the
|
||||||
|
blueprint bytes, the store document's `name`, and the run's trace directory
|
||||||
|
`traces/ny_momentum/`. (Finding W1.)
|
||||||
|
|
||||||
|
### Default collision story (omitting the name)
|
||||||
|
|
||||||
|
Two *different* unnamed crossovers (A: fast=3/slow=8, B: fast=5/slow=20), both
|
||||||
|
build to `name: "graph"`, both `--tap spread=record` over US500:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ aura run c331_1b_unnamed_A.bp.json --real US500 --tap spread=record # exit 0
|
||||||
|
sha256(runs/traces/graph/spread.json) = 5bb5dcfc…
|
||||||
|
$ aura run c331_1c_unnamed_B.bp.json --real US500 --tap spread=record # exit 0
|
||||||
|
sha256(runs/traces/graph/spread.json) = 3be8025b… ← overwritten, silently
|
||||||
|
$ ls runs/traces/ → graph ny_momentum
|
||||||
|
```
|
||||||
|
|
||||||
|
Both unnamed strategies share `traces/graph/`; B silently overwrote A's
|
||||||
|
`spread.json` (differing sha). This is exactly the documented "later runs
|
||||||
|
overwrite earlier ones" consequence — observable, no warning (as documented).
|
||||||
|
(Finding W1.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example 2 — refusal battery (`c331_2*`)
|
||||||
|
|
||||||
|
### Op route (op-indexed prose)
|
||||||
|
|
||||||
|
```
|
||||||
|
$ aura graph build < c331_2a_dup_name.ops.json # exit 1
|
||||||
|
aura: op 1 (name): a script names its blueprint at most once
|
||||||
|
$ aura graph build < c331_2b_slash_name.ops.json # exit 1
|
||||||
|
aura: op 0 (name): blueprint name "traces/evil" is invalid: must not contain '/' or '\' (a single path segment)
|
||||||
|
$ aura graph build < c331_2c_empty_name.ops.json # exit 1
|
||||||
|
aura: op 0 (name): blueprint name "" is invalid: must be non-empty (a single path segment)
|
||||||
|
$ aura graph build < c331_2d_dotdot_name.ops.json # exit 1
|
||||||
|
aura: op 0 (name): blueprint name ".." is invalid: must not be "." or ".." (a single path segment)
|
||||||
|
```
|
||||||
|
|
||||||
|
Each refusal is op-indexed, quotes the offending name, and states the exact
|
||||||
|
rule. Copy-paste fixable. (Finding W2.)
|
||||||
|
|
||||||
|
### Hand-edited envelope with a bad root name (`../escape`)
|
||||||
|
|
||||||
|
`c331_2e_open_named.bp.json` (clean name `clean_name`, `fast.length` open),
|
||||||
|
root `name` hand-edited to `../escape` → `c331_2e_badroot.bp.json`, fed to
|
||||||
|
every file intake:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ aura graph register c331_2e_badroot.bp.json # exit 1 GATED
|
||||||
|
aura: blueprint name "../escape" is invalid: must not contain '/' or '\' (a single path segment)
|
||||||
|
$ aura run c331_2e_badroot.bp.json --real US500 # exit 2 GATED
|
||||||
|
aura: …/c331_2e_badroot.bp.json: blueprint name "../escape" is invalid: …
|
||||||
|
$ aura sweep c331_2e_badroot.bp.json --axis fast.length=2,4 # exit 2 GATED
|
||||||
|
aura: blueprint name "../escape" is invalid: …
|
||||||
|
$ aura graph introspect --content-id c331_2e_badroot.bp.json # exit 1 GATED
|
||||||
|
$ aura graph introspect --params c331_2e_badroot.bp.json # exit 1 GATED
|
||||||
|
$ aura graph c331_2e_badroot.bp.json # exit 2 GATED
|
||||||
|
aura: …/c331_2e_badroot.bp.json: blueprint name "../escape" is invalid: …
|
||||||
|
|
||||||
|
$ aura sweep c331_2e_badroot.bp.json --list-axes # exit 0 *** NOT GATED ***
|
||||||
|
./escape.fast.length:I64
|
||||||
|
slow.length:I64 default=8
|
||||||
|
bias.scale:F64 default=0.5
|
||||||
|
```
|
||||||
|
|
||||||
|
Six of seven file intakes gate uniformly (Finding W2). **`sweep --list-axes`
|
||||||
|
does not** — it exits 0 and prints a mangled, retired *wrapped* axis form
|
||||||
|
`./escape.fast.length` for the open param only. The same on a `.` root:
|
||||||
|
`sweep --list-axes` → `.fast.length:I64` (exit 0) while `register`/`--params`
|
||||||
|
refuse. Pasting the leaked `./escape.fast.length` back as `--axis` refuses —
|
||||||
|
so discovery emits a name the bind surface rejects. (Finding B1.)
|
||||||
|
|
||||||
|
Prose note: `run` and `graph` prepend the full file path; `register`,
|
||||||
|
`sweep --axis`, `introspect` do not — the core sentence is uniform, the prefix
|
||||||
|
is not. (Finding SG1.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example 3 — use-splice interplay (`c331_3*`)
|
||||||
|
|
||||||
|
Registered patterns: `agree` (input `x`, SMA length open), `refute` (input `y`),
|
||||||
|
`unnamedsm` (unnamed → `graph`, input `z`).
|
||||||
|
|
||||||
|
```
|
||||||
|
# A — splice agree, NO instance name:
|
||||||
|
$ aura graph build < c331_3A_splice_named.ops.json # exit 0
|
||||||
|
aura: note: use "agree": agree -> 4f78a9f6e1f3…
|
||||||
|
$ aura graph introspect --params c331_3A.bp.json
|
||||||
|
agree.sma.length:I64 ← instance defaulted to authored name
|
||||||
|
|
||||||
|
# B — splice unnamedsm TWICE, no instance names:
|
||||||
|
$ aura graph build < c331_3B_two_unnamed.ops.json # exit 1
|
||||||
|
aura: note: use "graph": unnamedsm -> 25cdd4942f21…
|
||||||
|
aura: note: use "graph": unnamedsm -> 25cdd4942f21…
|
||||||
|
aura: op 3 (use): duplicate identifier "graph" ← collide, op-indexed
|
||||||
|
|
||||||
|
# C — splice agree + refute, no instance names:
|
||||||
|
$ aura graph build < c331_3C_two_named.ops.json # exit 0
|
||||||
|
$ aura graph introspect --params c331_3C.bp.json
|
||||||
|
agree.sma.length:I64
|
||||||
|
refute.sma.length:I64 ← distinct names, no collision
|
||||||
|
```
|
||||||
|
|
||||||
|
Instance identifier defaults to the authored render name; two unnamed
|
||||||
|
(`graph`) splices collide with an op-indexed `duplicate identifier "graph"`;
|
||||||
|
distinct authored names dissolve the collision. Matches guide + glossary
|
||||||
|
`blueprint label` entry exactly. (Finding W3.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example 4 — discovery / docs parity (`c331_4_*`)
|
||||||
|
|
||||||
|
`aura graph build --help` lists the name op last of the eleven:
|
||||||
|
|
||||||
|
```
|
||||||
|
{"op":"name","name":"ny_momentum"}
|
||||||
|
set the composite's render name, at most once per script (default
|
||||||
|
"graph" if omitted)
|
||||||
|
```
|
||||||
|
|
||||||
|
Consistent with the authoring guide's eleven-ops table (§1). Three distinct
|
||||||
|
`--name`-ish concepts, each with distinct help phrasing:
|
||||||
|
|
||||||
|
- op `name` → **render name** (keys `traces/<name>/`, use-splice instance default)
|
||||||
|
- `aura graph register --name` → **registry label** ("Label the registered
|
||||||
|
content id for `use` by name (#317)")
|
||||||
|
- `aura sweep --name` → **family name** ("Family name (records to the registry…)")
|
||||||
|
|
||||||
|
The glossary `blueprint label` entry separates render name from registry label
|
||||||
|
explicitly. Distinction is learnable from the public docs alone, though a
|
||||||
|
consumer must consult the glossary to fully disambiguate the three. (Finding W4.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Findings summary
|
||||||
|
|
||||||
|
- W1 (working, axis 1): authored name → blueprint bytes + store doc +
|
||||||
|
`traces/<name>/`; default `graph` collision observable (silent overwrite).
|
||||||
|
- W2 (working, axis 2): op-route refusals op-indexed + specific; hand-edited
|
||||||
|
bad root name gated uniformly on 6 of 7 file intakes.
|
||||||
|
- W3 (working, axis 3): use-splice instance defaults to authored name; unnamed
|
||||||
|
splices collide (op-indexed); named splices coexist.
|
||||||
|
- W4 (working, axis 4): `graph build --help` lists the name op consistently
|
||||||
|
with the guide; render-name vs registry-label distinction learnable.
|
||||||
|
- B1 (BUG, axis 2): `sweep --list-axes` does not gate a bad root name (exit 0)
|
||||||
|
and leaks a retired, non-bindable wrapped axis form (`./escape.fast.length`),
|
||||||
|
breaking #328's `--params`/`--list-axes` line-identity and the
|
||||||
|
every-discovered-name-is-verbatim-legal promise.
|
||||||
|
- SG1 (spec_gap, axis 2): "uniform refusal prose" — `run`/`graph` prepend the
|
||||||
|
file path, other seams don't; unclear whether "uniform" means the core
|
||||||
|
sentence or byte-identical.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"format_version":1,"blueprint":{"name":"ny_momentum","doc":"SMA crossover bias with a raw-spread measurement tap","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":8}}]}},{"primitive":{"type":"Sub","name":"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"}],"taps":[{"name":"spread","from":{"node":2,"field":0}}]}}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[
|
||||||
|
{"op": "name", "name": "ny_momentum"},
|
||||||
|
{"op": "doc", "text": "SMA crossover bias with a raw-spread measurement tap"},
|
||||||
|
{"op": "source", "role": "price", "kind": "F64"},
|
||||||
|
{"op": "add", "type": "SMA", "name": "fast", "bind": {"length": {"I64": 3}}},
|
||||||
|
{"op": "add", "type": "SMA", "name": "slow", "bind": {"length": {"I64": 8}}},
|
||||||
|
{"op": "feed", "role": "price", "into": ["fast.series", "slow.series"]},
|
||||||
|
{"op": "add", "type": "Sub", "name": "sub"},
|
||||||
|
{"op": "connect", "from": "fast.value", "to": "sub.lhs"},
|
||||||
|
{"op": "connect", "from": "slow.value", "to": "sub.rhs"},
|
||||||
|
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 0.5}}},
|
||||||
|
{"op": "connect", "from": "sub.value", "to": "bias.signal"},
|
||||||
|
{"op": "tap", "from": "sub.value", "as": "spread"},
|
||||||
|
{"op": "expose", "from": "bias.bias", "as": "bias"}
|
||||||
|
]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
registered blueprint 347a361997e8a65d4122b5ab50367031f78df24a1ff6ad14477ed2c04ec3b603 (/tmp/ftwork/nm_lab/runs/blueprints/347a361997e8a65d4122b5ab50367031f78df24a1ff6ad14477ed2c04ec3b603.json)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"manifest":{"commit":"4ff85b94e56c6946361ecbcd94a6f6f99d49268c","params":[],"defaults":[["fast.length",{"I64":3}],["slow.length",{"I64":8}],["bias.scale",{"F64":0.5}]],"window":[1407103199999000000,1784926380000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","topology_hash":"347a361997e8a65d4122b5ab50367031f78df24a1ff6ad14477ed2c04ec3b603","project":{"commit":"f33b20e6d6cbfda4d9031cfc9c2361f5e751436b"}},"metrics":{"total_pips":-13655.020833332888,"max_drawdown":13733.579166666279,"bias_sign_flips":536589,"r":{"expectancy_r":-0.024114883486709465,"n_trades":581881,"win_rate":0.3205792937043829,"avg_win_r":1.62333970987585,"avg_loss_r":-0.8014533205646299,"profit_factor":0.9557138265296112,"max_r_drawdown":14084.435329973425,"n_open_at_end":1,"sqn":-8.965122280661042,"sqn_normalized":-0.11752740018906035,"net_expectancy_r":-0.024114883486709465,"conviction_terciles_r":[-0.024905829212096428,-0.022878394661105868,-0.024560424289849826]}}}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# traces after example 1 (named) + collision runs (unnamed)
|
||||||
|
runs/traces
|
||||||
|
runs/traces/graph
|
||||||
|
runs/traces/graph/index.json
|
||||||
|
runs/traces/graph/spread.json
|
||||||
|
runs/traces/ny_momentum
|
||||||
|
runs/traces/ny_momentum/index.json
|
||||||
|
runs/traces/ny_momentum/spread.json
|
||||||
|
|
||||||
|
# trace index for ny_momentum:
|
||||||
|
{"manifest":{"commit":"4ff85b94e56c6946361ecbcd94a6f6f99d49268c","params":[],"defaults":[["fast.length",{"I64":3}],["slow.length",{"I64":8}],["bias.scale",{"F64":0.5}]],"window":[1407103199999000000,1784926380000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","topology_hash":"347a361997e8a65d4122b5ab50367031f78df24a1ff6ad14477ed2c04ec3b603","project":{"commit":"f33b20e6d6cbfda4d9031cfc9c2361f5e751436b"}},"taps":["spread"]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"format_version":1,"blueprint":{"name":"graph","doc":"unnamed crossover A (fast=3, slow=8) — no name op, defaults to graph","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":8}}]}},{"primitive":{"type":"Sub","name":"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"}],"taps":[{"name":"spread","from":{"node":2,"field":0}}]}}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{"op": "doc", "text": "unnamed crossover A (fast=3, slow=8) — no name op, defaults to graph"},
|
||||||
|
{"op": "source", "role": "price", "kind": "F64"},
|
||||||
|
{"op": "add", "type": "SMA", "name": "fast", "bind": {"length": {"I64": 3}}},
|
||||||
|
{"op": "add", "type": "SMA", "name": "slow", "bind": {"length": {"I64": 8}}},
|
||||||
|
{"op": "feed", "role": "price", "into": ["fast.series", "slow.series"]},
|
||||||
|
{"op": "add", "type": "Sub", "name": "sub"},
|
||||||
|
{"op": "connect", "from": "fast.value", "to": "sub.lhs"},
|
||||||
|
{"op": "connect", "from": "slow.value", "to": "sub.rhs"},
|
||||||
|
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 0.5}}},
|
||||||
|
{"op": "connect", "from": "sub.value", "to": "bias.signal"},
|
||||||
|
{"op": "tap", "from": "sub.value", "as": "spread"},
|
||||||
|
{"op": "expose", "from": "bias.bias", "as": "bias"}
|
||||||
|
]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"format_version":1,"blueprint":{"name":"graph","doc":"unnamed crossover B (fast=5, slow=20) — no name op, defaults to graph","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":5}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":20}}]}},{"primitive":{"type":"Sub","name":"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"}],"taps":[{"name":"spread","from":{"node":2,"field":0}}]}}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{"op": "doc", "text": "unnamed crossover B (fast=5, slow=20) — no name op, defaults to graph"},
|
||||||
|
{"op": "source", "role": "price", "kind": "F64"},
|
||||||
|
{"op": "add", "type": "SMA", "name": "fast", "bind": {"length": {"I64": 5}}},
|
||||||
|
{"op": "add", "type": "SMA", "name": "slow", "bind": {"length": {"I64": 20}}},
|
||||||
|
{"op": "feed", "role": "price", "into": ["fast.series", "slow.series"]},
|
||||||
|
{"op": "add", "type": "Sub", "name": "sub"},
|
||||||
|
{"op": "connect", "from": "fast.value", "to": "sub.lhs"},
|
||||||
|
{"op": "connect", "from": "slow.value", "to": "sub.rhs"},
|
||||||
|
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 0.5}}},
|
||||||
|
{"op": "connect", "from": "sub.value", "to": "bias.signal"},
|
||||||
|
{"op": "tap", "from": "sub.value", "as": "spread"},
|
||||||
|
{"op": "expose", "from": "bias.bias", "as": "bias"}
|
||||||
|
]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
aura: op 1 (name): a script names its blueprint at most once
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[
|
||||||
|
{"op": "name", "name": "first_name"},
|
||||||
|
{"op": "name", "name": "second_name"},
|
||||||
|
{"op": "doc", "text": "two name ops — should refuse at the second"},
|
||||||
|
{"op": "source", "role": "price", "kind": "F64"},
|
||||||
|
{"op": "add", "type": "SMA", "name": "fast", "bind": {"length": {"I64": 3}}},
|
||||||
|
{"op": "feed", "role": "price", "into": ["fast.series"]},
|
||||||
|
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 0.5}}},
|
||||||
|
{"op": "connect", "from": "fast.value", "to": "bias.signal"},
|
||||||
|
{"op": "expose", "from": "bias.bias", "as": "bias"}
|
||||||
|
]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
aura: op 0 (name): blueprint name "traces/evil" is invalid: must not contain '/' or '\' (a single path segment)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[
|
||||||
|
{"op": "name", "name": "traces/evil"},
|
||||||
|
{"op": "doc", "text": "name with a path separator — should refuse"},
|
||||||
|
{"op": "source", "role": "price", "kind": "F64"},
|
||||||
|
{"op": "add", "type": "SMA", "name": "fast", "bind": {"length": {"I64": 3}}},
|
||||||
|
{"op": "feed", "role": "price", "into": ["fast.series"]},
|
||||||
|
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 0.5}}},
|
||||||
|
{"op": "connect", "from": "fast.value", "to": "bias.signal"},
|
||||||
|
{"op": "expose", "from": "bias.bias", "as": "bias"}
|
||||||
|
]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
aura: op 0 (name): blueprint name "" is invalid: must be non-empty (a single path segment)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[
|
||||||
|
{"op": "name", "name": ""},
|
||||||
|
{"op": "doc", "text": "empty name — should refuse"},
|
||||||
|
{"op": "source", "role": "price", "kind": "F64"},
|
||||||
|
{"op": "add", "type": "SMA", "name": "fast", "bind": {"length": {"I64": 3}}},
|
||||||
|
{"op": "feed", "role": "price", "into": ["fast.series"]},
|
||||||
|
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 0.5}}},
|
||||||
|
{"op": "connect", "from": "fast.value", "to": "bias.signal"},
|
||||||
|
{"op": "expose", "from": "bias.bias", "as": "bias"}
|
||||||
|
]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
aura: op 0 (name): blueprint name ".." is invalid: must not be "." or ".." (a single path segment)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[
|
||||||
|
{"op": "name", "name": ".."},
|
||||||
|
{"op": "doc", "text": "parent-dir name — should refuse"},
|
||||||
|
{"op": "source", "role": "price", "kind": "F64"},
|
||||||
|
{"op": "add", "type": "SMA", "name": "fast", "bind": {"length": {"I64": 3}}},
|
||||||
|
{"op": "feed", "role": "price", "into": ["fast.series"]},
|
||||||
|
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 0.5}}},
|
||||||
|
{"op": "connect", "from": "fast.value", "to": "bias.signal"},
|
||||||
|
{"op": "expose", "from": "bias.bias", "as": "bias"}
|
||||||
|
]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"format_version":1,"blueprint":{"name":"../escape","doc":"open crossover (fast.length free) with a clean name, for hand-editing","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":8}}]}},{"primitive":{"type":"Sub","name":"sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
aura: blueprint name "../escape" is invalid: must not contain '/' or '\' (a single path segment)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"format_version":1,"blueprint":{"name":"clean_name","doc":"open crossover (fast.length free) with a clean name, for hand-editing","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":8}}]}},{"primitive":{"type":"Sub","name":"sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{"op": "name", "name": "clean_name"},
|
||||||
|
{"op": "doc", "text": "open crossover (fast.length free) with a clean name, for hand-editing"},
|
||||||
|
{"op": "source", "role": "price", "kind": "F64"},
|
||||||
|
{"op": "add", "type": "SMA", "name": "fast"},
|
||||||
|
{"op": "add", "type": "SMA", "name": "slow", "bind": {"length": {"I64": 8}}},
|
||||||
|
{"op": "feed", "role": "price", "into": ["fast.series", "slow.series"]},
|
||||||
|
{"op": "add", "type": "Sub", "name": "sub"},
|
||||||
|
{"op": "connect", "from": "fast.value", "to": "sub.lhs"},
|
||||||
|
{"op": "connect", "from": "slow.value", "to": "sub.rhs"},
|
||||||
|
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 0.5}}},
|
||||||
|
{"op": "connect", "from": "sub.value", "to": "bias.signal"},
|
||||||
|
{"op": "expose", "from": "bias.bias", "as": "bias"}
|
||||||
|
]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
aura: blueprint name "../escape" is invalid: must not contain '/' or '\' (a single path segment)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
aura: blueprint name "../escape" is invalid: must not contain '/' or '\' (a single path segment)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
aura: /home/brummel/dev/aura/.claude/worktrees/issue-331-blueprint-name/fieldtests/cycle-331-blueprint-name/c331_2e_badroot.bp.json: blueprint name "../escape" is invalid: must not contain '/' or '\' (a single path segment)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
aura: /home/brummel/dev/aura/.claude/worktrees/issue-331-blueprint-name/fieldtests/cycle-331-blueprint-name/c331_2e_badroot.bp.json: blueprint name "../escape" is invalid: must not contain '/' or '\' (a single path segment)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
aura: blueprint name "../escape" is invalid: must not contain '/' or '\' (a single path segment)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
./escape.fast.length:I64
|
||||||
|
slow.length:I64 default=8
|
||||||
|
bias.scale:F64 default=0.5
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"format_version":1,"blueprint":{"name":".","doc":"open crossover (fast.length free) with a clean name, for hand-editing","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":8}}]}},{"primitive":{"type":"Sub","name":"sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"format_version":1,"blueprint":{"name":"graph","doc":"splice agree once, NO instance name — instance should default to authored name","nodes":[{"composite":{"name":"agree","doc":"reusable SMA smoother over an open input role x; length open","nodes":[{"primitive":{"type":"SMA","name":"sma"}}],"input_roles":[{"name":"x","targets":[{"node":0,"slot":0}]}],"output":[{"node":0,"field":0,"name":"smoothed"}]}}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0}],"source":"F64"}],"output":[{"node":0,"field":0,"name":"out"}]}}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
aura: note: use "agree": agree -> 4f78a9f6e1f3e1c7671edaf1be29d1e339fd07f38fb2c65cb23637502f94d154
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[
|
||||||
|
{"op": "doc", "text": "splice agree once, NO instance name — instance should default to authored name"},
|
||||||
|
{"op": "source", "role": "price", "kind": "F64"},
|
||||||
|
{"op": "use", "ref": {"name": "agree"}},
|
||||||
|
{"op": "feed", "role": "price", "into": ["agree.x"]},
|
||||||
|
{"op": "expose", "from": "agree.smoothed", "as": "out"}
|
||||||
|
]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
aura: note: use "graph": unnamedsm -> 25cdd4942f217381da0d799b0c5b22752a963a2bdc7df83e5d1377ae3b974e96
|
||||||
|
aura: note: use "graph": unnamedsm -> 25cdd4942f217381da0d799b0c5b22752a963a2bdc7df83e5d1377ae3b974e96
|
||||||
|
aura: op 3 (use): duplicate identifier "graph"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[
|
||||||
|
{"op": "doc", "text": "splice the unnamed (graph) smoother TWICE, no instance names — should collide"},
|
||||||
|
{"op": "source", "role": "price", "kind": "F64"},
|
||||||
|
{"op": "use", "ref": {"name": "unnamedsm"}},
|
||||||
|
{"op": "use", "ref": {"name": "unnamedsm"}},
|
||||||
|
{"op": "feed", "role": "price", "into": ["graph.z"]},
|
||||||
|
{"op": "expose", "from": "graph.smoothed", "as": "out"}
|
||||||
|
]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"format_version":1,"blueprint":{"name":"graph","doc":"splice agree + refute, NO instance names — distinct authored names, no collision","nodes":[{"composite":{"name":"agree","doc":"reusable SMA smoother over an open input role x; length open","nodes":[{"primitive":{"type":"SMA","name":"sma"}}],"input_roles":[{"name":"x","targets":[{"node":0,"slot":0}]}],"output":[{"node":0,"field":0,"name":"smoothed"}]}},{"composite":{"name":"refute","doc":"a second reusable SMA smoother over open input role y; length open","nodes":[{"primitive":{"type":"SMA","name":"sma"}}],"input_roles":[{"name":"y","targets":[{"node":0,"slot":0}]}],"output":[{"node":0,"field":0,"name":"smoothed"}]}}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":0,"field":0,"name":"agree_out"},{"node":1,"field":0,"name":"refute_out"}]}}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
aura: note: use "agree": agree -> 4f78a9f6e1f3e1c7671edaf1be29d1e339fd07f38fb2c65cb23637502f94d154
|
||||||
|
aura: note: use "refute": refute -> 9580a623d1faf83841482ae08874ffe2464aff30a2ac2e800a9b47a3bb56d1f2
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[
|
||||||
|
{"op": "doc", "text": "splice agree + refute, NO instance names — distinct authored names, no collision"},
|
||||||
|
{"op": "source", "role": "price", "kind": "F64"},
|
||||||
|
{"op": "use", "ref": {"name": "agree"}},
|
||||||
|
{"op": "use", "ref": {"name": "refute"}},
|
||||||
|
{"op": "feed", "role": "price", "into": ["agree.x", "refute.y"]},
|
||||||
|
{"op": "expose", "from": "agree.smoothed", "as": "agree_out"},
|
||||||
|
{"op": "expose", "from": "refute.smoothed", "as": "refute_out"}
|
||||||
|
]
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[
|
||||||
|
{"op": "name", "name": "agree"},
|
||||||
|
{"op": "doc", "text": "reusable SMA smoother over an open input role x; length open"},
|
||||||
|
{"op": "input", "role": "x"},
|
||||||
|
{"op": "add", "type": "SMA", "name": "sma"},
|
||||||
|
{"op": "feed", "role": "x", "into": ["sma.series"]},
|
||||||
|
{"op": "expose", "from": "sma.value", "as": "smoothed"}
|
||||||
|
]
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[
|
||||||
|
{"op": "name", "name": "refute"},
|
||||||
|
{"op": "doc", "text": "a second reusable SMA smoother over open input role y; length open"},
|
||||||
|
{"op": "input", "role": "y"},
|
||||||
|
{"op": "add", "type": "SMA", "name": "sma"},
|
||||||
|
{"op": "feed", "role": "y", "into": ["sma.series"]},
|
||||||
|
{"op": "expose", "from": "sma.value", "as": "smoothed"}
|
||||||
|
]
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
registered blueprint 4f78a9f6e1f3e1c7671edaf1be29d1e339fd07f38fb2c65cb23637502f94d154 (/tmp/ftwork/nm_lab/runs/blueprints/4f78a9f6e1f3e1c7671edaf1be29d1e339fd07f38fb2c65cb23637502f94d154.json)
|
||||||
|
label "agree" -> 4f78a9f6e1f3e1c7671edaf1be29d1e339fd07f38fb2c65cb23637502f94d154
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
registered blueprint 9580a623d1faf83841482ae08874ffe2464aff30a2ac2e800a9b47a3bb56d1f2 (/tmp/ftwork/nm_lab/runs/blueprints/9580a623d1faf83841482ae08874ffe2464aff30a2ac2e800a9b47a3bb56d1f2.json)
|
||||||
|
label "refute" -> 9580a623d1faf83841482ae08874ffe2464aff30a2ac2e800a9b47a3bb56d1f2
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
registered blueprint 25cdd4942f217381da0d799b0c5b22752a963a2bdc7df83e5d1377ae3b974e96 (/tmp/ftwork/nm_lab/runs/blueprints/25cdd4942f217381da0d799b0c5b22752a963a2bdc7df83e5d1377ae3b974e96.json)
|
||||||
|
label "unnamedsm" -> 25cdd4942f217381da0d799b0c5b22752a963a2bdc7df83e5d1377ae3b974e96
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[
|
||||||
|
{"op": "doc", "text": "an UNNAMED reusable SMA smoother over open input role z; defaults to graph"},
|
||||||
|
{"op": "input", "role": "z"},
|
||||||
|
{"op": "add", "type": "SMA", "name": "sma"},
|
||||||
|
{"op": "feed", "role": "z", "into": ["sma.series"]},
|
||||||
|
{"op": "expose", "from": "sma.value", "as": "smoothed"}
|
||||||
|
]
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
Construct a graph from a stdin op-list
|
||||||
|
|
||||||
|
Usage: aura graph build [OPTIONS]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--release Load the project dylib from target/release instead of target/debug
|
||||||
|
-h, --help Print help
|
||||||
|
|
||||||
|
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":"add","type":"Session","name":"ny","args":{"tz":"America/New_York","open":"09:30"},"bind":{"period_minutes":{"I64":15}}}
|
||||||
|
an arg-bearing type applies "args" (closed, per-type-declared string
|
||||||
|
pairs) BEFORE "bind" — see graph introspect --node <T> for its args
|
||||||
|
{"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)
|
||||||
|
{"op":"use","ref":{"name":"agree"},"name":"gate","bind":{"sma.length":{"I64":9}}}
|
||||||
|
splice a registered blueprint (by "content_id" or "name") under an
|
||||||
|
instance name ("bind" path-qualifies the spliced instance's params)
|
||||||
|
{"op":"name","name":"ny_momentum"}
|
||||||
|
set the composite's render name, at most once per script (default
|
||||||
|
"graph" if omitted)
|
||||||
|
|
||||||
|
Node types and their ports: aura graph introspect --vocabulary | --node <T>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
Register a blueprint document into the content-addressed store (#196)
|
||||||
|
|
||||||
|
Usage: aura graph register [OPTIONS] <FILE>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<FILE> The blueprint .json file to register
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--name <NAME> Label the registered content id for `use` by name (#317); a re-registered label repoints (latest-wins on resolve)
|
||||||
|
--release Load the project dylib from target/release instead of target/debug
|
||||||
|
-h, --help Print help
|
||||||
Reference in New Issue
Block a user