Compare commits

32 Commits

Author SHA1 Message Date
claude 024e8652c0 fix(aura-cli, docs): fieldtest bugs — the non-literal override prose + the guide's family-id shape
- parse_override_tokens distinguishes the grammar fault (no '=' / no
  dotted path — prose unchanged) from a shape-valid token whose VALUE
  fails the scalar-literal parse: the refusal now names the value and
  the literal requirement (styled after the kind-mismatch sibling)
  instead of falsely claiming a NODE.PARAM=VALUE violation. RED-first;
  exit stays 2 on both branches.
- the authoring guide's multi-regime family ids corrected to the
  executor's real shape (infix -r{k} after the window segment, -r0
  explicit on the default regime), and §1's name-op paragraph no
  longer claims naming dissolves same-name trace overwriting (that
  trace-dir question is tracked on #311, not resolved here).
2026-07-26 14:25:26 +02:00
claude 7aac09d49e fieldtest: harvest 2026-07-26 — 4 examples, 2 bugs / 4 friction / 1 spec-gap / 4 working
Source-blind per-cycle fieldtest over the sweep's four axes: the
declared-tap discovery loop closes in three commands without provoked
refusals; fixed{distance} is campaign-reachable with honest R
compression beside the vol regime; the C14 exit partition branches
reliably across sixteen probes; reference-semantics hashes agree
between record line and trace index across bare/no-op/substantive
overrides. Findings routed: both bugs fixed in-cycle (next commit),
frictions bundled as #345, the trace-overwrite spec gap consolidated
onto #311, the guide-rot instance evidenced on #344.
2026-07-26 14:25:26 +02:00
claude f108291b7f audit: harvest 2026-07-26 cycle close — intake enumeration completed, ledger notes, stale comments
Architect drift review (scope 77ad046..HEAD): What holds — the #343
reference-semantics hash is code-backed on both output surfaces
(record line + trace index), the RiskRegime::Fixed stamp/re-derive
lockstep is complete both ways, and both new guide examples build
against the binary. Drift resolved in this commit: C24's
root-name-gated intake enumeration gains the introspect --taps FILE
branch plus its missing per-site pin (byte-identical refusal to the
register sibling — the #331 class-enumeration lesson applied); C27
gains the #337 realization note (positive discovery view closes the
recovery-only gap); C12's inline-hash sentence updated to the topo
parameter; five stale dispatch_run comments reworded to name
exec_blueprint_leg as the surviving owner.

Remaining drift routed: the unpinned guide op-scripts are a durable
debt class, filed as #344 (idea) rather than swept here.

Regression gate: aura-bench all five fingerprints OK (report-only
deltas within noise on a loaded box; no baseline move, nothing to
ratify).

refs #337
refs #343
2026-07-26 14:03:09 +02:00
claude 567f98b4e5 fix(aura-runner, aura-cli): the reference hash reaches the run before persistence
Harvest sweep, review re-check fix.

The prior fix corrected only the stdout record line: run_signal_r
persists the manifest to runs/traces/<name>/index.json (bound.finish)
BEFORE the post-hoc overwrite ran, so an overridden run's trace store
still carried the reopened hash — diverging from stdout in exactly the
case the revised C24/C18 clause describes. run_signal_r now takes
topo: Option<&str> (None = inline computation as before; Some = the
caller's reference-semantics hash, the run_blueprint_member precedent)
so record line and trace index read the one hash built before any
persistence; exec's override branch passes the base document's id and
the post-run mutation is gone. RED-first: the new trace-store pin
failed against the divergent state.

refs #343
2026-07-26 13:47:58 +02:00
claude ef24f06547 fix(aura-cli, ledger): review fixes — override stamps the base hash (#343 revised), prose corrections
Harvest sweep, whole-diff review fix block.

The review refuted the #343 ratification's factual premise: the
campaign leg does NOT mint a reopened member's hash — runner.rs passes
the stored strategy_id as topo even for reopened members (reference
semantics). Decision revised on the issue (fix, not ratify): exec's
override run now stamps the LOADED base document's content id
(computed before reopen_all; RED-first — the inverted pin failed on
the old reopened-hash behaviour), keeping topology_hash
store-resolvable and identical in meaning across both legs; the
variation lives in manifest.params. C24/C12/C18 rewritten to the
corrected reading.

Also per review: C24's retired build-free-introspection sentence
updated (old wording archived in the history sidecar, #339 item 4);
C14's partition key precisely scoped to the routing seam, naming the
legs' differing content-validation exits honestly; measure.rs's false
unreachability comment corrected; --taps added to the guide's
introspect cheat-sheet and pinned on the content-id path; the
zero-trade-cell note routed through the diag macro; doc-comment,
issue-ref, and gate-name corrections; the Fixed-regime NaN branch
pinned.

refs #339
refs #342
refs #343
2026-07-26 13:37:01 +02:00
claude 2e532bce00 docs(ledger), fix(aura-cli): override-hash ratified, exec exit partition pinned, cost-wiring example
Harvest sweep, batch 6 of 6.

- #343 ratified (per the decision minuted on the issue): an override IS
  a topology-parameter variation — the executed run's topology_hash is
  the reopened topology's own, deliberately not required to resolve in
  the store; the manifest's params map plus the base document
  reconstruct the variant. C24 carries the full clause, C12/C18
  cross-reference it; pinned by a bare-vs-noop-override hash test
  (distinct, and stable across runs).
- #342 item 5: C14 pins exec's unresolved-target exit partition on the
  existing usage/runtime line — file-content faults (not-JSON,
  op-script, wrong-kind) exit 2, missing-state faults (unreadable
  non-id target, unregistered content id) exit 1. One sanctioned
  realignment: the wrong-kind refusal moves 1 → 2 to join its
  file-content siblings (RED-first on the updated pin); the
  unregistered-id row gains its missing sibling pin.
- #341 item 4: the authoring guide gains a worked cost-wiring op-script
  (ConstantCost → CostSum via PositionManagement geometry, built and
  verified), honest about being a wiring reference — production cost
  models still come from a campaign document's cost block (C10).

closes #341
closes #342
closes #343
2026-07-26 13:05:36 +02:00
claude 74281842b8 feat(aura-cli, aura-engine): use-aware unwired introspection; gang-rule prose; LinComb discovery
Harvest sweep, batch 5 of 6.

- introspect --unwired resolves use refs through the registry store:
  the parse+resolve phase is extracted from graph build's path into the
  shared parse_and_resolve_ops, so a use-bearing document introspects
  identically to how it builds (fetch, C29 gate, label resolution) —
  retiring both the hard miss and the 'content id' mislabeling of
  by-name refs. The engine's subgraph closure contract is unchanged
  (store-freedom binds the engine, not the CLI caller).
- ganging a spliced instance's member path refuses with the rule
  (OpError::GangOfSplicedInstance) instead of the typo-shaped
  no-such-param prose; the leaf-typo case keeps its UnknownParam shape
  under a new sibling pin.
- the --node pending note names the follow-up moves on the current
  surface (build, then introspect --params / --unwired), and the
  authoring guide gains a worked LinComb op-script example
  (args-then-bind order, term[i]/weights[i], verified by building it).

refs #339
refs #341
2026-07-26 12:47:33 +02:00
claude d2b0cdf64c feat(aura-cli, aura-engine): zero-trade cell + synthetic-smoke notes; pending-builder serialize fence
Harvest sweep, batch 4 of 6.

- present_campaign's cell loop gains the non-walk-forward sibling of
  the #313 note: a cell whose families are all non-wf and whose every
  report traded zero times gets 'aura: note: ... metrics are vacuous,
  not a break-even result' on stderr (emit-independent; wf cells keep
  their window note, gate-truncated cells stay silent).
- exec's single-run bias leg (always RunData::Synthetic — a direct
  blueprint exec never binds real data) notes on a zero-trade run that
  the built-in 18-cycle stream is a smoke fixture and points at a
  campaign document's data section for a meaningful run.
- blueprint_serde refuses to serialize a still-pending arg-bearing
  builder (SerializeError::PendingBuilder naming node type + missing
  args + the try_args remedy) instead of emitting an args-free v1
  document whose fault only surfaces at load as MissingArg — the
  write-side twin of the load-path fence; only the Rust GraphBuilder
  path can reach this state.

closes #324
refs #341
2026-07-26 12:26:27 +02:00
claude 2b692a71e3 feat(aura-research, aura-runner): RiskRegime::Fixed — the fixed stop is campaign-reachable
Harvest sweep, batch 3 of 6 (second half).

The third externally-tagged variant ('{"fixed":{"distance":..}}',
additive — stored Vol/VolTf documents keep their bytes): doc-tier
validation mirrors FixedStop::new's distance>0 assert (NaN refused)
so the assert is unreachable from a document; stop_rule_for_regime
binds the shipped FixedStop composite; run_blueprint_member stamps
stop_distance beside the vol knobs' precedent and
stop_rule_from_params re-derives Fixed from it (stamps are mutually
exclusive; the pre-#233 no-knob fallback is unchanged). BadRegime
prose widened to name the distance rule. C10's risk-axis paragraph
updates from 'additive when needed' to shipped; glossary gains the
fixed variant.

closes #338
2026-07-26 12:10:43 +02:00
claude c39f5e4762 feat(aura-cli, aura-engine): graph introspect --taps — positive declared-tap discovery
Harvest sweep, batch 3 of 6 (first half).

Composite::declared_taps() walks the blueprint depth-first collecting
(tap name, source wire, column kind), names bare at every depth —
mirroring inline_composite's hoist, where a tap's name IS the flat
graph's load-bearing identifier — and bounds-total over invalid wires
(compile's validate_wiring stays the real gate). The CLI view loads
like --params (FILE gates the authored root name, a content id does
not), prints one 'name  node.field  Kind' row per tap, and renders the
tap-less case as a stderr note with empty stdout, exit 0.

Closes the recovery-only discovery loop: since #333 the only way to
learn a blueprint's declared tap names was provoking the undeclared-tap
refusal roster.

closes #337
2026-07-26 12:10:43 +02:00
claude 521459dd50 feat(aura-cli, aura-runner): exec routing-seam refusals + by-name unbound-role prose
Harvest sweep, batch 2 of 6.

- exec's file-target classification is now a four-way shape peek
  (campaign / blueprint / op-script / wrong-kind): a valid-JSON op-script
  array refuses at the routing seam with a build-first pointer ('aura
  graph build') instead of the blueprint leg's false 'not valid JSON'
  claim, and a kind-bearing non-campaign document names the found kind
  plus exec's two executable classes. Exit-code classes unchanged
  (op-script usage-class 2, wrong-kind runtime-class 1).
- the bare-tap measurement leg renders CompileError::UnboundRootRole by
  the signal's own root-role NAME (captured pre-consumption), retiring
  the raw Debug-form leak; every other variant keeps the total fallback.

refs #342
refs #339
2026-07-26 11:47:21 +02:00
claude 6b6086fdef fix(aura-cli): unknown-key refusal cites the op index; two diagnostics hints
Harvest sweep, batch 1 of 6.

- graph build's unknown-key refusal now parses the op-list element-by-
  element and attributes any per-element parse fault as 'op N (kind):',
  immune to serde's deny-unknown-fields token position landing on the
  following element (RED-first: the misattribution was pinned failing
  before the fix).
- the UnsupportedVersion refusal names the full accepted range
  ('versions 1..=2'), not just the ceiling.
- a bare unresolved type id inside a data-only project now carries the
  same Rust-escalation pointer as the namespaced form ('aura nodes new');
  outside any project a bare id stays hint-free (a std-name typo is the
  likelier cause there).

closes #336
refs #341
2026-07-26 11:31:51 +02:00
claude 77ad0465cb fix(aura-research, aura-cli, aura-runner): milestone-fieldtest bugs — the unwired ramp hole + three diagnostics
RED-first fixes for the four M42 A/B findings. campaign introspect
--unwired now drills into a present-but-empty presentation section
exactly as it drills into data, listing persist_taps/emit as open slots
— the miss caused three of the cold start's five dead ends (the process
sibling has no analogous nested-required shape, verified). The override
panic catch wraps known non-diagnostic payloads (capacity overflow) in
domain prose while passing real constructor messages through
byte-identical; a kind-mismatched override renders as prose naming
path, expected and got kinds with a write-this example instead of the
ParamKindMismatch debug struct; chart's unknown-handle refusal names
the current trace surface (exec --tap / presentation.persist_taps)
instead of the retired --trace flag.

closes #319
2026-07-26 00:37:28 +02:00
claude e6b60bf680 fieldtest: milestone-42 A/B — promise holds, cold start 9x slower to first family
Milestone-scope consumer run on the release binary at 2f1bace, three
scenarios: cold start (help-only) reaches a first single run in 5
commands and a first selecting 4-cell family in 28 with 5 dead ends;
the five retired verbs refuse without re-routing; the full retired
research scope (grid, walk-forward, monte-carlo bootstrap, generalize)
runs document-first over two years of GER40+USDJPY in 2.4s with
bit-identical reproduce (21/21). A/B against the sugar-era cycle-300
transcript: comparable to first run, ~9x slower to first family,
strictly better artifact — the dominant cost is an introspect --unwired
bug on present-but-empty presentation (fixed in-cycle, follow-up
commit) plus the missing pre-run coverage view (evidence on the
tracker). 4 bugs / 6 friction / 3 spec-gap / 4 working, routed on the
tracker.

refs #319
2026-07-26 00:21:47 +02:00
claude 2f1baceec7 fix(aura-cli): exec --override refuses out-of-domain values and the wrapped form
RED-first fixes for the two field-test bugs on the new flag's blueprint
leg, both formerly exit-101 panics. An out-of-domain override value now
renders the node constructor's own message as a prose refusal, exit 1 —
the single-run bootstrap rides the same catch mechanism the campaign
path's #272 per-cell containment uses (a campaign-leg pin confirms the
identical value was already contained there, exit 3, unchanged). The
retired wrapped axis form refuses before the params zip with a
did-you-mean naming the raw node.param namespace and the discovery verb,
mirroring the campaign leg's existing prose family.

refs #319
2026-07-26 00:01:20 +02:00
claude 3aa63833f1 fieldtest: 319-sugar-retirement — 4 examples, 2 bugs / 4 friction / 2 spec-gap / 4 working
Source-blind consumer run over the retired-sugar surface on the release
binary at 1476990: the blueprint smoke loop (op-script -> graph build ->
exec --tap/--override) runs and charts; twelve exec target-class probes
and the --override refusal family behave as documented on both legs; a
retired-verb consumer reaches a running 4-member real-data sweep family
through the document ramp; the campaign walk-forward zero-trade note
fires and stays silent correctly. The two bugs are exit-101 panics on
--override's blueprint leg (out-of-domain value; the retired wrapped
axis form) — fixed in-cycle RED-first in the follow-up commit. Friction
and spec-gap findings are bundled on the tracker; the trace-dir
overwrite evidence landed on the standing collision issue.

refs #319
2026-07-25 23:48:51 +02:00
claude 1476990cfd audit: #319 cycle close — architect drift resolved, bench green
Architect review over 9cfe296..dc5f174 (drift_found -> all five items
fixed in this close): C24's family-machinery sentence re-amended to
current truth (synthetic walks are reproduce-only inputs since the
builder retirement; superseded text to the sidecar); the ported
generalize test's doc no longer promises the retired CrossInstrument
family handle and its stale section header is gone;
FamilyKind::CrossInstrument carries its C18 dead-machinery/read-back
note in code; live comments in campaign_run, bench surfaces, and
graph_construct name exec instead of the retired verbs; the five
inlined topology_hash copies in the bin test module consolidated into
one test helper. What holds, per the architect: C24<->code gated-intake
lockstep exact, C12's single-hash claim true in code, exec dispatch
matches C14's amended text.

Bench (regression gate) exit 0, all five fingerprints OK:
engine_throughput bars_per_s 14014049 (+0.1%), ingest_throughput
12935441 (-0.5%), campaign_sweep wall 1.407s (+0.9%), campaign_heavy
wall 5.688s (+2.1%), cli_fixed_cost run_ms 3.6 (+6.6%, spawn-floor
jitter under parallel-agent load, report-only). No baseline moved.
Also rides: the reviewer re-check's two comment nits (classify-once
wording, SIGPIPE example verb).

refs #319
2026-07-25 23:32:18 +02:00
claude dc5f1742f4 fix(aura-cli, aura-runner): review findings — duplicate --override refusal, live discovery prose, neutral JSON refusal
In-cycle fixes for the independent review of the #319 package, RED-first
for both Importants. A repeated --override path now refuses at the shared
lexer (exit 2, path named) instead of panicking exit 101 through
reopen_all — which also un-misattributes the campaign leg's collision
prose for duplicates. override_paths' refusal points at aura graph
introspect --params (the retired --list-axes reference swept from every
live comment, one explicitly-historical mention remains). exec classifies
file targets through a Result-bearing peek: unparseable JSON refuses
neutrally before any leg is chosen. RunPresentation (single-variant)
removed; run_campaign_returning's contract note names its one deliberate
usage-class exit; C14's routing sentence and C20's family-builder
paragraph amended to current truth (superseded text to the history
sidecars); bench error strings and deletion blank-line runs tidied.

refs #319
2026-07-25 23:21:07 +02:00
claude 9eb6d6b4f6 docs(readme, guides, glossary, ledger), bench: the surviving surface everywhere
Slice 8, closing the #319 retirement's prose and bench debt. Bench argv
rides exec (fixed_cost single-run probe, campaign surfaces, and the seed
helper that still called the retired sweep at runtime — caught by the
bench run itself); all five fingerprints unchanged against the committed
baselines, confirming record-line parity through the retirement. Live
docs (README, authoring guide, project layout, glossary) describe only
exec + campaign documents + graph introspect --params; three glossary
entries explicitly mark their verb retired. Ledger amendments per the C29
discipline — C25's executor-verb-set re-settled (exec + the --override
residue), C14's dual grammar reduced to the one dispatch, C24's
document-built runs/families + gated-intake route list (lockstep with the
code comment), C12's override clause and identity anchor repointed to the
runner-layer hash computation, C18/C22/C27/C01 mention rewrites —
superseded sentences moved verbatim to the history sidecars (c25's
created). Stale dual-grammar doc comment on is_blueprint_file rewritten.

refs #319
2026-07-25 22:45:24 +02:00
claude 06d7e0f30a refactor(aura-runner, aura-cli): retire the family builders and the CLI topology_hash duplicate
Slice 7 of the #319 retirement. aura-runner's blueprint_sweep_family /
blueprint_walkforward_family / blueprint_mc_family lost their only
production callers with the quintet and are gone (family.rs 889 -> 199
lines) together with their zero-caller helpers; reproduce's live inputs
(DataSource, showcase/walkforward sources) stay. Coverage was ported
first: member-computation identity + family-shared topology_hash now pin
through a campaign-document twin in tests/exec.rs, reproduce's multi-member
and MC-seed-label branches re-mint their fixtures directly. The CLI-side
topology_hash helper goes with them — its keep-rationale was disproven
(the record line's hash is computed in the runner's member machinery;
minuted on the issue) — and c28_layering's file list drops the stale
verb_sugar entry. The synthetic per-seed MC family remains an intentional
retirement casualty, recorded in the test dispositions.

refs #319
2026-07-25 22:14:24 +02:00
claude db8f947441 feat(aura-cli)!: retire the research-verb sugar — run/sweep/walkforward/mc/generalize removed
Slice 6 of the #319 retirement, the destructive half. The five flag verbs,
their five Args structs and dispatch arms, the whole argv->document
translator (verb_sugar.rs, 1257 lines) and 33 quintet-only helpers are
gone; retired verbs now refuse at the clap layer as unknown commands.
main.rs shrinks 4445 -> 2764 lines. The gated-intake route list narrows to
the canonical layer + exec's blueprint leg; exec's own refusal prose no
longer names dead verbs; test seeding recipes ride graph register.

cli_run.rs walked the full disposition table: 94 tests retired with their
dead flag surface (each with a named green twin where the property
survives), 75 ported — exec ports keep assertions byte-identical, campaign
ports inherit their golden grades unchanged (walkforward's 90/30/30 real
roller and the archive window clipping the sugar applied silently are now
explicit document fields — the equivalence held exactly, one reconstructed
walkforward pooled expectancy_r within 1e-12, cause documented inline).
Surface deltas recorded by the ports: per-cell refusals are #272 contained
faults (exit 3 + warning) rather than hard exit 1; the synthetic per-seed
mc family and generalize's merged cross-instrument family record were
sugar-only constructs and retire with it (the campaign record's
generalizations[] carries the data).

refs #319
2026-07-25 21:36:58 +02:00
claude 24782caaec feat(aura-cli): surviving-surface prose — scaffold template, concepts help, sweep-vehicle tests
Slice 5 of the #319 retirement. The scaffolder's project CLAUDE.md and the
CLI concepts help now teach only the surviving surface (exec over both
document classes, --override, graph introspect --params for discovery;
traces via exec --tap / presentation.persist_taps). Sweep vehicles moved
onto campaign documents driven by exec (project_new's starter quickstarts,
cli_broken_pipe — whose bare-sweep EPIPE probe was latently vacuous and now
streams a real 4-member campaign, project_sweep_campaign's real-data leg,
graph_construct's gang-axis parity pair as paired one-cell campaigns);
pure discovery sites became graph introspect --params, and the line-identity
test kept its literal --params expectations minus the sweep half. Help pins
rewritten; the scaffold template's in-src pin follows the new bytes.

refs #319
2026-07-25 19:34:07 +02:00
claude 5dc8e03249 test(aura-cli): re-target the run-vehicle argv sites onto exec
Slice 4 of the #319 retirement: 34 mechanical argv swaps across six test
files (tap_recording 12, project_load 13, run_measurement 3, measure_ic 1,
run_refuses_unrunnable_blueprint 1, graph_construct 4 run-halves) — every
site drove run as a synthetic vehicle with at most --tap, so no one-cell
campaign conversions were needed; assertions untouched. graph_construct's
sweep sites and the remaining vehicle files stay for the next slice.

refs #319
2026-07-25 19:01:29 +02:00
claude 696d7fe59a feat(aura-cli): exec --override on the campaign leg + zero-trade note migration
Slice 3 of the #319 retirement. The campaign leg's --override injects a
single-value axis over the named bound param into every strategy entry —
after intrinsic validation, before the referential gate, so an unknown
path speaks validate_campaign_refs' existing did-you-mean prose and a
kind mismatch its existing fault; collision with a document-declared axis
refuses (an override overrides a bound value, never an axis, exit 2).
The stored bytes and the recorded campaign_id stay those of the original
document; the audit record of an override is the raw member manifests.
The walk-forward zero-trade aura: note: diagnostic now has a campaign-
path producer call (present_campaign per-cell), so it survives the
quintet's removal at today's strength.

refs #319
2026-07-25 18:56:56 +02:00
claude da19e27b6a feat(aura-cli): exec --override (the #246 residue) + the measurement leg
Slice 2 of the #319 retirement. The blueprint leg now carries both run
shapes: a bias signal via run_signal_r, a no-bias measurement blueprint
(>=1 declared tap) via run_measurement — record lines byte-identical to
run's. --override NODE.PARAM=VALUE (repeatable) is the one deliberate
sugar residue: validated through the shared override_paths prose, the
closed-guard runs on the reopened probe (space == override set), values
bind in reopened param_space order so the manifest records them raw under
params ("what varied"). A measurement blueprint refuses --override
explicitly (its space is not wrap_r-wrapped) rather than dropping it
silently.

refs #319
2026-07-25 18:41:14 +02:00
claude 78b80ec0fd feat(aura-cli): exec — the one executor verb, campaign legs + blueprint single run
The #319 retirement's constructive half, slice 1: a new top-level
`exec <target>` executes a campaign document (file, register-then-run, or
64-hex content id — byte-behaviour of the retired `campaign run`, which no
longer parses) or a fully-bound signal blueprint (synthetic single run,
record line byte-identical to `aura run`'s, root-name gate migrated).
Flag discipline is per-leg: --parallel-instruments on campaign targets,
--tap on blueprint targets (refused on campaign targets with prose).
File routing peeks the document's top-level "kind" key: is_blueprint_file
alone cannot tell two .json classes apart.

--override (the #246 residue) and the measurement leg follow in later
slices; run/sweep/walkforward/mc/generalize stay alive until the removal
slice.

refs #319
2026-07-25 18:29:16 +02:00
claude 9cfe2965c0 feat(aura-cli, aura-runner): JSON data namespace — info verb, NDJSON list, coverage retired
Reshape the aura data namespace around one principle: every verb emits
JSON, no format flag (the headless-agent posture #264 named).

- aura data info <symbol> (new): one flat JSON object — symbol plus the
  six neutral geometry fields (digits, pipSize, tickSize, lotSize,
  baseAsset, quoteAsset) from DataServer::symbol_meta when the
  <SYMBOL>.meta.json sidecar yields geometry; a symbol is known via bar
  files OR sidecar (a sidecar-only symbol still reports its geometry);
  an unknown symbol refuses with prose, exit 1. description stays out
  until the neutral reader exposes it (data-server#4).
- aura data list: NDJSON — one JSON string per line; an empty archive
  emits zero stdout lines, with the human affordance moved to a stderr
  note in the #278 class vocabulary.
- aura data coverage: retired — #272's per-cell fault isolation subsumed
  the prophylactic pre-run check; aura-runner's data_coverage_report and
  its presentation helpers go with it, while interior_gap_months stays
  as the campaign path's single gap walk (#295).

The three derived design decisions (description omitted, broader
refusal prose, empty-archive stderr notice) are minuted on the issue.

closes #273
2026-07-25 12:25:45 +02:00
claude 32eb5a6a9e fieldtest: blueprint-name — 4 examples, 1 bug / 0 friction / 1 spec-gap / 4 working
Source-blind consumer run over the four cycle axes on the release
binary at 4ff85b9: the authoring loop lands the name in bytes, store
document, and traces/<name>/ (with the documented default collision
observable on the unnamed twin); the refusal battery is uniform and
op-indexed across the op route and the file intakes; a named blueprint
defaults its use-splice instance identifier as documented, two unnamed
splices collide, two named ones coexist; help/guide/glossary agree on
eleven ops and the render-name vs registry-label split. Finding B1
(sweep --list-axes ungated, mangled listing at exit 0) is fixed
RED-first in the preceding commit; SG1 (prefix variance in the refusal
prose) is ratified there in C24.

refs #331
2026-07-25 05:15:01 +02:00
claude 564a767974 fix(aura-cli): sweep --list-axes joins the gated intake class
Fieldtest finding B1: the one remaining authored-envelope FILE route,
sweep --list-axes, skipped the root-name gate — a hand-edited
"name":"../escape" listed mangled, non-bindable wrapped axis names
at exit 0 while every sibling intake refused. The discovery surface now
gates before any axis line prints (exit 1, shared prose), restoring
both the #331 every-authored-intake claim and #328's discovery-surface
identity. RED-first: the e2e refusal test was written and observed
failing (exit 0, axis lines printed) before the fix.

Re-enumeration of every read_to_string-fed envelope load confirms the
class is now closed; C24 additionally ratifies the fieldtest's SG1 —
the refusal core sentence is byte-uniform across sites while the
leading context prefix varies by site convention.

refs #331, #328
2026-07-25 05:14:50 +02:00
claude 4ff85b94e5 audit: #331 cycle close — every authored intake gates the root name, cli_fixed_cost re-pinned
Architect drift review (drift_found) resolved fix-path on all three
items. [high] The sweep/walkforward/mc family builders and
validate_and_register_axes read authored envelope files and
put_blueprint'd them ungated — falsifying the C29 exemption's premise
that the store is populated through gated intakes; all four sites now
gate the root name before the family/axis build (placing the gate
after the build produced a misleading unknown-axis error instead of
the gate refusal — RED-verified), with an e2e pin on the synthetic
sweep route. The decision was gate-the-routes, not weaken-the-prose:
the exemption's premise is the invariant, not a wording choice.
[medium] C24 now states the gate rule as the class it is (the op
intake plus every CLI intake reading an authored envelope from a
file; store read-back exempt, C29). [low] C23 reconciles
names-as-debug-symbols with the render name's one operational role
(trace-directory key — never compilation, identity, or execution
semantics).

Ratify: aura-bench cli_fixed_cost baseline re-pinned
(run_line_fnv 6bb0d796f760d140 -> 9bdbc3acf7b2926a). The moved metric
was caused by the #328 tidy (4474814), which switched single-run
manifest defaults from wrapped to raw axis names after that cycle's
bench rerun had already passed; bbac29d and this cycle's HEAD produce
fingerprint-identical record lines (verified with both binaries on
one scratch project), so this cycle only inherits the stale pin. All
five surfaces fingerprint OK after the re-pin.

refs #331, #328
2026-07-25 04:55:12 +02:00
claude 10570b75f8 docs(guide, ledger, glossary): the eleventh op — naming a blueprint from the script
The authoring guide's ops table grows the name row (ten -> eleven) plus
a paragraph on what the graph default costs a multi-strategy project
(indistinguishable store entries, one shared trace directory, colliding
use-splice instance defaults). C24's op-script grammar records the
eleventh verb and the two-seam shape gate with its C29 read-back
exemption; the glossary's blueprint-label entry now separates the
render name (op-settable, keys trace runs and instance defaults) from
the registry label (register --name, explicit, orthogonal).

refs #331
2026-07-25 04:33:25 +02:00
claude a851af993a feat(aura-engine, aura-cli): the blueprint name op — name_gate on every authored intake
An eleventh op-script op {"op":"name","name":"<n>"} sets the composite
render name (engine Op::Name + CLI OpDoc mirror); omitting it keeps the
default "graph" byte-identically. The default leaked everywhere one
research project has more than one strategy: every store entry named
graph, every default tap recording sharing traces/graph/, and two
use-splices of unnamed blueprints colliding on the default instance
identifier — the authored name dissolves all three through existing
mechanics (no downstream site edited).

Mechanics: at-most-once per script (doc-op precedent), position-free
(read only at finish). A shared deterministic name_gate (aura-engine:
non-empty, no path separators, not . or ..) guards every seam where a
name is born from authored data, because the name keys a trace
directory unsanitized: the op intake (GraphSession::set_name) and all
four CLI fresh-file envelope intakes (register, introspect
--content-id FILE, the bare graph FILE viewer, aura run FILE — the run
route reached begin_run(signal.name()) ungated, and introspect
--params FILE rode the same parse). Store read-back (reproduce, use
resolution, introspect/params by content id) stays deliberately
ungated — C29: registered artifacts are never retroactively
invalidated — pinned by a test that plants a bad-root-name blueprint
via the registry API and asserts introspect --params still answers.
Refusal prose is single-sourced (name_gate_fault_prose) so every seam
reads identically, op-indexed on the op route.

Identity semantics: the authored name hashes into the content id (a
named document is a different document) and never into the identity id
(names are stripped as debug symbols, C23) — pinned by a twin test.
The previously untested use-splice instance-name default
(construction.rs) got its ratifying pin before the collision claim
leans on it. The registry label (register --name) stays orthogonal.

Review rounds caught and fixed: the envelope gate initially fired on
store read-back (C29 violation at the introspect surface), and the
run/params fresh-file routes were unenumerated intake seams — closed
with the call-site classification now recorded in the wrapper docs.

Verification: cargo build/test/clippy -D warnings all green (99 test
targets, 0 failures, independently re-run); the new run-route test was
RED-verified by hand-removing the gate.

closes #331
refs #328, #311

Spec: blueprint-name-op (fork minutes on #331)
2026-07-25 04:33:18 +02:00
179 changed files with 10539 additions and 10060 deletions
+27 -28
View File
@@ -31,47 +31,46 @@ Invoke it as `aura <command> …` (examples below use the plain name).
- **Blueprint** — a serialized signal graph as data: a param-generic
`price → bias` node graph. A blueprint file (`blueprint.json`) is the unit a
downstream consumer loads and drives verbs over.
downstream consumer loads and executes or varies.
- **Open vs. closed** — a blueprint with unbound (free) numeric knobs is *open*;
one with every knob bound is *closed*. Different verbs want different states
(see each verb below).
- **Family** — the set of runs one verb produces over a blueprint (a sweep grid,
a Monte-Carlo seed set, a walk-forward window sequence). Families are persisted
in a content-addressed store and can be listed, ranked, and reproduced.
one with every knob bound is *closed*. A plain `exec` wants closed; naming a
knob as a campaign axis (or an `exec --override`) is what reopens it.
- **Family** — the set of runs a campaign document's process pipeline produces
over a blueprint (a sweep grid, a Monte-Carlo seed set, a walk-forward window
sequence). Families are persisted in a content-addressed store and can be
listed, ranked, and reproduced.
- **Axis** — one named, sweepable knob of a blueprint (e.g. `fast.length`),
bound with a comma-separated value list. A **gang** fuses several sibling
knobs into one axis (one value drives all members).
## Running & orchestrating a loaded blueprint
## Executing a document
These verbs all take the **blueprint file as their first positional argument**.
Most drive a family of runs over it; `graph` renders its structure so a mis-wire
is visible before a run. `walkforward`, `mc`, and `generalize` share
`sweep`'s generic grammar — the blueprint positional plus `--real` and a
repeatable `--axis <name>=<csv>` (`generalize` takes `--real <SYM1,SYM2,…>`, at
least two instruments, with a single value per axis) — see `--help`.
`aura exec <target>` (#319) is the one executor verb: `<target>` is either a
loaded **blueprint** file (a single synthetic run) or a **campaign document**
(a `.json` file or its registered 64-hex content id — instruments × windows ×
strategy × param axes × process, see `docs/authoring-guide.md` §3). `graph`
renders a blueprint's structure so a mis-wire is visible before a run.
| Command | Purpose |
|---|---|
| `aura run <bp.json>` | Run one backtest of a **closed** blueprint and print its report. An open (free-knob) blueprint is refused with a clean error — bind it, or use `sweep`. |
| `aura exec <bp.json>` | Run one backtest of a **closed** blueprint and print its report. An open (free-knob) blueprint is refused with a clean error — bind it, or vary it as a campaign axis. |
| `aura exec <bp.json> --override <node.param>=<value>` | Reopen one **bound** param for this single execution only (the value is recorded raw in the manifest); repeatable. |
| `aura exec <bp.json> --tap <name>=<fold>` | Subscribe a declared tap to a fold (`record`/`count`/`sum`/`mean`/`min`/`max`/`first`/`last`) for this run; repeatable. |
| `aura graph <bp.json>` | **Render** the blueprint's structure as an interactive HTML DAG so a mis-wire is visible before a run. Omit the file to render the built-in sample; a named-but-unreadable file is a usage error. |
| `aura sweep <bp.json> --list-axes` | **Discover** the blueprint's open, sweepable knobs. Prints each as `<name>:<kind>`. Run this first to learn the axis names. |
| `aura sweep <bp.json> --axis <name>=<csv> [--axis …]` | Run a **grid family** over the named axes and persist it. |
| `aura mc <bp.json> --seeds <n>` | Run a **synthetic Monte-Carlo family** of `n` seeded realizations. Wants a **closed** blueprint (the inverse of sweep). |
| `aura mc <bp.json> --real <sym> --axis <name>=<csv> [--axis …]` | Run a **Monte-Carlo R-bootstrap campaign** over recorded data — driven through the same generated-campaign pipeline as `sweep`; `--block-len`/`--resamples`/`--seed` tune the bootstrap. |
| `aura walkforward <bp.json> --axis <name>=<csv> [--axis …]` | Run an **in-sample-refit walk-forward family** — rolling optimize + out-of-sample test across windows, stitched into one verdict + parameter stability. `--select` chooses the per-window objective. |
| `aura graph introspect --params <bp.json\|id>` | **Discover** a blueprint's open, sweepable knobs. Prints each as `<name>:<kind>`, bound params trailing `default=<value>`. Run this first to learn the axis names for a campaign document. |
| `aura exec <campaign.json\|id> [--parallel-instruments <n>]` | Execute a **campaign document** — a grid family, gates, walk-forward, Monte-Carlo, and cross-instrument generalization per its process document, one cell per (strategy, instrument, window). |
| `aura runs families` | List every persisted family (id, kind, member count). |
| `aura runs family <id> [rank <metric>]` | List one family's members, optionally ranked best-first by an R metric (e.g. `sqn_normalized`, `expectancy_r`). |
| `aura reproduce <family-id>` | Re-derive every member of a persisted family from the content-addressed store and check it is bit-identical (the C18/C1 determinism guarantee). |
**Important contract — every open knob is mandatory.** On `sweep` and
`walkforward`, the knobs enumerated by `--list-axes` are *all* required: you must
supply an `--axis` for each open knob. There is no default value — pin a knob you
don't want to vary with a single-value axis (`--axis name=4`). Omitting one is a
clean error naming the missing knob, not a silent default.
**Important contract — every open knob is mandatory on a campaign axis.** A
campaign document's `strategies[].axes` must name every open knob
`graph introspect --params` lists: there is no default value — pin a knob you
don't want to vary with a single-value axis (`{"kind":"I64","values":[4]}`).
Omitting one is a clean error naming the missing knob, not a silent default.
Use `aura <cmd> --help` for the full data-window (`--real`/`--from`/`--to`),
naming (`--name`), and selection (`--select`) flags.
Use `aura exec --help` for the exact flag grammar, `docs/authoring-guide.md`
for the campaign document shape (data window, naming, presentation/emit).
## Authoring & introspecting topology
@@ -111,11 +110,11 @@ Each element is one op, tagged by `"op"`. Node params are bound with the **typed
```
Piping this document into `aura graph build` emits a `blueprint.json` you can
then feed to `run` / `sweep` / `mc` / `walkforward` above:
then feed to `exec` above:
```sh
aura graph build < crossover.ops.json > crossover.bp.json
aura sweep crossover.bp.json --list-axes
aura graph introspect --params crossover.bp.json
```
The op kinds are `source`, `input`, `add`, `feed`, `connect`, `expose`, `tap`,
@@ -1,16 +1,16 @@
{
"surface": "cli_fixed_cost",
"metrics": {
"help_ms": 1.5046899999999999,
"run_ms": 3.63305
"help_ms": 1.487171,
"run_ms": 3.376192
},
"fingerprint": "run_line_fnv=6bb0d796f760d140",
"fingerprint": "run_line_fnv=9bdbc3acf7b2926a",
"reps": 3,
"host": {
"hostname": "Raki",
"nproc": 24
},
"profile": "release",
"commit": "9c7f60b",
"date": "2026-07-23"
"commit": "10570b7",
"date": "2026-07-25"
}
+7 -20
View File
@@ -95,31 +95,18 @@ fn blueprint_stems(dir: &Path) -> Vec<String> {
.unwrap_or_default()
}
/// Seed one blueprint into the scratch store via a tiny synthetic sweep (the
/// E2E `seed_blueprint` pattern) and return its content id.
/// Seed one blueprint into the scratch store via `graph register` (#319: the
/// surviving registration surface) and return its content id.
fn seed_blueprint(bin: &Path, dir: &Path, file: &str, name: &str) -> Result<String, String> {
let before = blueprint_stems(dir);
let (out, code) = run_in(
bin,
dir,
&[
"sweep",
file,
"--axis",
"fast.length=2,4",
"--axis",
"slow.length=8,16",
"--name",
name,
],
)?;
let (out, code) = run_in(bin, dir, &["graph", "register", file, "--name", name])?;
if code != Some(0) {
return Err(format!("seed sweep failed ({code:?}): {out}"));
return Err(format!("seed register failed ({code:?}): {out}"));
}
blueprint_stems(dir)
.into_iter()
.find(|s| !before.contains(s))
.ok_or_else(|| "seed sweep must store a new blueprint".to_string())
.ok_or_else(|| "seed register must store a new blueprint".to_string())
}
fn register_process(bin: &Path, dir: &Path, file: &str, doc: &str) -> Result<String, String> {
@@ -261,10 +248,10 @@ pub fn winner_fingerprint(stdout: &str) -> Result<String, String> {
fn campaign_rep(bin: &Path, sizing: Sizing, process_doc: &str) -> Result<RepOutcome, String> {
let (scratch, doc) = build_scratch(bin, sizing, process_doc)?;
let timed: TimedChild = run_timed(bin, &["campaign", "run", &doc], &scratch.0)?;
let timed: TimedChild = run_timed(bin, &["exec", &doc], &scratch.0)?;
if timed.exit != Some(0) {
return Err(format!(
"campaign run exited {:?}\nstdout: {}\nstderr: {}",
"aura exec exited {:?}\nstdout: {}\nstderr: {}",
timed.exit, timed.stdout, timed.stderr
));
}
+6 -6
View File
@@ -1,6 +1,6 @@
//! CLI fixed cost: the spawn floor (`aura --help`, no fingerprint — nothing is
//! computed) and a minimal data-only project run whose fingerprint is the
//! FNV-1a hash of the run's single stdout JSON line. Fresh scratch project per
//! computed) and a minimal data-only project exec whose fingerprint is the
//! FNV-1a hash of the exec's single stdout JSON line. Fresh scratch project per
//! repetition, so store counters start identical.
use super::{median, RepOutcome};
@@ -11,7 +11,7 @@ use std::collections::BTreeMap;
use std::path::Path;
use std::time::Instant;
/// Fingerprint of an `aura run` record line, invariant across rebuilds: the
/// Fingerprint of an `aura exec` record line, invariant across rebuilds: the
/// line's `manifest.commit` is the aura binary's compile-time build sha
/// (crates/aura-cli/build.rs — the invariant-8 audit trail), so hashing the
/// raw line would flip the fingerprint on EVERY new commit and report a
@@ -59,9 +59,9 @@ pub fn rep(bin: &Path) -> Result<RepOutcome, String> {
let mut run_walls = Vec::new();
let mut first_line = None;
for _ in 0..RUN_SAMPLES {
let timed = run_timed(bin, &["run", "bench_sma_a.json"], &scratch.0)?;
let timed = run_timed(bin, &["exec", "bench_sma_a.json"], &scratch.0)?;
if timed.exit != Some(0) {
return Err(format!("aura run exited {:?}: {}", timed.exit, timed.stdout));
return Err(format!("aura exec exited {:?}: {}", timed.exit, timed.stdout));
}
run_walls.push(timed.wall_s * 1000.0);
if first_line.is_none() {
@@ -70,7 +70,7 @@ pub fn rep(bin: &Path) -> Result<RepOutcome, String> {
.stdout
.lines()
.next()
.ok_or("aura run must print its JSON record line")?
.ok_or("aura exec must print its JSON record line")?
.to_string(),
);
}
+2 -2
View File
@@ -105,7 +105,7 @@ mod tests {
/// `run_reps` composed with the real campaign sweep surface — a fresh
/// scratch project, a freshly seeded blueprint, and a genuine
/// `aura campaign run` child process spawned per repetition (not an
/// `aura exec` child process spawned per repetition (not an
/// in-process shortcut). Protects that two independently built-and-run
/// scratch campaigns agree on the winner-ordinal fingerprint extracted
/// from the child's stdout — C1's determinism carried through the
@@ -136,7 +136,7 @@ mod tests {
/// `run_reps` composed with the real CLI fixed-cost surface — a genuine
/// `aura --help` spawn-floor wall plus a fresh scratch project and a real
/// `aura run` child process per repetition (not an in-process shortcut).
/// `aura exec` child process per repetition (not an in-process shortcut).
/// Protects that two independent scratch runs agree on the run-line FNV
/// fingerprint extracted from the child's stdout, and that the metrics
/// map carries the wall-clock keys the fixed_cost `BaselineDoc` needs.
+101 -55
View File
@@ -1,4 +1,5 @@
//! `aura campaign run` — the driver that turns a stored campaign document into
//! `aura exec`'s campaign legs (#319; formerly `aura campaign run`) — the
//! driver that turns a stored campaign document into
//! a realized run-set (#198). The execution *semantics* (preflight, cell loop,
//! stage sequencing, selection, registry writes) live in `aura-campaign`; the
//! [`MemberRunner`](aura_campaign::MemberRunner) implementation over the shipped loaded-blueprint machinery
@@ -27,7 +28,7 @@ use aura_engine::FamilySelection;
use aura_registry::CampaignRunRecord;
use aura_research::{
campaign_to_json, content_id_of, parse_campaign, parse_process, validate_campaign,
validate_process, CampaignDoc, DocRef,
validate_process, Axis, CampaignDoc, DocRef,
};
use aura_runner::axes::is_content_id;
@@ -108,27 +109,17 @@ struct CampaignRunLine<'a> {
campaign_run: &'a CampaignRunRecord,
}
/// Stdout shape of a campaign run. `Full` is `aura campaign run` (emit-gated
/// member/selection lines + the always-on final `campaign_run` record line).
/// `MemberLinesOnly` is dissolved-verb sugar: the generated document's
/// `emit: ["family_table"]` already limits emission to member lines; the mode
/// additionally suppresses the final record line so the verb's stdout
/// contract is reproduced byte-for-byte. The record append is identical in
/// both modes — presentation changes, the record does not.
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum RunPresentation {
Full,
MemberLinesOnly,
}
/// `aura campaign run <target>`: resolve, gate, execute, emit. Every `Err`
/// `aura exec <target>` (campaign legs): resolve, gate, execute, emit. Every `Err`
/// is a refusal `campaign_cmd` renders as `aura: {msg}` + exit 1; an `Ok`
/// carries the failed-cell count (#272) so the caller can exit 3 on a
/// completed-with-failures run via `exit_on_campaign_result`.
/// completed-with-failures run via `exit_on_campaign_result`. `overrides`
/// (#319 Task 4, the `exec --override` campaign leg) is threaded through
/// unchanged to `run_campaign_returning`, the one injection seam.
pub(crate) fn run_campaign(
target: &str,
env: &Env,
parallel_instruments: NonZeroUsize,
overrides: &[(String, Scalar)],
) -> Result<usize, String> {
// Project gate FIRST: nothing (not even the file-sugar registration)
// touches a store outside a project.
@@ -162,7 +153,7 @@ pub(crate) fn run_campaign(
));
};
run_campaign_by_id(&campaign_id, env, RunPresentation::Full, parallel_instruments)
run_campaign_by_id(&campaign_id, env, parallel_instruments, overrides)
}
/// An executed campaign plus the context its presenter and the dissolved-verb
@@ -181,20 +172,15 @@ pub(crate) struct CampaignRun {
/// The one campaign executor path from a resolved content id onward: fetch
/// the stored canonical bytes by id (so file addressing and id addressing
/// produce the same realization by construction) and re-run the intrinsic
/// tier on them, execute, and emit per `presentation`. Shared by
/// `run_campaign` (`RunPresentation::Full`) and the dissolved-verb sugar path
/// (`verb_sugar::run_sweep_sugar`, `RunPresentation::MemberLinesOnly`) — no
/// project gate here: the sugar path must work project-less exactly as the
/// inline verb it replaces did (store mechanics run against `env.registry()`
/// in both cases).
/// tier on them, execute, and emit. Called by `run_campaign`.
pub(crate) fn run_campaign_by_id(
campaign_id: &str,
env: &Env,
presentation: RunPresentation,
parallel_instruments: NonZeroUsize,
overrides: &[(String, Scalar)],
) -> Result<usize, String> {
let run = run_campaign_returning(campaign_id, env, parallel_instruments)?;
present_campaign(run, presentation, env)
let run = run_campaign_returning(campaign_id, env, parallel_instruments, overrides)?;
present_campaign(run, env)
}
/// The one campaign executor path from a resolved content id up to (not
@@ -204,18 +190,32 @@ pub(crate) fn run_campaign_by_id(
/// outcome bundled with the context the presenter needs. Shared by
/// `run_campaign_by_id` (which then presents) and a forthcoming dissolved-verb
/// sugar path that reads the outcome and self-prints (lands in a later task,
/// alongside the generalize translator's own runner).
/// alongside the generalize translator's own runner). `overrides` (#319
/// Task 4) is injected into the parsed document AFTER intrinsic validation
/// and BEFORE the referential gate — the stored bytes and the recorded
/// `campaign_id` stay those of the ORIGINAL document; only the executed
/// run-set changes, its audit record being the raw member manifests.
///
/// One deliberate exception to "every refusal is an `Err`" (review Minor-4):
/// the injection loop's axis-collision check exits directly with code 2
/// instead of returning `Err`. That collision is a USAGE-class fault (C14) —
/// `--override` naming a path the document already declares as an axis is a
/// malformed invocation, exactly like `--override`'s own malformed-token
/// refusal (`parse_override_tokens`, exit 2) — not a RUNTIME-class refusal
/// (missing environment/recorded state, exit 1), which is what this fn's
/// `Result<_, String>` return otherwise carries end to end.
pub(crate) fn run_campaign_returning(
campaign_id: &str,
env: &Env,
parallel_instruments: NonZeroUsize,
overrides: &[(String, Scalar)],
) -> Result<CampaignRun, String> {
let registry = env.registry();
let campaign_text = registry
.get_campaign(campaign_id)
.map_err(|e| e.to_string())?
.ok_or_else(|| format!("no campaign {campaign_id} in the project store"))?;
let campaign = parse_campaign(&campaign_text)
let mut campaign = parse_campaign(&campaign_text)
.map_err(|e| doc_error_prose("stored campaign document", &e))?;
let faults = validate_campaign(&campaign);
if !faults.is_empty() {
@@ -225,6 +225,25 @@ pub(crate) fn run_campaign_returning(
));
}
// --override NODE.PARAM=VALUE (#246 residue, #319 Task 4): inject as a
// single-value axis over the named bound param into EVERY strategy
// entry. Collision with a document-declared axis refuses (an override
// overrides a bound value, never an axis); an unknown path then falls
// out of `validate_campaign_refs` below with its existing did-you-mean
// prose — no `RefFault` change, no registry change.
for (path, val) in overrides {
for entry in &mut campaign.strategies {
if entry.axes.contains_key(path) {
eprintln!(
"aura: exec: --override `{path}` collides with a declared axis \
of the campaign; an override overrides a bound value, never an axis"
);
std::process::exit(2);
}
entry.axes.insert(path.clone(), single_value_axis(*val));
}
}
// Referential gate: zero faults or refuse (the campaign-validate seam).
let resolve = |t: &str| env.resolve(t);
let ref_faults = registry
@@ -295,15 +314,11 @@ pub(crate) fn run_campaign_returning(
Ok(CampaignRun { outcome, campaign, strategies, server: runner.server() })
}
/// Emit an executed campaign's results per `presentation`: the zero-survivor
/// stderr notes, the emit-gated family/selection lines, the always-on record
/// line (Full only), then trace persistence. Split out of `run_campaign_by_id`
/// so the dissolved-verb sugar can consume the outcome without this stdout tail.
fn present_campaign(
run: CampaignRun,
presentation: RunPresentation,
env: &Env,
) -> Result<usize, String> {
/// Emit an executed campaign's results: the zero-survivor stderr notes, the
/// emit-gated family/selection lines, the always-on record line, then trace
/// persistence. Split out of `run_campaign_by_id` so a forthcoming caller
/// could consume the outcome without this stdout tail.
fn present_campaign(run: CampaignRun, env: &Env) -> Result<usize, String> {
let CampaignRun { outcome, campaign, strategies, server } = run;
// Zero-survivor stderr notes (exit stays 0 — a null result is a valid
@@ -355,6 +370,32 @@ fn present_campaign(
let emit_family = campaign.presentation.emit.iter().any(|e| e == "family_table");
let emit_selection = campaign.presentation.emit.iter().any(|e| e == "selection_report");
for cell_out in &outcome.cells {
// #313 zero-trade note (#319 Task 5, migrated from the retiring
// `dispatch_walkforward`/`run_walkforward_sugar` paths): every campaign
// cell's `std::walk_forward` family, if any, gets the same shared
// `note_zero_trade_windows` producer over its OOS members' trade
// counts — independent of `emit` (a stderr diagnostic, not gated
// stdout presentation).
if let Some(wf) = cell_out.families.iter().find(|f| f.block == "std::walk_forward") {
crate::diag::note_zero_trade_windows(
wf.reports.iter().map(|r| r.metrics.r.as_ref().map_or(0, |m| m.n_trades)),
);
} else if !cell_out.families.is_empty() {
// #324 (re-scoped): the non-walk-forward sibling — a cell whose
// families are ALL non-`std::walk_forward` (grid sweep, MC
// bootstrap, …) has no OOS-window vocabulary, so without a note
// here a cell that never traded is indistinguishable from a
// break-even one. Over every report across every family this
// cell ran (a cell with zero families, e.g. gate-truncated,
// emits nothing new here).
let trades: Vec<u64> = cell_out
.families
.iter()
.flat_map(|f| f.reports.iter())
.map(|r| r.metrics.r.as_ref().map_or(0, |m| m.n_trades))
.collect();
crate::diag::note_zero_trade_cell(trades.into_iter());
}
if emit_family {
for fam in &cell_out.families {
for report in &fam.reports {
@@ -381,23 +422,21 @@ fn present_campaign(
}
}
}
if presentation == RunPresentation::Full {
println!(
"{}",
serde_json::to_string(&CampaignRunLine { campaign_run: &outcome.record })
.expect("campaign run record serializes")
);
eprintln!(
"aura: campaign run {} recorded: {} cells{}",
outcome.record.run,
outcome.record.cells.len(),
if failed == 0 {
String::new()
} else {
format!(", {failed} failed ({})", fail_labels.join(", "))
}
);
}
println!(
"{}",
serde_json::to_string(&CampaignRunLine { campaign_run: &outcome.record })
.expect("campaign run record serializes")
);
eprintln!(
"aura: campaign run {} recorded: {} cells{}",
outcome.record.run,
outcome.record.cells.len(),
if failed == 0 {
String::new()
} else {
format!(", {failed} failed ({})", fail_labels.join(", "))
}
);
// Trace persistence runs LAST, after the always-on record line (0109):
// stdout stays data-pure (the record line is the wire contract) and the
@@ -417,6 +456,13 @@ fn present_campaign(
Ok(failed)
}
/// Build the single-value `Axis` an `exec --override` injects (#319 Task 4):
/// exactly the shape the document form `{"kind": "I64", "values": [v]}`
/// deserializes to — `Scalar::kind` names the tag, one value in the list.
fn single_value_axis(v: Scalar) -> Axis {
Axis { kind: v.kind(), values: vec![v] }
}
/// #272: the fault-kind label the failed-cell summary line names — the same
/// closed `CellFaultKind` vocabulary an aggregate over `campaign_runs.jsonl`
/// counts by. Debug-leak-free (a fixed lowercase-snake label, not a Debug
+67 -6
View File
@@ -32,11 +32,11 @@ fn zero_trade_note_text(n_windows: usize) -> String {
}
}
/// The #313 zero-trade note, shared by both walk-forward paths (the
/// synthetic-blueprint path in `main.rs` and the `--real` sugar path in
/// `verb_sugar.rs`) so the wording AND the condition live in exactly one
/// place. Takes the per-window trade counts; a no-op unless there is at
/// least one window and every one of them traded zero times.
/// The #313 zero-trade note (#319 Task 5: migrated onto the campaign
/// walk-forward leg in `campaign_run.rs`, the surviving executor path) so
/// the wording AND the condition live in exactly one place. Takes the
/// per-window trade counts; a no-op unless there is at least one window and
/// every one of them traded zero times.
pub(crate) fn note_zero_trade_windows(mut window_trades: impl ExactSizeIterator<Item = u64>) {
let n_windows = window_trades.len();
if n_windows > 0 && window_trades.all(|n| n == 0) {
@@ -44,9 +44,54 @@ pub(crate) fn note_zero_trade_windows(mut window_trades: impl ExactSizeIterator<
}
}
/// #324 (re-scoped): the non-walk-forward sibling of `note_zero_trade_windows`
/// — a campaign cell whose families are ALL non-walk-forward (grid sweep, MC
/// bootstrap, …) has no per-window vocabulary to note through, yet a
/// consumer still cannot tell "0.0 = break-even" from "0.0 = never traded"
/// without this. Pure over the count so the wording is unit-pinned (mirrors
/// `zero_trade_note_text`'s singular/plural split, #313).
fn zero_trade_cell_note_text(n_reports: usize) -> String {
if n_reports == 1 {
"the cell's one report recorded zero trades; its metrics are vacuous, not a break-even \
result"
.to_string()
} else {
format!(
"the cell's {n_reports} reports all recorded zero trades; its metrics are vacuous, \
not a break-even result"
)
}
}
/// See `zero_trade_cell_note_text`. Takes the per-report trade counts across
/// every family the cell ran; a no-op unless there is at least one report
/// and every one of them traded zero times. Called from the campaign cell
/// loop's non-walk-forward branch (`campaign_run.rs`), independent of `emit`.
pub(crate) fn note_zero_trade_cell(mut report_trades: impl ExactSizeIterator<Item = u64>) {
let n_reports = report_trades.len();
if n_reports > 0 && report_trades.all(|n| n == 0) {
note!("{}", zero_trade_cell_note_text(n_reports));
}
}
/// #324 (comment 4501): `exec <blueprint.json>`'s single-run signal/bias leg
/// always drives the built-in synthetic stream (`RunData::Synthetic`, no
/// direct-blueprint exec ever binds real data — only a campaign document's
/// `data` section does) — a tiny `n_cycles`-bar smoke fixture, not market
/// data. Zero trades on it is expected, not a broken strategy; a caller that
/// validates against it needs telling so. Unconditional — the caller gates
/// on the zero-trade condition before calling.
pub(crate) fn note_synthetic_smoke_zero_trades(n_cycles: usize) {
note!(
"the built-in synthetic stream ({n_cycles} cycles) is a small smoke fixture; zero \
trades is expected here — bind real data via a campaign document's `data` section \
(`aura exec <campaign.json>`) for a meaningful run"
);
}
#[cfg(test)]
mod tests {
use super::zero_trade_note_text;
use super::{zero_trade_cell_note_text, zero_trade_note_text};
#[test]
/// #278 fieldtest friction: the single-window form must read as
@@ -62,4 +107,20 @@ mod tests {
"all 3 walk-forward windows recorded zero trades"
);
}
#[test]
/// #324: the cell-level sibling pluralizes the same way and names the
/// vacuous-metrics property, not just the raw zero count.
fn zero_trade_cell_note_pluralizes() {
assert_eq!(
zero_trade_cell_note_text(1),
"the cell's one report recorded zero trades; its metrics are vacuous, not a \
break-even result"
);
assert_eq!(
zero_trade_cell_note_text(4),
"the cell's 4 reports all recorded zero trades; its metrics are vacuous, not a \
break-even result"
);
}
}
+320 -112
View File
@@ -8,9 +8,9 @@ use std::collections::BTreeMap;
use std::path::Path;
use aura_engine::{
blueprint_from_json, blueprint_identity_json, blueprint_to_json, replay, ArgOpError,
BindOpError, BlueprintDoc, CompileError, Composite, GraphSession, LoadError, Op, OpError,
Scalar, ScalarKind,
blueprint_from_json, blueprint_identity_json, blueprint_to_json, name_gate, replay, ArgOpError,
BindOpError, BlueprintDoc, CompileError, Composite, GraphSession, LoadError, NameGateFault, Op,
OpError, Scalar, ScalarKind,
};
use aura_runner::runner::render_value;
use serde::Deserialize;
@@ -22,9 +22,10 @@ use crate::research_docs::resolve_id_prefix;
#[cfg(test)]
use aura_vocabulary::std_vocabulary;
/// The op-list reference `aura graph build --help` appends (#323): the ten
/// op kinds with their fields and one worked element each. Lives beside
/// [`OpDoc`] so a new op variant is one screen away from its help line.
/// The op-list reference `aura graph build --help` appends (#323): the eleven
/// op kinds with their fields and one worked element each (#331: `name` joins
/// 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):
{"op":"source","role":"price","kind":"F64"}
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}}}
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>"#;
@@ -111,6 +115,9 @@ enum OpDoc {
#[serde(default)]
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
@@ -143,21 +150,23 @@ impl OpDoc {
OpDoc::Doc { .. } => "doc".to_string(),
OpDoc::Use { name: Some(n), .. } => format!("use {n:?}"),
OpDoc::Use { name: None, .. } => "use".to_string(),
OpDoc::Name { .. } => "name".to_string(),
}
}
}
impl From<OpDoc> for Op {
/// Infallible, context-free conversion — used directly only by
/// build-free introspection paths (`introspect --unwired`, #317's
/// spec: "Build-free introspection paths pass a `|_| None` closure"),
/// which never resolve a `use` ref through the registry. A bare
/// `OpDoc::Use` therefore maps its `UseRef` payload verbatim into
/// `ref_id` (unresolved) — that session's `subgraph` closure is always
/// `&|_| None`, so any `use` op there faults `UnknownSubgraph`
/// regardless of the exact `ref_id` text; `graph build`'s real path
/// (`composite_from_str`) never reaches this arm — it resolves and
/// replaces each `Op::Use` before conversion (see `resolve_use_op`).
/// Infallible, context-free conversion. Neither production caller
/// (`composite_from_str`'s / `introspect_unwired`'s shared
/// [`parse_and_resolve_ops`] phase, #339 item 4 harvest) reaches the
/// `OpDoc::Use` arm below with an unresolved ref: both intercept every
/// `Op::Use` op and route it through `resolve_use_op` (registry fetch +
/// C29 gate + full-vocabulary resolve) BEFORE ever calling `Op::from` —
/// only the remaining, `use`-free op kinds reach this conversion as-is.
/// The `OpDoc::Use` arm stays for match exhaustiveness (a bare,
/// registry-free conversion, `ref_id` verbatim off the `UseRef`); a
/// hypothetical direct caller would still get one, but every op-script
/// entry point in this file resolves first.
fn from(d: OpDoc) -> Op {
match d {
OpDoc::Source { role, kind } => Op::Source { role, kind },
@@ -182,6 +191,7 @@ impl From<OpDoc> for Op {
name,
bind: bind.into_iter().collect(),
},
OpDoc::Name { name } => Op::Name { name },
}
}
}
@@ -246,20 +256,42 @@ fn format_op_error(e: &OpError) -> String {
format!("gang: `{node}.{param}` is already ganged")
}
OpError::GangArity { gang } => format!("gang `{gang}`: needs at least two members"),
OpError::GangOfSplicedInstance { node, member } => format!(
"gang: {node}.{member} is a spliced instance's member — ganging a used instance's member params is not yet supported"
),
OpError::Incomplete(ce) => format!("{ce:?}"),
OpError::DuplicateDoc => "a doc op may appear at most once".to_string(),
// #317: `graph build`'s real path (`composite_from_str`) resolves and
// fetches every `use` op before replay, so this never fires there —
// but `introspect --unwired` stays build-free/subgraph-free by spec
// (`&|_| None`, see `From<OpDoc> for Op`), so a `use` op in a partial
// document reaches this arm through THAT path, by-identifier on the
// (unresolved) `ref_id` text.
// #317, extended #339 item 4 harvest: BOTH CLI entry points
// (`composite_from_str`'s and `introspect_unwired`'s shared
// `parse_and_resolve_ops` phase) resolve and fetch every `use` op
// before replay/apply, so neither ever constructs an `Op::Use` whose
// `ref_id` is anything but an already-resolved content id — this
// arm is unreachable from the CLI surface. It stays reachable only
// against a bare `GraphSession`/`replay` caller that hands `Op::Use`
// a `ref_id` its own `subgraph` closure doesn't resolve (an engine-
// level API misuse, not a document-authoring fault).
OpError::UnknownSubgraph { 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):
/// 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
@@ -383,14 +415,43 @@ fn resolve_use_op(
Ok(Op::Use { ref_id: full_id, name, bind: bind.into_iter().collect() })
}
/// Parse a JSON op-list document and replay it through the env's vocabulary into
/// a built `Composite` — or a `op N (kind): cause` message (a per-op fault,
/// attributed by the retained op-kind list) / `finalize: cause` (a holistic fault
/// past the last op). Every `use` op resolves through the registry HERE, before
/// replay (#317): a resolution/doc-gate fault is attributed exactly like any
/// other per-op construction fault (same `op N (kind): cause` shape, exit 1).
fn composite_from_str(doc: &str, env: &aura_runner::project::Env) -> Result<Composite, String> {
let docs: Vec<OpDoc> = serde_json::from_str(doc).map_err(|e| format!("invalid op-list: {e}"))?;
/// [`parse_and_resolve_ops`]'s success payload: the parsed `Op`s, their
/// by-index kind labels (`op N (kind): cause` attribution), and the id -> json
/// cache the caller's own `subgraph` closure reads.
type ResolvedOps = (Vec<Op>, Vec<String>, std::collections::HashMap<String, String>);
/// Parse a JSON op-list document into `Op`s, resolving every `use` op through
/// the registry (fetch, C29-gate, full-vocabulary resolve) HERE, before either
/// caller ever builds a session (#317, extended #339 item 4 harvest): the
/// shared first phase of `composite_from_str` (`graph build`'s real path) AND
/// `introspect_unwired`'s build-free path, so a `use`-bearing document
/// resolves identically under both — introspection no longer treats a
/// resolvable ref as a hard miss. A resolution/doc-gate fault is attributed
/// exactly like any other per-op construction fault (same `op N (kind):
/// cause` shape).
fn parse_and_resolve_ops(doc: &str, env: &aura_runner::project::Env) -> Result<ResolvedOps, String> {
// #336: deserialize element-by-element off the untyped `Value` array, not in
// one `Vec<OpDoc>` shot. For an internally-tagged enum, serde_json's
// `deny_unknown_fields` fault fires only once the parser has consumed the
// WHOLE offending object — its reported line/column lands on the start of
// the FOLLOWING element, misattributing the fault in longer op-lists. The
// op-list's own index is immune to that token-position quirk, so every
// per-element parse fault is wrapped with it directly.
let raw: Vec<serde_json::Value> =
serde_json::from_str(doc).map_err(|e| format!("invalid op-list: {e}"))?;
let mut docs: Vec<OpDoc> = Vec::with_capacity(raw.len());
for (idx, v) in raw.into_iter().enumerate() {
// Read the op-kind tag straight off the still-untyped value: a parse
// fault (e.g. an unknown key) means `OpDoc` never materializes, so
// `OpDoc::kind_label` isn't available yet, but the raw "op" string is
// the same lowercase label it would produce for every kind but `use`.
let kind = v.get("op").and_then(serde_json::Value::as_str).map(str::to_string);
let parsed: OpDoc = serde_json::from_value(v).map_err(|e| match &kind {
Some(k) => format!("op {idx} ({k}): {e}"),
None => format!("op {idx}: {e}"),
})?;
docs.push(parsed);
}
let labels: Vec<String> = docs.iter().map(OpDoc::kind_label).collect();
let mut cache: std::collections::HashMap<String, String> = std::collections::HashMap::new();
let mut ops: Vec<Op> = Vec::with_capacity(docs.len());
@@ -404,6 +465,15 @@ fn composite_from_str(doc: &str, env: &aura_runner::project::Env) -> Result<Comp
};
ops.push(op);
}
Ok((ops, labels, cache))
}
/// Parse a JSON op-list document and replay it through the env's vocabulary into
/// a built `Composite` — or a `op N (kind): cause` message (a per-op fault,
/// attributed by the retained op-kind list) / `finalize: cause` (a holistic fault
/// past the last op).
fn composite_from_str(doc: &str, env: &aura_runner::project::Env) -> Result<Composite, String> {
let (ops, labels, cache) = parse_and_resolve_ops(doc, env)?;
// The injected `subgraph` lookup (#317): a pure, registry-free map read —
// every `use` op's blueprint was already fetched, gated, AND resolved
// (`resolve_use_op`'s eager `blueprint_from_json` check) above; this
@@ -481,7 +551,15 @@ pub fn introspect_node(type_id: &str, env: &aura_runner::project::Env) -> Result
out.push_str(&format!(" arg {}: {:?} ({})\n", spec.name, spec.kind, spec.kind.hint()));
}
if builder.is_pending() {
out.push_str(" note ports and params form at construction; args are required\n");
// #341 item 3 harvest: name the follow-up moves on the CURRENT
// surface, not just the fact that ports/params are absent — a
// consumer's first port guess is otherwise a coin flip, and only
// trial refusals surface the real names.
out.push_str(
" note ports and params form at construction; args are required — build \
the op-script, then `aura graph introspect --params <bp.json>` shows the open knobs and \
`--unwired` on a partial document shows the unfilled slots\n",
);
}
for port in &schema.inputs {
out.push_str(&format!(" in {}:{:?}\n", port.name, port.kind));
@@ -496,17 +574,29 @@ pub fn introspect_node(type_id: &str, env: &aura_runner::project::Env) -> Result
}
/// `aura graph introspect --unwired`: the still-open interior slots of a partial
/// op-list document, by-identifier (applies the ops, does NOT finalize).
/// op-list document, by-identifier (applies the ops, does NOT finalize). A
/// `use` op resolves through the registry via the SAME [`parse_and_resolve_ops`]
/// phase `composite_from_str` uses (#339 item 4 harvest: this path used to pass
/// `&|_| None` as the subgraph resolver, so a use-bearing document always
/// missed with `OpError::UnknownSubgraph` — mislabeling a by-name ref as a
/// "content id" in the bargain); the real store resolver threads in cleanly
/// because `unwired()` is read straight off the (unfinished) session,
/// unaffected by the `use` resolution happening one phase earlier.
pub fn introspect_unwired(doc: &str, env: &aura_runner::project::Env) -> Result<String, String> {
let docs: Vec<OpDoc> = serde_json::from_str(doc).map_err(|e| format!("invalid op-list: {e}"))?;
let (ops, labels, cache) = parse_and_resolve_ops(doc, env)?;
let resolver = |t: &str| env.resolve(t);
// #317: build-free introspection stays subgraph-free by design (spec:
// "Build-free introspection paths pass a `|_| None` closure") — a `use`
// op here always misses (`OpError::UnknownSubgraph`, `From<OpDoc>`'s own
// doc comment), never a registry read.
let mut session = GraphSession::new("introspect", &resolver, &|_: &str| None);
for (i, d) in docs.into_iter().enumerate() {
session.apply(Op::from(d)).map_err(|e| format!("op {i}: {}", format_op_error(&e)))?;
let subgraph = |ref_id: &str| {
cache.get(ref_id).and_then(|json| blueprint_from_json(json, &|t| env.resolve(t)).ok())
};
let mut session = GraphSession::new("introspect", &resolver, &subgraph);
for (i, op) in ops.into_iter().enumerate() {
session.apply(op).map_err(|e| {
let cause = format_op_error(&e);
match labels.get(i) {
Some(kind) => format!("op {i} ({kind}): {cause}"),
None => format!("op {i}: {cause}"),
}
})?;
}
let mut out = String::new();
for (slot, kind) in session.unwired() {
@@ -543,10 +633,11 @@ fn introspect_registered(env: &aura_runner::project::Env) -> Result<String, Stri
}
/// `aura graph introspect`: dispatch the read-only queries. Exactly one of
/// `--vocabulary` / `--node <T>` / `--unwired` / `--params <FILE|ID>` / the id
/// group must be set; zero or more than one is the usage error (exit 2). The id
/// group is `--content-id [FILE]` and/or `--identity-id` — the two id flags may
/// combine (one build, both ids, content id first).
/// `--vocabulary` / `--node <T>` / `--unwired` / `--params <FILE|ID>` /
/// `--taps <FILE|ID>` / the id group must be set; zero or more than one is the
/// usage error (exit 2). The id group is `--content-id [FILE]` and/or
/// `--identity-id` — the two id flags may combine (one build, both ids,
/// content id first).
pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd, env: &aura_runner::project::Env) {
let count = cmd.vocabulary as usize
+ cmd.node.is_some() as usize
@@ -554,10 +645,11 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd, env: &aura_runner::project
+ cmd.folds as usize
+ cmd.registered as usize
+ cmd.params.is_some() as usize
+ cmd.taps.is_some() as usize
+ (cmd.content_id.is_some() || cmd.identity_id) as usize;
if count != 1 {
eprintln!(
"aura: Usage: aura graph introspect --vocabulary | --node <T> | --unwired | --folds | --registered | --params <FILE|ID> | --content-id [FILE] | --identity-id (the two id flags may be combined)"
"aura: Usage: aura graph introspect --vocabulary | --node <T> | --unwired | --folds | --registered | --params <FILE|ID> | --taps <FILE|ID> | --content-id [FILE] | --identity-id (the two id flags may be combined)"
);
std::process::exit(2);
}
@@ -617,10 +709,11 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd, env: &aura_runner::project
} else if let Some(target) = cmd.params.as_deref() {
// --params <FILE|ID> (#196): the RAW composite param space — the one
// namespace campaign axes are validated against
// (`validate_campaign_refs`) and `aura sweep --list-axes` prints
// (#328: the wrapped `<blueprint>.<node>.<param>` form was retired —
// there is exactly one axis namespace now, so the two discovery
// surfaces agree line-for-line, see `params_lines`'s own doc comment).
// (`validate_campaign_refs`) and `exec --override`'s own resolution
// (`override_paths`, member.rs) resolves against (#328: the wrapped
// `<blueprint>.<node>.<param>` form was retired — there is exactly
// one axis namespace now, so the two surfaces agree line-for-line,
// see `params_lines`'s own doc comment).
match params_lines(target, env) {
Ok(s) => print!("{s}"),
Err(m) => {
@@ -628,6 +721,18 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd, env: &aura_runner::project
std::process::exit(1);
}
}
} else if let Some(target) = cmd.taps.as_deref() {
// --taps <FILE|ID> (#337): the positive declared-tap discovery view —
// #333 shipped only the refusal roster (`bind_tap_plan`'s
// `UnknownTap`), so provoking that refusal was the only way to learn
// a blueprint's declared tap names.
match taps_lines(target, env) {
Ok(s) => print!("{s}"),
Err(m) => {
eprintln!("aura: {m}");
std::process::exit(1);
}
}
} else {
// --content-id [FILE] / --identity-id (combinable): one build, then each
// requested id on its own line, content id first. With a FILE value the
@@ -638,8 +743,11 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd, env: &aura_runner::project
// The content id (#158) is the SHA256 of the same `blueprint_to_json`
// bytes `graph build` emits; the identity id (#171) the SHA256 of the
// debug-name-blind `blueprint_identity_json` form — both via the one
// shared `crate::content_id` primitive `topology_hash` also uses, so all
// surfaces agree by construction.
// shared `crate::content_id` primitive, the same hash the run
// manifest's `topology_hash` uses (computed inline in
// `aura_runner::member`'s `run_signal_r`/`run_blueprint_member` — #319
// Task 9 retired the CLI-side `topology_hash` wrapper this comment
// used to name), so all surfaces agree by construction.
let composite = match cmd.content_id.as_ref() {
Some(Some(file)) => {
let text = match std::fs::read_to_string(file) {
@@ -649,7 +757,7 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd, env: &aura_runner::project
std::process::exit(1);
}
};
match composite_from_any(&text, env) {
match composite_from_authored_text(&text, env) {
Ok(c) => c,
Err(m) => {
eprintln!("aura: {m}");
@@ -697,14 +805,18 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd, env: &aura_runner::project
/// Phrase a blueprint-document `LoadError` as prose (Debug-leak-free; the
/// engine error types are `Display`-free by convention — this is the CLI's
/// presentation layer, like `format_op_error`). `pub(crate)`: shared by the
/// `graph build` path (below) and `aura run`'s loaded-blueprint branch
/// `graph build` path (below) and `aura exec`'s blueprint leg
/// (main.rs) — env-agnostic on purpose, so both callers get identical wording
/// (#184).
pub(crate) fn blueprint_load_prose(e: &LoadError) -> String {
match e {
LoadError::Json(err) => format!("blueprint document is not valid JSON: {err}"),
LoadError::UnsupportedVersion { found, supported } => {
format!("blueprint format_version {found} is unsupported (this build reads {supported})")
// #341 item 2: name the full accepted range, not just the ceiling — the
// loader's own floor is the fixed `1` of its `1..=BLUEPRINT_FORMAT_VERSION`
// check (`blueprint_serde::blueprint_from_json`), never carried in the
// error itself.
format!("blueprint format_version {found} is unsupported (this build reads versions 1..={supported})")
}
LoadError::UnknownNodeType(t) => format!("unknown node type {t:?}"),
LoadError::Gang(e) => format!("gang section invalid: {}", gang_fault_prose(e)),
@@ -745,28 +857,38 @@ fn gang_fault_prose(e: &CompileError) -> String {
}
}
/// (#185/#241/#244) When an unresolved type id LOOKS project-namespaced
/// (`::`), the likeliest causes are environmental, tiered: outside any
/// project the hint points at the missing `Aura.toml`; inside a data-only
/// project it points at the missing node crate. Bare std ids (no `::`) and
/// invocations with a loaded node crate get no hint (the vocabulary error
/// carries the message). Shared core: both the blueprint LOAD path
/// (`unresolved_namespace_hint`, over `LoadError`) and the op-script `graph
/// build` path (`composite_from_str`, over `OpError`) call this same helper,
/// so the tier texts cannot drift between the two error families.
/// (#185/#241/#244/#341 item 5) When an unresolved type id LOOKS project-
/// namespaced (`::`), the likeliest causes are environmental, tiered: outside
/// any project the hint points at the missing `Aura.toml`; inside a data-only
/// project it points at the missing node crate. Outside any project, a BARE id
/// still gets no hint — at least as likely a plain typo of a std name as an
/// escalation miss, and "no Aura.toml" would mislead a std-typo author. Inside
/// a data-only project, though, a bare id gets the SAME escalation pointer as
/// a namespaced one (#341 item 5): the consumer who wrote a bare name is the
/// one least likely to know yet that new logic needs a namespace at all, so
/// withholding the hint from exactly them was backwards. A project with a
/// loaded node crate gets no hint either way (the vocabulary error carries the
/// message; an unresolved type there is a genuine miss, not an escalation
/// gap). Shared core: both the blueprint LOAD path (`unresolved_namespace_
/// hint`, over `LoadError`) and the op-script `graph build` path
/// (`composite_from_str`, over `OpError`) call this same helper, so the tier
/// texts cannot drift between the two error families.
fn tier_hint_for_type_id(type_id: &str, env: &aura_runner::project::Env) -> Option<String> {
if !type_id.contains("::") {
return None;
}
let namespaced = type_id.contains("::");
match env.provenance() {
None => Some(
None if namespaced => Some(
"type id looks project-namespaced but no Aura.toml was found — run inside a project directory"
.to_string(),
),
Some(p) if p.namespace.is_none() => Some(
None => None,
Some(p) if p.namespace.is_none() && namespaced => Some(
"type id looks project-namespaced but this project binds no node crate — attach one with `aura nodes new <name>`"
.to_string(),
),
Some(p) if p.namespace.is_none() => Some(
"new logic is authored in Rust — this project binds no node crate yet; attach one with `aura nodes new <name>`"
.to_string(),
),
Some(_) => None,
}
}
@@ -780,45 +902,22 @@ pub(crate) fn unresolved_namespace_hint(e: &LoadError, env: &aura_runner::projec
}
}
/// Validate a blueprint-slot document — the `sweep`/`walkforward`/`mc`
/// loaded-blueprint branches' dispatch-boundary check (#210 c0110 fieldtest
/// finding: the sweep slot used to print the raw `{e:?}` Debug leak). Single-
/// sourced here so the three dual-grammar subcommands cannot diverge (#184's
/// convention, extended). Two refusals, checked in order:
///
/// 1. The document parses as JSON but is an op-script ARRAY (#157), not a
/// built blueprint envelope — refused with a targeted hint pointing at
/// `aura graph build`, since `blueprint_from_json` would otherwise surface
/// a confusing internal serde shape mismatch.
/// 2. `blueprint_from_json` fails to load the envelope — phrased house-style
/// via [`blueprint_load_prose`] (+ [`unresolved_namespace_hint`] where it
/// applies), never the raw `LoadError` Debug form.
///
/// `Ok(())` means the document loaded cleanly; callers that only need the
/// validation (not the `Composite`) re-parse `doc` themselves afterward
/// (`blueprint_from_json` is cheap and infallible at that point).
pub(crate) fn blueprint_slot_prose(doc: &str, env: &aura_runner::project::Env) -> Result<(), String> {
if matches!(serde_json::from_str::<serde_json::Value>(doc), Ok(serde_json::Value::Array(_))) {
return Err(
"this is an op-script (an op array), not a built blueprint — run \
'aura graph build' first"
.to_string(),
);
}
blueprint_from_json(doc, &|t| env.resolve(t)).map(|_| ()).map_err(|e| {
let mut msg = blueprint_load_prose(&e);
if let Some(hint) = unresolved_namespace_hint(&e, env) {
msg.push_str("");
msg.push_str(&hint);
}
msg
})
}
/// #196: build a `Composite` from a document that is EITHER a #155 blueprint
/// envelope (a JSON object: `format_version` + `blueprint`) OR a construction
/// op-list (a JSON array) — shape-discriminated on the top-level JSON type,
/// 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> {
let value: serde_json::Value =
serde_json::from_str(text).map_err(|e| format!("invalid document: {e}"))?;
@@ -836,12 +935,76 @@ 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 exception — the one direct `gate_authored_root_name` caller
/// (main.rs).** One fresh-FILE intake reaches the same unsanitized
/// `traces/<name>/` seam without going through this wrapper, because it
/// 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:
/// - `exec <blueprint.json>` (`exec_blueprint_leg`, #319): envelope-only
/// grammar (no op-script fallback), feeds `signal.name()` into
/// `run_signal_r`/`run_measurement` -> `bind_tap_plan` ->
/// `TraceStore::begin_run`.
///
/// (The quintet's other direct callers — the now-retired `dispatch_run`,
/// `validate_and_register_axes`, `run_blueprint_sweep`/`_walkforward`/`_mc`,
/// `list_blueprint_axes` — none of these functions survives in the tree
/// today; all were retired together with the quintet, #319 Task 8.
/// `exec_blueprint_leg` above is the only direct caller left.)
///
/// This shares 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 `exec_blueprint_leg`'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
/// id in the project store (the campaign-run target-addressing convention).
/// 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
/// cannot drift apart on what counts as a store address.
fn resolve_blueprint_text(target: &str, env: &aura_runner::project::Env) -> Result<String, String> {
/// `exec`'s campaign-target resolution uses, so the two FILE-or-id surfaces
/// cannot drift apart on what counts as a store address. The `bool` names
/// 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
// fields stay bare-only.
let target = target
@@ -851,11 +1014,12 @@ fn resolve_blueprint_text(target: &str, env: &aura_runner::project::Env) -> Resu
let path = Path::new(target);
if path.is_file() {
return std::fs::read_to_string(path)
.map(|text| (text, true))
.map_err(|e| format!("cannot read {}: {e}", path.display()));
}
if aura_runner::axes::is_content_id(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")),
Err(e) => Err(e.to_string()),
};
@@ -870,14 +1034,19 @@ fn resolve_blueprint_text(target: &str, env: &aura_runner::project::Env) -> Resu
/// the same form `introspect --node` already uses for param kinds. Followed
/// by one `{name}:{kind:?} default={value}` line per BOUND param (#328):
/// `bound_param_space()`'s names are already RAW (#203), so no blueprint-name
/// concatenation is needed — line-identical to the reconciled `--list-axes`
/// bound pass (`list_blueprint_axes`, main.rs), same `render_value` lexicon.
/// concatenation is needed — line-identical to the retired `aura sweep
/// --list-axes`'s own bound pass (`list_blueprint_axes`, main.rs, gone with
/// the #319 sugar retirement), 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> {
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)
// 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();
for p in composite.param_space() {
let _ = writeln!(out, "{}:{:?}", p.name, p.kind);
@@ -888,6 +1057,31 @@ fn params_lines(target: &str, env: &aura_runner::project::Env) -> Result<String,
Ok(out)
}
/// `aura graph introspect --taps <FILE|ID>` (#337): one row per declared tap
/// — `<name> <node>.<field> <kind:?>` — the positive discovery view #333's
/// refusal roster (`bind_tap_plan`'s `UnknownTap`) deliberately deferred:
/// before this, the only way to learn a blueprint's declared tap names was
/// provoking that refusal. Loads exactly like `--params` (FILE gates the
/// authored root name, a store content id does not, C29). A tap-less
/// blueprint prints nothing to stdout — nothing was declared to list — plus a
/// stderr note (the `bind_tap_plan` unbound-this-run note's own "aura: note:"
/// convention), exit 0: a listing, not a fault.
fn taps_lines(target: &str, env: &aura_runner::project::Env) -> Result<String, String> {
use std::fmt::Write as _;
let (text, is_file) = resolve_blueprint_text(target, env)?;
let composite = if is_file { composite_from_authored_text(&text, env)? } else { composite_from_any(&text, env)? };
let taps = composite.declared_taps();
if taps.is_empty() {
crate::diag::note!("{target} declares no taps");
return Ok(String::new());
}
let mut out = String::new();
for (name, wire, kind) in taps {
let _ = writeln!(out, "{name} {wire} {kind:?}");
}
Ok(out)
}
/// `aura graph register <blueprint.json> [--name <label>]` (#196, `--name`
/// #317): parse the blueprint through the project vocabulary, canonicalize,
/// content-address, and store — the `process register` pattern, printing
@@ -918,7 +1112,9 @@ fn register_blueprint(
.map_err(|e| format!("cannot read {}: {e}", file.display()))?;
// Shape-discriminated like file-mode --content-id (#202): either shape
// 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 id = crate::content_id(&canonical);
let registry = env.registry();
@@ -1012,6 +1208,18 @@ mod tests {
assert_eq!(err, "op 1 (add): unknown node type \"Nope\"");
}
/// Property (#341 item 2): a v3 document's `UnsupportedVersion` refusal names
/// the FULL supported range, not just the ceiling — the pre-fix prose named
/// only `supported` ("this build reads 2"), leaving a reader to guess whether
/// 0/1 are also refused.
#[test]
fn blueprint_load_prose_unsupported_version_names_the_full_range() {
let e = LoadError::UnsupportedVersion { found: 3, supported: 2 };
let msg = super::blueprint_load_prose(&e);
assert!(msg.contains("1..=2"), "names the full supported range 1..=2: {msg}");
assert!(msg.contains('3'), "still names the found version: {msg}");
}
#[test]
fn build_from_str_reports_unconnected_slot_at_finalize() {
// sub.rhs left unwired -> the holistic 0-arm fires at the finalize step.
+745 -2079
View File
File diff suppressed because it is too large Load Diff
+2 -27
View File
@@ -65,7 +65,7 @@ impl DocIntrospectCmd {
/// Parse `--parallel-instruments` in domain terms: clap's built-in
/// `NonZeroUsize` parser would leak the Rust type name ("number would be
/// zero for non-zero type") at exactly the moment a user needs guidance.
fn parse_parallel_instruments(s: &str) -> Result<std::num::NonZeroUsize, String> {
pub(crate) fn parse_parallel_instruments(s: &str) -> Result<std::num::NonZeroUsize, String> {
s.parse::<usize>().ok().and_then(std::num::NonZeroUsize::new).ok_or_else(|| {
"must be a whole number of at least 1 — it bounds how many distinct \
instruments are resident in parallel"
@@ -161,7 +161,7 @@ pub(crate) fn doc_fault_prose(f: &DocFault) -> String {
format!("data.windows[{index}]: from_ms must be earlier than to_ms")
}
DocFault::BadRegime { index } => {
format!("risk[{index}]: stop length must be >= 1 and k must be > 0")
format!("risk[{index}]: stop length must be >= 1, k must be > 0, and a fixed-stop distance must be > 0")
}
DocFault::BadCost { index } => {
format!("cost[{index}]: the component's price-unit knob must be finite and >= 0")
@@ -411,19 +411,6 @@ pub enum CampaignSub {
Introspect(DocIntrospectCmd),
/// Register a valid campaign document into the store under the runs root.
Register { file: PathBuf },
/// Execute a stored campaign into a realized run-set (a .json file is
/// register-then-run sugar; the canonical address is the content id).
Run {
target: String,
/// Bound on distinct instruments resident in parallel (the RAM
/// lever; 1 reproduces the sequential loop's footprint).
#[arg(
long,
default_value_t = aura_campaign::DEFAULT_PARALLEL_INSTRUMENTS,
value_parser = parse_parallel_instruments,
)]
parallel_instruments: std::num::NonZeroUsize,
},
/// List stored campaign realizations, or dump one campaign's records
/// (the bare store lines, not the run-emit wrapper).
Runs { campaign: Option<String>, run: Option<usize> },
@@ -542,18 +529,7 @@ fn campaign_runs(campaign: Option<&str>, run: Option<usize>, env: &Env) -> Resul
Ok(())
}
/// #272: `Run` threads a failed-cell count (exit 3 on completed-with-failures,
/// via `exit_on_campaign_result`), so it is pulled out of the unified
/// `Result<(), String>` match the other four subcommands still share.
pub fn campaign_cmd(cmd: CampaignCmd, env: &Env) {
if let CampaignSub::Run { target, parallel_instruments } = &cmd.sub {
crate::exit_on_campaign_result(crate::campaign_run::run_campaign(
target,
env,
*parallel_instruments,
));
return;
}
let result = match &cmd.sub {
CampaignSub::Validate { file } => validate_campaign_file(file, env),
CampaignSub::Introspect(i) => {
@@ -563,7 +539,6 @@ pub fn campaign_cmd(cmd: CampaignCmd, env: &Env) {
CampaignSub::Register { file } => register_campaign(file, env),
CampaignSub::Runs { campaign, run } => campaign_runs(campaign.as_deref(), *run, env),
CampaignSub::Show { id } => show_campaign(id, env),
CampaignSub::Run { .. } => unreachable!("handled above"),
};
if let Err(m) = result {
eprintln!("aura: {m}");
+15 -14
View File
@@ -231,12 +231,13 @@ const CLAUDE_MD_PROJECT: &str = r#"# __NAME__ — an aura research project (data
This directory is an aura project: blueprints + research documents over the
std vocabulary, anchored by `Aura.toml`. There is no crate and no build step.
- Run: `aura run blueprints/signal.json` (the starter is closed — all
params bound; bound values are defaults — any `--axis` may override them
(#246))
- Sweep: `aura sweep blueprints/signal.json --axis fast.length=2,4,8`
(`aura sweep <bp> --list-axes` lists the open + bound-overridable axes, RAW
`<node>.<param>` names — #328)
- Run: `aura exec blueprints/signal.json` (single smoke run, synthetic stream
— the starter is closed; all params bound; bound values are defaults —
`--override NODE.PARAM=VALUE` may override one for this run (#246))
- Campaign: `aura exec <campaign.json>` executes a registered campaign
document (file or content id) — the multi-cell/axis surface
- Axes: `aura graph introspect --params blueprints/signal.json` lists the
open + bound-overridable axes, RAW `<node>.<param>` names (#328)
- Native nodes: when the project needs its first project-specific node,
`aura nodes new <name>` scaffolds a node crate beside this project and
attaches it via `[nodes]` in `Aura.toml` (build it with `cargo build`).
@@ -246,9 +247,9 @@ std vocabulary, anchored by `Aura.toml`. There is no crate and no build step.
unit R, and quality metrics are R-based. Entry signals become held state
via the signal-side latch/edge-pulse idiom (see
`aura graph introspect --vocabulary`).
- Data plane: the research verbs are sugar over registered process/campaign
documents — author them directly with `aura process` / `aura campaign`,
growing one from a bare `{}` via `aura campaign introspect --unwired`.
- Data plane: process/campaign documents are authored directly with
`aura process` / `aura campaign`, growing one from a bare `{}` via
`aura campaign introspect --unwired`.
"#;
/// Render a data-only project template: only `__NAME__`/`__NAME_SNAKE__`
@@ -459,12 +460,12 @@ mod tests {
}
let claude = render_project(CLAUDE_MD_PROJECT, "demo-lab");
assert!(claude.contains("demo-lab"));
// The CLAUDE.md sweep quickstart targets the closed starter itself
// with the RAW axis name (#246: a bound param is a default an axis
// overrides; #328: the axis namespace is raw, no blueprint-name
// prefix).
// The CLAUDE.md quickstart targets the closed starter itself through
// the surviving surface (#319): exec for both document classes, the
// #246 override residue, and raw-namespace axis discovery (#328).
assert!(claude.contains("blueprints/signal.json"));
assert!(claude.contains("--axis fast.length"));
assert!(claude.contains("--override NODE.PARAM=VALUE"));
assert!(claude.contains("graph introspect --params"));
}
/// #315: the scaffolded project CLAUDE.md teaches the execution semantics
File diff suppressed because it is too large Load Diff
+113 -26
View File
@@ -5,47 +5,136 @@
//! handled cleanly, never a panic.
use std::io::Read;
use std::path::Path;
use std::process::{Command, Stdio};
mod common;
use common::{fresh_project_with_data, ScratchGuard, ScratchPath};
/// Path to the freshly-built `aura` binary (Cargo sets this env var for the test
/// crate; the binary is named `aura` in `Cargo.toml`).
const BIN: &str = env!("CARGO_BIN_EXE_aura");
/// A fresh, unique working directory so the spawned `aura sweep` writes its
/// `./runs/runs.jsonl` into a throwaway dir, never dirtying the repo. Unique
/// per test + per process; no external tempfile dependency (mirrors `cli_run.rs`).
fn temp_cwd(name: &str) -> std::path::PathBuf {
let dir = std::path::Path::new(env!("CARGO_TARGET_TMPDIR")).join(format!("aura-cli-{name}"));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("create temp cwd");
dir
fn run_code_in(dir: &Path, args: &[&str]) -> (String, Option<i32>) {
let out = Command::new(BIN).args(args).current_dir(dir).output().expect("binary runs");
let text = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
(text, out.status.code())
}
fn write_doc(dir: &Path, name: &str, text: &str) -> std::path::PathBuf {
let p = dir.join(name);
std::fs::write(&p, text).expect("write doc");
p
}
/// The minimal executable pipeline (one sweep stage) — copied verbatim from
/// `research_docs.rs`'s constant of the same name.
const SWEEP_ONLY_PROCESS_DOC: &str = r#"{
"format_version": 1,
"kind": "process",
"name": "sweep-only",
"pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" } ]
}"#;
/// Seed one blueprint into the built demo project's store via `aura graph
/// register` and return its content id — copied verbatim from
/// `research_docs.rs`'s recipe of the same name (#319 — the retired `sweep`
/// side-effect seeding this used to ride played no role in the returned
/// content id, a pure function of the loaded blueprint's canonical bytes;
/// `name` labels the registered id, mirroring the old per-call sweep family
/// name).
fn seed_blueprint(dir: &Path, name: &str) -> String {
let closed_bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let (out, code) = run_code_in(dir, &["graph", "register", &closed_bp, "--name", name]);
assert_eq!(code, Some(0), "seed register failed: {out}");
out.lines()
.find(|l| l.starts_with("registered blueprint "))
.expect("register line")
.trim_start_matches("registered blueprint ")
.split(' ')
.next()
.expect("id")
.trim_start_matches("content:")
.to_string()
}
/// Register `doc` as a process document in the project store; returns its
/// id — copied verbatim from `research_docs.rs`'s recipe of the same name.
fn register_process_doc(dir: &Path, file: &str, doc: &str) -> String {
write_doc(dir, file, doc);
let (out, code) = run_code_in(dir, &["process", "register", file]);
assert_eq!(code, Some(0), "process register failed: {out}");
out.lines()
.find(|l| l.starts_with("registered process "))
.expect("register line")
.trim_start_matches("registered process ")
.split(' ')
.next()
.expect("id")
.trim_start_matches("content:")
.to_string()
}
/// A one-strategy, two-axis (2×2) campaign document over `bp_id`/`proc_id` —
/// copied verbatim from `exec.rs`'s `campaign_doc_json_for` of the same
/// shape, which yields exactly four family members (the four-line stream
/// this test's property needs).
fn campaign_doc_json_for(instrument: &str, bp_id: &str, proc_id: &str, window: (i64, i64)) -> String {
format!(
r#"{{
"format_version": 1,
"kind": "campaign",
"name": "broken-pipe",
"data": {{ "instruments": ["{instrument}"], "windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
"axes": {{ "fast.length": {{ "kind": "I64", "values": [2, 4] }},
"slow.length": {{ "kind": "I64", "values": [8, 16] }} }} }} ],
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
"seed": 7,
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
}}"#,
from = window.0,
to = window.1,
)
}
/// Property: a family-emitting command whose stdout reader closes early exits on
/// the EPIPE cleanly — it does NOT panic (`failed printing to stdout: Broken
/// pipe`) and does NOT exit 101. `aura sweep` streams four family-member JSON
/// lines via `println!`; when the reader closes stdout after the first line, the
/// next write hits a closed pipe. Rust's runtime sets SIGPIPE to SIG_IGN at
/// startup, so that write returns EPIPE and `println!` panics rather than the
/// process terminating quietly — exactly the behaviour a CLI must not have. This
/// is the contract: any aura subcommand can be piped into `| head` / `| less` /
/// pipe`) and does NOT exit 101. `aura exec <campaign.json>` streams four
/// family-member JSON lines via `println!` (a 2×2 axis grid — #319: the
/// surviving surface for what a bare `aura sweep` demo grid used to stream);
/// when the reader closes stdout after the first line, the next write hits a
/// closed pipe. Rust's runtime sets SIGPIPE to SIG_IGN at startup, so that
/// write returns EPIPE and `println!` panics rather than the process
/// terminating quietly — exactly the behaviour a CLI must not have. This is
/// the contract: any aura subcommand can be piped into `| head` / `| less` /
/// a UI pane that goes away, and it must finish quietly, not panic.
///
/// Autonomous: constructs its own early-closing reader inline (no `head`, no
/// shell, no fixture files) by spawning the binary with a piped stdout and
/// dropping the read handle after a single short read, which closes the pipe's
/// read end and makes the writer's next write hit EPIPE.
#[test]
fn family_emitting_command_survives_early_reader_close() {
let cwd = temp_cwd("broken-pipe-sweep");
let (dir, _fixture) = fresh_project_with_data();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
ScratchPath::Dir(runs_dir),
ScratchPath::File(dir.join("broken-pipe.process.json")),
ScratchPath::File(dir.join("broken-pipe.campaign.json")),
]);
let bp_id = seed_blueprint(&dir, "broken-pipe-seed");
let proc_id = register_process_doc(&dir, "broken-pipe.process.json", SWEEP_ONLY_PROCESS_DOC);
let doc = campaign_doc_json_for("SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999));
write_doc(&dir, "broken-pipe.campaign.json", &doc);
let mut child = Command::new(BIN)
.arg("sweep")
.current_dir(&cwd)
.args(["exec", "broken-pipe.campaign.json"])
.current_dir(&dir)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn aura sweep");
.expect("spawn aura exec");
// Read just enough to guarantee the writer is mid-stream, then drop the read
// handle. Dropping it closes the read end of the pipe; the next `println!`
@@ -63,9 +152,7 @@ fn family_emitting_command_survives_early_reader_close() {
if let Some(mut err) = child.stderr.take() {
let _ = err.read_to_string(&mut stderr);
}
let status = child.wait().expect("reap aura sweep");
let _ = std::fs::remove_dir_all(&cwd);
let status = child.wait().expect("reap aura exec");
// The defect: stderr carries the broken-pipe panic.
assert!(
File diff suppressed because it is too large Load Diff
+10 -8
View File
@@ -1,7 +1,9 @@
//! Shared fixture helpers for the `aura-cli` end-to-end test binaries
//! (`research_docs.rs`, `cli_run.rs`, `project_load.rs`), all of which build
//! and drive the `tests/fixtures/demo-project` fixture through the real
//! `aura` binary (#223).
//! (`research_docs.rs`, `cli_run.rs`, `project_load.rs`, `exec.rs`, and —
//! for [`synthetic_data`] alone — `project_new.rs`, `cli_broken_pipe.rs`,
//! `graph_construct.rs`), all of which build and drive the
//! `tests/fixtures/demo-project` fixture through the real `aura` binary
//! (#223).
//!
//! **Per-test project directories, no shared store.** [`fresh_project`] mints
//! a unique tempdir per test, wired to the shared, once-built fixture crate
@@ -9,10 +11,10 @@
//! no two tests ever race on the same `runs/` store — libtest's default
//! thread parallelism applies without a serializing lock (#250).
//!
//! Each of the three test binaries compiles this file as its own `mod
//! common`, so an item unused by one binary trips that binary's `-D
//! warnings` `dead_code` lint; see the `#[allow(dead_code)]` markers below
//! rather than deleting a helper just because one binary doesn't need it.
//! Each consuming test binary compiles this file as its own `mod common`, so
//! an item unused by one binary trips that binary's `-D warnings` `dead_code`
//! lint; see the `#[allow(dead_code)]` markers below rather than deleting a
//! helper just because one binary doesn't need it.
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
@@ -182,7 +184,7 @@ impl Drop for ScratchGuard {
/// the two no-window `generalize` E2E tests get a real, tiny, hostless
/// archive instead of streaming the 6.6 GB host archive (#250).
#[allow(dead_code)]
mod synthetic_data {
pub mod synthetic_data {
use std::io::Write;
use std::path::Path;
File diff suppressed because it is too large Load Diff
+579 -79
View File
@@ -5,6 +5,9 @@
use std::io::Write;
use std::process::{Command, Stdio};
mod common;
use common::{fresh_project_with_data, ScratchGuard, ScratchPath};
const BIN: &str = env!("CARGO_BIN_EXE_aura");
/// Run `aura <args>` with `stdin_doc` piped in; return (stdout, stderr, success).
@@ -88,6 +91,43 @@ const SIGNAL_DOC_NAME_RESTATED: &str = r#"[
{"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
/// 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.
@@ -242,7 +282,11 @@ fn graph_build_bad_tz_is_exit_1_naming_the_arg() {
/// #271: `graph introspect --node Session` self-describes its declared
/// construction args (name, kind, hint) plus the pending note — the
/// discovery surface an author reads before writing the `args` object.
/// discovery surface an author reads before writing the `args` object. The
/// note itself names the follow-up moves on the current surface (#341 item 3
/// harvest): `graph build` + `introspect --params` for the open knobs,
/// `--unwired` for a partial document's unfilled slots — so a consumer's
/// first port/param guess isn't a coin flip.
#[test]
fn graph_introspect_node_session_lists_arg_rows() {
let (stdout, _stderr, ok) = run(&["graph", "introspect", "--node", "Session"], "");
@@ -254,6 +298,14 @@ fn graph_introspect_node_session_lists_arg_rows() {
stdout.contains("ports and params form at construction"),
"shows the pending note: {stdout}"
);
assert!(
stdout.contains("aura graph introspect --params <bp.json>") && stdout.contains("open knobs"),
"the note names the params discovery follow-up: {stdout}"
);
assert!(
stdout.contains("--unwired") && stdout.contains("unfilled slots"),
"the note names the unwired-slots follow-up: {stdout}"
);
}
#[test]
@@ -442,6 +494,26 @@ fn graph_build_rejects_an_unknown_op_field() {
assert!(stderr.contains("params"), "names the unknown key: {stderr}");
}
/// Property (#336): the unknown-key refusal cites the OFFENDING element's own op
/// index, not serde's `deny_unknown_fields` token position, which (for an
/// internally-tagged enum) surfaces only once the parser has consumed the whole
/// object and lands on the START OF THE FOLLOWING element. With the typo'd key at
/// index 1 (of 3), a line/column-based message would mislead toward index 2;
/// this pins the op-list's own coordinate instead.
#[test]
fn graph_build_unknown_op_field_names_the_offending_op_index() {
let doc = r#"[
{"op":"source","role":"price","kind":"F64"},
{"op":"add","type":"Const","params":{"value":{"F64":1.0}}},
{"op":"expose","from":"const.value","as":"bias"}
]"#;
let (stderr, code) = run_code(&["graph", "build"], doc);
assert_eq!(code, Some(1), "unknown op field is a content fault -> exit 1; stderr: {stderr}");
assert!(stderr.contains("op 1"), "names the offending element's own op index (1): {stderr}");
assert!(!stderr.contains("op 2"), "must not attribute the fault to the following element: {stderr}");
assert!(stderr.contains("params"), "still names the unknown key: {stderr}");
}
/// Property (exit-code partition, #175 iteration 2): within the SAME `graph
/// introspect` subcommand, an invalid `--node <T>` flag VALUE is a USAGE error (a
/// command-line fault the user must fix in argv) — exit 2 — distinct from bad stdin
@@ -677,26 +749,25 @@ fn graph_params_lists_raw_axis_namespace() {
);
}
/// Property (#328, cycle-close tidy fix): `graph introspect --params` and
/// `aura sweep --list-axes` are two independently-dispatched discovery
/// surfaces over the SAME raw axis namespace — the open pass shares one
/// wrap-strip convention, the bound pass shares one `bound_param_space()` +
/// `render_value` lexicon (`list_blueprint_axes`, main.rs; `params_lines`,
/// this crate). Pinned here as a same-fixture LINE EQUALITY (not just two
/// independently-asserted shapes) so the duplicated formatting cannot
/// silently drift apart across an edit to either surface.
/// Property (#328, #319 retirement residue): `graph introspect --params`
/// prints the raw axis namespace — one `{name}:{kind:?}` line per open param,
/// then each bound param with its default (`list_blueprint_axes`, main.rs;
/// `params_lines`, this crate). This test used to also pin `aura sweep
/// --list-axes` as a byte-identical SECOND discovery surface over the same
/// namespace (guarding the two independently-dispatched surfaces against
/// drift); `sweep` is retired, so only `graph introspect --params`'s own
/// literal output survives here — see `graph_params_lists_raw_axis_namespace`
/// above for the sibling pin over the same fixture.
#[test]
fn graph_params_and_sweep_list_axes_are_line_identical() {
let dir = temp_cwd("params-vs-list-axes");
fn graph_params_prints_raw_axis_names() {
let dir = temp_cwd("params-raw-axis-names");
let bp = fixture("r_sma_open.json");
let (params_out, params_err, params_code) =
run_in(&dir, &["graph", "introspect", "--params", &bp]);
assert_eq!(params_code, Some(0), "graph introspect --params: {params_out} {params_err}");
let (axes_out, axes_err, axes_code) = run_in(&dir, &["sweep", &bp, "--list-axes"]);
assert_eq!(axes_code, Some(0), "sweep --list-axes: {axes_out} {axes_err}");
assert_eq!(
params_out, axes_out,
"the two discovery surfaces must print byte-identical lines for the same blueprint"
params_out, "fast.length:I64\nslow.length:I64\nbias.scale:F64 default=0.5\n",
"the raw open params, then the raw bound param with its default"
);
}
@@ -773,6 +844,93 @@ fn graph_params_tolerates_content_prefix_on_target() {
);
}
/// Property (#337): `--taps <FILE>` is the positive declared-tap discovery
/// view — before this, the only way to learn a blueprint's declared tap names
/// was provoking #333's undeclared-tap refusal roster. One row per declared
/// tap: name, source wire (`node.field`), column kind, in declaration order.
#[test]
fn graph_introspect_taps_lists_declared_taps_wire_and_kind() {
let dir = temp_cwd("taps-listed");
let ops = r#"[
{"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":"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":"tap","from":"fast.value","as":"fast_ma"},
{"op":"tap","from":"sub.value","as":"spread"},
{"op":"expose","from":"sub.value","as":"bias"}
]"#;
let (build_out, build_err, build_code) = run_in_stdin(&dir, &["graph", "build"], ops);
assert_eq!(build_code, Some(0), "graph build: {build_out} {build_err}");
let bp = dir.join("tapped.json");
std::fs::write(&bp, &build_out).expect("write built blueprint");
let (stdout, stderr, code) =
run_in(&dir, &["graph", "introspect", "--taps", bp.to_str().unwrap()]);
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
assert_eq!(
stdout, "fast_ma fast.value F64\nspread sub.value F64\n",
"one row per declared tap: name, source wire, column kind"
);
}
/// Property (#337 follow-up, harvest audit item 8): `--taps <ID>` resolves
/// through the project store exactly like `--params <ID>` already does
/// (`graph_params_by_content_id_matches_params_by_file`'s sibling) — register
/// a tap-bearing blueprint, then introspect its declared taps by content id;
/// the store path carries no `FILE`'s root-name gate (C29).
#[test]
fn graph_introspect_taps_by_content_id_matches_by_file() {
let dir = temp_cwd("taps-by-id");
let ops = r#"[
{"op":"doc","text":"fast/slow spread, tapped, exposed as bias"},
{"op":"source","role":"price","kind":"F64"},
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":2}}},
{"op":"add","type":"SMA","name":"slow","bind":{"length":{"I64":4}}},
{"op":"add","type":"Sub"},
{"op":"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":"tap","from":"fast.value","as":"fast_ma"},
{"op":"tap","from":"sub.value","as":"spread"},
{"op":"expose","from":"sub.value","as":"bias"}
]"#;
let (build_out, build_err, build_code) = run_in_stdin(&dir, &["graph", "build"], ops);
assert_eq!(build_code, Some(0), "graph build: {build_out} {build_err}");
let bp = dir.join("tapped.json");
std::fs::write(&bp, &build_out).expect("write built blueprint");
let (reg_out, reg_err, reg_code) = run_in(&dir, &["graph", "register", bp.to_str().unwrap()]);
assert_eq!(reg_code, Some(0), "register: {reg_out} {reg_err}");
let id = registered_id(&reg_out);
let (by_id_out, by_id_err, by_id_code) =
run_in(&dir, &["graph", "introspect", "--taps", &id]);
assert_eq!(by_id_code, Some(0), "stdout: {by_id_out} stderr: {by_id_err}");
assert_eq!(
by_id_out, "fast_ma fast.value F64\nspread sub.value F64\n",
"same declared-tap rows resolved through the store by content id"
);
}
/// Property (#337): a blueprint with no declared taps prints nothing to
/// stdout (nothing was declared to list) plus a one-line stderr note — a
/// listing, not a fault, exit 0.
#[test]
fn graph_introspect_taps_reports_no_taps_on_stderr_when_none_declared() {
let dir = temp_cwd("taps-empty");
let bp = fixture("r_sma_open.json");
let (stdout, stderr, code) = run_in(&dir, &["graph", "introspect", "--taps", &bp]);
assert_eq!(code, Some(0), "a tap-less blueprint is a listing, not a fault: {stderr}");
assert_eq!(stdout, "", "no declared taps to list: {stdout}");
assert!(
stderr.contains("declares no taps"),
"stderr carries the one-line note: {stderr}"
);
}
/// Property (#196, negative): `--params <ID>` with a well-shaped 64-hex id
/// that was never registered refuses with a store-miss message, not a panic
/// or a silent empty namespace — the registry lookup branch of
@@ -929,6 +1087,146 @@ fn graph_register_refuses_an_op_script_doc_that_restates_the_composite_name() {
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");
}
/// (#331 spec test 10's missing sibling, harvest audit item 1): `introspect
/// --taps <FILE>` (#337) reads its FILE branch through the same
/// `composite_from_authored_text` choke point as `register` and `introspect
/// --content-id <FILE>` (C24's name-op clause), so a hand-crafted envelope
/// with a shape-violating root name refuses there too, with the identical
/// `name_gate_fault_prose` text — the per-site pin the C24 enumeration's
/// addition of `--taps` to its gated-intake list otherwise leaves unpinned.
#[test]
fn graph_introspect_taps_refuses_a_hand_crafted_envelope_with_a_bad_root_name() {
let dir = temp_cwd("taps-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}");
let (out2, err2, code2) =
run_in(&dir, &["graph", "introspect", "--taps", file.to_str().unwrap()]);
assert_eq!(code2, Some(1), "--taps <FILE> refuses too: {out2} {err2}");
assert_eq!(err2, err, "--taps shares the identical fault prose with register/--content-id");
}
/// 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
/// envelope — the raw axis namespace it prints for the op-script equals the one it
@@ -954,6 +1252,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");
}
/// (#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):
/// `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
@@ -1082,13 +1409,85 @@ fn open_r_breakout_fixture_lists_its_axis_namespace() {
);
}
/// The minimal executable pipeline (one sweep stage) — copied verbatim from
/// `research_docs.rs`'s constant of the same name.
const SWEEP_ONLY_PROCESS_DOC: &str = r#"{
"format_version": 1,
"kind": "process",
"name": "sweep-only",
"pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" } ]
}"#;
/// Register `doc` as a process document in `dir`'s project store; returns its
/// id — copied verbatim from `research_docs.rs`'s recipe of the same name.
fn register_process_doc(dir: &std::path::Path, file: &str, doc: &str) -> String {
std::fs::write(dir.join(file), doc).expect("write process doc");
let (out, err, code) = run_in(dir, &["process", "register", file]);
assert_eq!(code, Some(0), "process register failed: {out} {err}");
out.lines()
.find(|l| l.starts_with("registered process "))
.expect("register line")
.trim_start_matches("registered process ")
.split(' ')
.next()
.expect("id")
.trim_start_matches("content:")
.to_string()
}
/// A one-strategy campaign document over `bp_id`/`proc_id`, with a raw
/// `axes_json` object literal spliced in verbatim — a campaign document
/// refuses an empty `axes` map ("axes is empty"), so a "closed" comparison
/// cell (nothing to vary) passes a single-value axis over one of its OWN
/// already-bound params at its own default (a no-op reopen, #246: every
/// bound param is an equally re-openable axis). #319: campaigns are `exec`'s
/// surviving surface for binding a genuinely OPEN param to a value —
/// `--override` only reopens an already-BOUND one
/// (`aura_runner::member::override_paths` skips a name already in the open
/// space), so the gang-axis parity tests below run both the closed example
/// and its open-plus-axis twin through this SAME campaign machinery, over
/// the same synthetic archive window, keeping their metrics directly
/// comparable.
fn one_cell_campaign_doc(bp_id: &str, proc_id: &str, window: (i64, i64), axes_json: &str) -> String {
format!(
r#"{{
"format_version": 1,
"kind": "campaign",
"name": "gang-axis-vehicle",
"data": {{ "instruments": ["SYMA"], "windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }}, "axes": {{{axes_json}}} }} ],
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
"seed": 7,
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
}}"#,
from = window.0,
to = window.1,
)
}
/// Execs a one-cell campaign document (written to `dir.join(file)`) and
/// returns its single member's `report.metrics`.
fn one_cell_campaign_metrics(dir: &std::path::Path, file: &str) -> serde_json::Value {
let (out, err, code) = run_in(dir, &["exec", file]);
assert_eq!(code, Some(0), "campaign exec failed: {out} {err}");
let line = out
.lines()
.find(|l| l.starts_with(r#"{"family_id":"#))
.unwrap_or_else(|| panic!("no member line: {out} {err}"));
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
v["report"]["metrics"].clone()
}
/// Property (#61 + #159 cut 2): binding the shipped open example's ganged
/// `channel_length` axis through the REAL `aura sweep` CLI pipeline (argv ->
/// `parse_axes` -> `compile_with_cells`'s gang expansion) reproduces the exact
/// metrics of running the shipped CLOSED example, whose `channel_hi`/
/// `channel_lo` are separately hardwired to the same value. The equivalence
/// between a gang and its member-bound twin is already pinned at the builder/
/// engine level (aura-engine's `gang_e2e.rs`, and this crate's
/// `channel_length` axis through a REAL campaign axis (#319: the surviving
/// surface — `compile_with_cells`'s gang expansion, same as the retired
/// `aura sweep --axis`) reproduces the exact metrics of running the shipped
/// CLOSED example, whose `channel_hi`/`channel_lo` are separately hardwired
/// to the same value. Both legs run as one-cell campaigns over the same
/// synthetic archive window, so the comparison is data-source-agnostic (only
/// the axis-vs-hardwire binding differs). The equivalence between a gang and
/// its member-bound twin is already pinned at the builder/engine level
/// (aura-engine's `gang_e2e.rs`, and this crate's
/// `r_breakout_example_loaded_runs_identically_to_the_carved_signal`, which
/// calls `run_signal_r` directly); this instead drives the actual public
/// binary end-to-end on the actual shipped files, so a regression in the
@@ -1098,25 +1497,53 @@ fn open_r_breakout_fixture_lists_its_axis_namespace() {
/// equivalence still holds.
#[test]
fn open_r_breakout_fixture_gang_axis_matches_the_closed_example() {
let closed_dir = temp_cwd("r-breakout-gang-axis-closed");
let (closed_stdout, closed_stderr, closed_code) = run_in(&closed_dir, &["run", &example("r_breakout.json")]);
assert_eq!(closed_code, Some(0), "closed run stderr: {closed_stderr}");
let closed: serde_json::Value =
serde_json::from_str(closed_stdout.trim()).expect("closed run report parses as JSON");
let (dir, _fixture) = fresh_project_with_data();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
ScratchPath::Dir(runs_dir),
ScratchPath::File(dir.join("breakout-gang.process.json")),
ScratchPath::File(dir.join("breakout-gang-closed.campaign.json")),
ScratchPath::File(dir.join("breakout-gang-open.campaign.json")),
]);
let sweep_dir = temp_cwd("r-breakout-gang-axis-sweep");
let (sweep_stdout, sweep_stderr, sweep_code) = run_in(
&sweep_dir,
&["sweep", &fixture("r_breakout_open.json"), "--axis", "channel_length=3"],
);
assert_eq!(sweep_code, Some(0), "sweep stderr: {sweep_stderr}");
let lines: Vec<&str> = sweep_stdout.lines().collect();
assert_eq!(lines.len(), 1, "one grid point for a single-value axis: {sweep_stdout}");
let member: serde_json::Value = serde_json::from_str(lines[0]).expect("member line parses as JSON");
let (reg_closed_out, reg_closed_err, reg_closed_code) =
run_in(&dir, &["graph", "register", &example("r_breakout.json")]);
assert_eq!(reg_closed_code, Some(0), "register closed: {reg_closed_out} {reg_closed_err}");
let closed_id = registered_id(&reg_closed_out);
let (reg_open_out, reg_open_err, reg_open_code) =
run_in(&dir, &["graph", "register", &fixture("r_breakout_open.json")]);
assert_eq!(reg_open_code, Some(0), "register open: {reg_open_out} {reg_open_err}");
let open_id = registered_id(&reg_open_out);
let proc_id = register_process_doc(&dir, "breakout-gang.process.json", SWEEP_ONLY_PROCESS_DOC);
// The shared SYMA window used throughout the suite's campaign vehicles
// (inside `fresh_project_with_data`'s Jan-Aug 2024 synthetic archive).
let window = (1709251200000, 1719791999999);
std::fs::write(
dir.join("breakout-gang-closed.campaign.json"),
// No-op axis: `delay.lag` reopened at its own bound default (1).
one_cell_campaign_doc(&closed_id, &proc_id, window, r#""delay.lag": { "kind": "I64", "values": [1] }"#),
)
.expect("write closed campaign doc");
std::fs::write(
dir.join("breakout-gang-open.campaign.json"),
one_cell_campaign_doc(
&open_id,
&proc_id,
window,
r#""channel_length": { "kind": "I64", "values": [3] }"#,
),
)
.expect("write open campaign doc");
let closed_metrics = one_cell_campaign_metrics(&dir, "breakout-gang-closed.campaign.json");
let open_metrics = one_cell_campaign_metrics(&dir, "breakout-gang-open.campaign.json");
assert_eq!(
member["report"]["metrics"], closed["metrics"],
"channel_length=3, bound via the real sweep CLI, must fan out to both channel_hi \
open_metrics, closed_metrics,
"channel_length=3, bound via the campaign axis, must fan out to both channel_hi \
and channel_lo identically to the closed example's hardwired channel=3"
);
}
@@ -1150,39 +1577,69 @@ fn open_r_meanrev_fixture_lists_its_axis_namespace() {
);
}
/// Property (#61 + #159 cut 3): a sweep over TWO axes at once — the ganged
/// `window` knob (fusing `mean_window.length`/`var_window.length`) alongside
/// the independent, un-ganged `band.factor` knob — binds both correctly
/// through the real CLI grid, matching the shipped closed example's hardwired
/// window=3/band=2.0. This is a distinct risk from the r_breakout gang test
/// (a lone gang axis): a mis-scoped gang-expansion map that shifts sibling
/// param positions could silently mis-bind the co-present un-ganged axis
/// instead (or vice-versa) even though each axis alone still resolves.
/// Property (#61 + #159 cut 3): a campaign over TWO axes at once (#319: the
/// surviving surface for what `aura sweep --axis` used to do inline) — the
/// ganged `window` knob (fusing `mean_window.length`/`var_window.length`)
/// alongside the independent, un-ganged `band.factor` knob — binds both
/// correctly through the real CLI grid, matching the shipped closed
/// example's hardwired window=3/band=2.0. This is a distinct risk from the
/// r_breakout gang test (a lone gang axis): a mis-scoped gang-expansion map
/// that shifts sibling param positions could silently mis-bind the
/// co-present un-ganged axis instead (or vice-versa) even though each axis
/// alone still resolves. Both legs run as one-cell campaigns over the same
/// synthetic archive window, so the comparison is data-source-agnostic.
#[test]
fn open_r_meanrev_fixture_gang_plus_plain_axis_matches_the_closed_example() {
let closed_dir = temp_cwd("r-meanrev-gang-axis-closed");
let (closed_stdout, closed_stderr, closed_code) = run_in(&closed_dir, &["run", &example("r_meanrev.json")]);
assert_eq!(closed_code, Some(0), "closed run stderr: {closed_stderr}");
let closed: serde_json::Value =
serde_json::from_str(closed_stdout.trim()).expect("closed run report parses as JSON");
let (dir, _fixture) = fresh_project_with_data();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
ScratchPath::Dir(runs_dir),
ScratchPath::File(dir.join("meanrev-gang.process.json")),
ScratchPath::File(dir.join("meanrev-gang-closed.campaign.json")),
ScratchPath::File(dir.join("meanrev-gang-open.campaign.json")),
]);
let sweep_dir = temp_cwd("r-meanrev-gang-axis-sweep");
let (sweep_stdout, sweep_stderr, sweep_code) = run_in(
&sweep_dir,
&[
"sweep", &fixture("r_meanrev_open.json"),
"--axis", "window=3",
"--axis", "band.factor=2.0",
],
);
assert_eq!(sweep_code, Some(0), "sweep stderr: {sweep_stderr}");
let lines: Vec<&str> = sweep_stdout.lines().collect();
assert_eq!(lines.len(), 1, "one grid point for two single-value axes: {sweep_stdout}");
let member: serde_json::Value = serde_json::from_str(lines[0]).expect("member line parses as JSON");
let (reg_closed_out, reg_closed_err, reg_closed_code) =
run_in(&dir, &["graph", "register", &example("r_meanrev.json")]);
assert_eq!(reg_closed_code, Some(0), "register closed: {reg_closed_out} {reg_closed_err}");
let closed_id = registered_id(&reg_closed_out);
let (reg_open_out, reg_open_err, reg_open_code) =
run_in(&dir, &["graph", "register", &fixture("r_meanrev_open.json")]);
assert_eq!(reg_open_code, Some(0), "register open: {reg_open_out} {reg_open_err}");
let open_id = registered_id(&reg_open_out);
let proc_id = register_process_doc(&dir, "meanrev-gang.process.json", SWEEP_ONLY_PROCESS_DOC);
let window = (1709251200000, 1719791999999);
std::fs::write(
dir.join("meanrev-gang-closed.campaign.json"),
// No-op axis: `mean_window.length` reopened at its own bound default (3).
one_cell_campaign_doc(
&closed_id,
&proc_id,
window,
r#""mean_window.length": { "kind": "I64", "values": [3] }"#,
),
)
.expect("write closed campaign doc");
std::fs::write(
dir.join("meanrev-gang-open.campaign.json"),
one_cell_campaign_doc(
&open_id,
&proc_id,
window,
r#""window": { "kind": "I64", "values": [3] }, "band.factor": { "kind": "F64", "values": [2.0] }"#,
),
)
.expect("write open campaign doc");
let closed_metrics = one_cell_campaign_metrics(&dir, "meanrev-gang-closed.campaign.json");
let open_metrics = one_cell_campaign_metrics(&dir, "meanrev-gang-open.campaign.json");
assert_eq!(
member["report"]["metrics"], closed["metrics"],
"window=3 (ganged) + band.factor=2.0 (plain), bound via the real sweep CLI, must \
open_metrics, closed_metrics,
"window=3 (ganged) + band.factor=2.0 (plain), bound via the campaign axes, must \
match the closed example's hardwired window=3/band=2.0"
);
}
@@ -1586,6 +2043,25 @@ fn graph_build_hints_attach_for_namespaced_ids_in_a_data_only_project() {
);
}
/// Property (#341 item 5): the BARE counterpart of the test above — the
/// consumer least likely to know yet that new logic is Rust (and therefore
/// least likely to namespace their type) is the one who most needs the
/// escalation pointer, not the one least likely to get it. Same data-only
/// project, same unresolved-type refusal, no `::` in the type id this time.
#[test]
fn graph_build_hints_attach_for_a_bare_unresolved_type_in_a_data_only_project() {
let dir = temp_cwd("graph-build-dataonly-bare-hint");
std::fs::write(dir.join("Aura.toml"), "").expect("write bare Aura.toml");
let ops = r#"[{"op":"add","type":"ThirdCandle","name":"n"}]"#;
let (stdout, stderr, code) = run_in_stdin(&dir, &["graph", "build"], ops);
assert_ne!(code, Some(0), "an unresolvable bare type refuses: {stderr}");
assert!(stdout.is_empty(), "no blueprint emitted on a fault: {stdout}");
assert!(
stderr.contains("aura nodes new"),
"the bare-form refusal now carries the same escalation pointer as the namespaced one: {stderr}"
);
}
/// Property (#284): the `tap` op-script op, driven through the real `aura
/// graph build` process (not an in-crate call to `build_from_str`), resolves
/// a name-addressed interior wire (`"fast.value"`, session node 0 / output
@@ -1654,7 +2130,7 @@ fn tap_authored_via_op_script_runs_and_persists_the_series() {
let bp_path = dir.join("tapped.json");
std::fs::write(&bp_path, &blueprint_json).expect("write built blueprint");
let (_stdout, stderr2, code2) = run_in(&dir, &["run", bp_path.to_str().unwrap()]);
let (_stdout, stderr2, code2) = run_in(&dir, &["exec", bp_path.to_str().unwrap()]);
assert_eq!(code2, Some(0), "aura run over the op-authored tap: {stderr2}");
// `aura graph build` always names the root composite "graph" (`composite_from_str`),
@@ -1791,6 +2267,30 @@ fn graph_build_use_resolves_a_label_and_echoes_the_id() {
);
}
/// `graph introspect --unwired` on a use-bearing document (#339 item 4
/// harvest): the build-free introspection path resolves `use` refs through
/// the store exactly like `graph build` does now, so a document splicing a
/// registered pattern lists the open slots reaching THROUGH the splice (the
/// pattern's own open input role, surfaced as `trend.x`) alongside an
/// ordinary leaf node's open slot — instead of a hard `UnknownSubgraph` miss.
#[test]
fn graph_introspect_unwired_lists_open_slots_through_a_use_splice() {
let dir = temp_cwd("unwired-use-splice");
let bp = build_envelope_in(&dir, OPEN_PATTERN_DOC, "pattern");
let (reg_out, reg_err, reg_code) =
run_in(&dir, &["graph", "register", bp.to_str().unwrap(), "--name", "smooth"]);
assert_eq!(reg_code, Some(0), "register --name: {reg_out} {reg_err}");
let consumer = r#"[
{"op":"add","type":"SMA","name":"solo"},
{"op":"use","ref":{"name":"smooth"},"name":"trend"}
]"#;
let (stdout, stderr, code) = run_in_stdin(&dir, &["graph", "introspect", "--unwired"], consumer);
assert_eq!(code, Some(0), "a use-bearing document introspects: {stderr}");
assert!(stdout.contains("trend.x"), "the splice's own open role surfaces through: {stdout}");
assert!(stdout.contains("solo.series"), "the leaf node's own open slot still lists: {stdout}");
}
/// An unknown `use` label enumerates every registered label (C29's "name the
/// closed set" idiom) and refuses at exit 1 (op-list content fault, #175).
#[test]
@@ -1896,10 +2396,10 @@ fn graph_build_use_docless_source_refuses_with_the_c29_shape_exit_1() {
/// The worked acceptance flow's last leg (spec §Concrete code shapes): a
/// pattern registered with an open param splices under an instance, and
/// `aura sweep --list-axes` on the CONSUMER shows the path-qualified bare
/// (unbound) axis `graph.<instance>.<node>.<param>:<kind>` — the existing
/// nested-composite param-prefix discipline, reached through `use` this
/// time, not a Rust-authored nested composite.
/// `aura graph introspect --params` on the CONSUMER shows the path-qualified
/// bare (unbound) axis `graph.<instance>.<node>.<param>:<kind>` — the
/// existing nested-composite param-prefix discipline, reached through `use`
/// this time, not a Rust-authored nested composite.
#[test]
fn graph_build_use_end_to_end_axes() {
let dir = temp_cwd("use-end-to-end-axes");
@@ -1934,8 +2434,8 @@ fn graph_build_use_end_to_end_axes() {
std::fs::write(&consumer_bp, &build.stdout).expect("write consumer envelope");
let (axes_out, axes_err, axes_code) =
run_in(&dir, &["sweep", consumer_bp.to_str().unwrap(), "--list-axes"]);
assert_eq!(axes_code, Some(0), "list-axes: {axes_out} {axes_err}");
run_in(&dir, &["graph", "introspect", "--params", consumer_bp.to_str().unwrap()]);
assert_eq!(axes_code, Some(0), "graph introspect --params: {axes_out} {axes_err}");
assert_eq!(
axes_out, "trend.sma.length:I64\n",
"the spliced instance's open param surfaces path-qualified, bare (unbound) RAW form (#328)"
@@ -1961,10 +2461,9 @@ fn graph_build_accepts_an_open_input_pattern() {
/// through `wrap_r` (an existing, unrelated nesting mechanism unaffected by
/// this cycle), so a bare-tap (no-`bias`) pattern is used here: its
/// `run_measurement` path compiles the signal directly, hitting
/// `CompileError::UnboundRootRole` at bootstrap — via the EXISTING `{e:?}`
/// Debug rendering (a raw index, not a role name); a name mapping there is
/// left for a follow-up (out of this cycle's scope, per the plan's own
/// escape hatch), so this pins the existing form.
/// `CompileError::UnboundRootRole` at bootstrap — rendered by role NAME
/// (#339 item 3), not the raw flat index the earlier form left as a
/// follow-up.
#[test]
fn running_an_open_blueprint_refuses_at_bootstrap() {
let doc = r#"[
@@ -1978,11 +2477,12 @@ fn running_an_open_blueprint_refuses_at_bootstrap() {
assert!(built, "an open (Input) root role finishes without root-role boundness (#317)");
let bp = dir.join("open.bp.json");
std::fs::write(&bp, &build_out).expect("write built blueprint");
let (stdout, stderr, code) = run_in(&dir, &["run", bp.to_str().unwrap()]);
let (stdout, stderr, code) = run_in(&dir, &["exec", bp.to_str().unwrap()]);
assert_eq!(code, Some(1), "an open blueprint refuses standalone at bootstrap, not finish: {stdout} {stderr}");
assert!(stdout.is_empty(), "no report emitted on a bootstrap refusal: {stdout}");
assert!(
stderr.contains("UnboundRootRole"),
"the runnability gate (compile), unchanged, names the fault: {stderr}"
stderr.contains("root role \"price\" is unbound"),
"the runnability gate (compile), unchanged, now names the role, not a raw index: {stderr}"
);
assert!(!stderr.contains("UnboundRootRole"), "Debug leak: {stderr}");
}
+20 -25
View File
@@ -1,7 +1,7 @@
//! End-to-end pins for the self-description surfaces (#315, #323): the help
//! opens with the two-layer concept, the sugar verbs name the document shape
//! they desugar to, every introspection roster carries per-entry meanings,
//! and `graph build --help` carries the op-list reference. Driven over the
//! End-to-end pins for the self-description surfaces (#315, #323, #319): the
//! help opens with the two-layer concept, `exec` names both document classes
//! it executes, every introspection roster carries per-entry meanings, and
//! `graph build --help` carries the op-list reference. Driven over the
//! built binary — the zero-setup surface a reader without repo access gets.
use std::process::Command;
@@ -26,7 +26,10 @@ fn run(args: &[&str]) -> (String, String, bool) {
fn top_level_help_opens_with_the_two_layer_concept() {
let (out, err, ok) = run(&["--help"]);
assert!(ok, "aura --help failed: {err}");
assert!(out.contains("research verbs"), "names the sugar layer: {out}");
assert!(
out.contains("the one executor over both document classes"),
"names exec + the document classes: {out}"
);
assert!(out.contains("directly authorable"), "names the document data plane: {out}");
assert!(out.contains("bias in [-1,+1]"), "names the bias output: {out}");
assert!(out.contains("defines the risk unit"), "names the protective stop / R: {out}");
@@ -34,31 +37,22 @@ fn top_level_help_opens_with_the_two_layer_concept() {
assert!(out.contains("aura chart"), "names the trace consumers: {out}");
}
/// #315: each document-bridged sugar verb's long help names the process
/// shape it desugars to and points at the document layer. `run` (not
/// document-bridged) points at the canonical document-first form instead.
/// #319: `exec --help` names both document classes it accepts — the sugar
/// verbs' retirement leaves `exec` as the one executor over a campaign
/// document (file or content id) or a signal blueprint (single run).
#[test]
fn sugar_verbs_name_their_document_shape() {
for (verb, needle) in [
("sweep", "std::sweep"),
("walkforward", "std::walk_forward"),
("mc", "std::monte_carlo"),
("generalize", "std::generalize"),
] {
let (out, err, ok) = run(&[verb, "--help"]);
assert!(ok, "aura {verb} --help failed: {err}");
assert!(out.contains("Sugar"), "{verb} --help names the sugar relation: {out}");
assert!(out.contains(needle), "{verb} --help names {needle}: {out}");
assert!(out.contains("aura campaign"), "{verb} --help points at the documents: {out}");
}
let (out, err, ok) = run(&["run", "--help"]);
assert!(ok, "aura run --help failed: {err}");
assert!(out.contains("document-first"), "run --help names the canonical form: {out}");
fn exec_help_names_both_document_classes() {
let (out, err, ok) = run(&["exec", "--help"]);
assert!(ok, "aura exec --help failed: {err}");
assert!(out.contains("campaign"), "names the campaign document class: {out}");
assert!(out.contains("blueprint"), "names the blueprint document class: {out}");
assert!(out.contains("single run"), "names the blueprint leg's single-run shape: {out}");
}
/// #323: `graph build --help` carries the op-list reference — the op kinds
/// and fields are learnable from the binary, not only from serde refusals.
/// #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]
fn graph_build_help_carries_the_op_reference() {
let (out, err, ok) = run(&["graph", "build", "--help"]);
@@ -74,6 +68,7 @@ fn graph_build_help_carries_the_op_reference() {
r#"{"op":"gang""#,
r#"{"op":"doc""#,
r#"{"op":"use""#,
r#"{"op":"name""#,
] {
assert!(out.contains(op), "op reference carries {op}: {out}");
}
+1 -1
View File
@@ -35,7 +35,7 @@ fn run_measurement(cwd: &Path) {
let bp = cwd.join("measurement.json");
std::fs::write(&bp, two_tap_blueprint_json()).expect("write blueprint");
let out = Command::new(BIN)
.args(["run", bp.to_str().unwrap()])
.args(["exec", bp.to_str().unwrap()])
.current_dir(cwd)
.output()
.expect("spawn aura run");
+89 -29
View File
@@ -38,13 +38,13 @@ fn aura(args: &[&str], cwd: &Path) -> Output {
#[test]
fn project_run_resolves_demo_node_and_is_bit_identical() {
let dir = built_project();
let a = aura(&["run", "demo_signal.json"], dir);
let a = aura(&["exec", "demo_signal.json"], dir);
assert!(
a.status.success(),
"run failed: {}",
String::from_utf8_lossy(&a.stderr)
);
let b = aura(&["run", "demo_signal.json"], dir);
let b = aura(&["exec", "demo_signal.json"], dir);
assert!(b.status.success());
assert_eq!(a.stdout, b.stdout, "two runs must be byte-identical (C1)");
}
@@ -52,7 +52,7 @@ fn project_run_resolves_demo_node_and_is_bit_identical() {
#[test]
fn project_run_manifest_carries_provenance() {
let dir = built_project();
let out = aura(&["run", "demo_signal.json"], dir);
let out = aura(&["exec", "demo_signal.json"], dir);
assert!(out.status.success());
let v: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("run report is JSON");
@@ -83,7 +83,7 @@ fn outside_a_project_the_demo_blueprint_is_unknown() {
// cwd = the aura-cli crate dir: no Aura.toml anywhere up the tree.
let cwd = Path::new(env!("CARGO_MANIFEST_DIR"));
let bp = fixture_dir().join("demo_signal.json");
let out = aura(&["run", bp.to_str().unwrap()], cwd);
let out = aura(&["exec", bp.to_str().unwrap()], cwd);
assert!(!out.status.success());
let err = String::from_utf8_lossy(&out.stderr);
assert!(
@@ -104,31 +104,91 @@ fn outside_a_project_the_demo_blueprint_is_unknown() {
/// `<project-root>/runs/`, and no stray `runs/` must appear under the
/// subdirectory. This is the property that makes `[paths]` project-relative
/// rather than shell-relative (C17).
///
/// #319 vehicle note: the synthetic in-process family builder this test used
/// to drive via bare `sweep --axis` (no `--real`) retired with the quintet —
/// no surviving CLI surface produces a family record without touching a real
/// archive (`exec`'s campaign leg always resolves against `DataServer`), so
/// the vehicle is now a one-cell campaign over the `fresh_project_with_data`
/// synthetic `SYMA` archive (mirrors `exec.rs`'s `seed_blueprint` +
/// `campaign_doc_json_for` recipe), executed from a project subdirectory.
#[test]
fn project_registry_anchors_at_discovered_root_not_invocation_cwd() {
let dir = built_project();
let sub = dir.join("src");
let (dir, _fixture) = common::fresh_project_with_data();
let sub = dir.join("sub");
std::fs::create_dir_all(&sub).expect("create invocation subdirectory");
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let closed_bp =
format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let closed_bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let reg_out = Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["graph", "register", &closed_bp, "--name", "proj-anchor-seed"])
.current_dir(&dir)
.output()
.expect("spawn aura graph register");
assert!(reg_out.status.success(), "register failed: {}", String::from_utf8_lossy(&reg_out.stderr));
let reg_text = String::from_utf8_lossy(&reg_out.stdout);
let bp_id = reg_text
.lines()
.find(|l| l.starts_with("registered blueprint "))
.expect("register line")
.trim_start_matches("registered blueprint ")
.split(' ')
.next()
.expect("id")
.to_string();
let process_doc = r#"{
"format_version": 1,
"kind": "process",
"name": "sweep-only",
"pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" } ]
}"#;
std::fs::write(dir.join("anchor.process.json"), process_doc).expect("write process doc");
let proc_out = Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["process", "register", "anchor.process.json"])
.current_dir(&dir)
.output()
.expect("spawn aura process register");
assert!(proc_out.status.success(), "process register failed: {}", String::from_utf8_lossy(&proc_out.stderr));
let proc_text = String::from_utf8_lossy(&proc_out.stdout);
let proc_id = proc_text
.lines()
.find(|l| l.starts_with("registered process "))
.expect("register line")
.trim_start_matches("registered process ")
.split(' ')
.next()
.expect("id")
.trim_start_matches("content:")
.to_string();
// The established `SYMA` window (2024-03..06, fully inside its 2024-01..08
// span) other #319 vehicle campaigns use.
let campaign_doc = format!(
r#"{{
"format_version": 1,
"kind": "campaign",
"name": "run-seam",
"data": {{ "instruments": ["SYMA"], "windows": [ {{ "from_ms": 1709251200000, "to_ms": 1719791999999 }} ] }},
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
"axes": {{ "fast.length": {{ "kind": "I64", "values": [2, 4] }},
"slow.length": {{ "kind": "I64", "values": [8, 16] }} }} }} ],
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
"seed": 7,
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
}}"#
);
std::fs::write(dir.join("anchor.campaign.json"), &campaign_doc).expect("write campaign doc");
let out = Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"sweep",
&closed_bp,
"--axis",
"fast.length=2,4",
"--axis",
"slow.length=8,16",
"--name",
"proj-anchor",
])
.args(["exec", "../anchor.campaign.json"])
.current_dir(&sub)
.output()
.expect("spawn aura sweep");
.expect("spawn aura exec");
assert!(
out.status.success(),
"sweep failed: {}",
"exec failed: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
@@ -162,7 +222,7 @@ fn vocabulary_charter_violation_refuses_end_to_end() {
"badcharter fixture build failed:\n{}",
String::from_utf8_lossy(&build.stderr)
);
let run = aura(&["run", "x.json"], &dir);
let run = aura(&["exec", "x.json"], &dir);
assert_eq!(
run.status.code(),
Some(1),
@@ -191,7 +251,7 @@ fn undescribed_vocabulary_entry_refuses_end_to_end() {
"undescribed fixture build failed:\n{}",
String::from_utf8_lossy(&build.stderr)
);
let run = aura(&["run", "x.json"], &dir);
let run = aura(&["exec", "x.json"], &dir);
assert_eq!(
run.status.code(),
Some(1),
@@ -229,7 +289,7 @@ fn restated_name_vocabulary_entry_refuses_end_to_end() {
"restated fixture build failed:\n{}",
String::from_utf8_lossy(&build.stderr)
);
let run = aura(&["run", "x.json"], &dir);
let run = aura(&["exec", "x.json"], &dir);
assert_eq!(
run.status.code(),
Some(1),
@@ -287,7 +347,7 @@ fn stale_dylib_warns_naming_both_timestamps_but_still_runs() {
set_mtime(&dylib, DYLIB_MTIME);
set_mtime(&src, SOURCE_MTIME);
let out = aura(&["run", "demo_signal.json"], dir);
let out = aura(&["exec", "demo_signal.json"], dir);
// Restore sane mtimes before asserting, so a failed assertion does not
// leave the shared fixture wedged "stale" (or with a future mtime that
@@ -351,7 +411,7 @@ fn nested_nodes_dir() -> PathBuf {
fn data_only_project_runs_and_stamps_commit_only_provenance() {
let dir = dataonly_dir();
let out = Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "signal.json"])
.args(["exec", "signal.json"])
.current_dir(&dir)
.output()
.expect("spawn aura run");
@@ -387,7 +447,7 @@ fn data_only_project_runs_and_stamps_commit_only_provenance() {
fn multi_crate_pointer_refuses_end_to_end() {
let dir = multicrate_dir();
let out = Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "x.json"])
.args(["exec", "x.json"])
.current_dir(&dir)
.output()
.expect("spawn aura run");
@@ -427,7 +487,7 @@ fn nodes_pointer_crate_resolves_and_stamps_its_namespace() {
String::from_utf8_lossy(&build.stderr)
);
let out = Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "pointer_signal.json"])
.args(["exec", "pointer_signal.json"])
.current_dir(&dir)
.output()
.expect("spawn aura run");
@@ -455,7 +515,7 @@ fn data_only_project_hints_attach_for_namespaced_ids() {
let bp = tmp.join("bp.json");
std::fs::write(&bp, r#"{"format_version":1,"blueprint":{"name":"x","nodes":[{"primitive":{"type":"nosuch::Node"}}],"edges":[],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0}],"source":"F64"}],"output":[{"node":0,"field":0,"name":"bias"}]}}"#).unwrap();
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", bp.to_str().unwrap()])
.args(["exec", bp.to_str().unwrap()])
.current_dir(&tmp)
.output()
.expect("spawn aura");
@@ -479,7 +539,7 @@ fn missing_artifact_refuses_with_build_hint() {
"[package]\nname = \"nobuild\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[workspace]\n",
)
.unwrap();
let out = aura(&["run", "x.json"], &tmp);
let out = aura(&["exec", "x.json"], &tmp);
assert_eq!(out.status.code(), Some(1), "runtime refusal");
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("cargo build"), "build hint present: {err}");
+130 -39
View File
@@ -1,11 +1,12 @@
//! E2E: the `aura new` scaffolder (#241 T4). Proves the data-only authoring
//! loop — scaffold → `aura run`/`aura sweep`, zero Rust toolchain interaction
//! — plus the refusal contract. Scaffolded projects live in per-test temp
//! dirs.
//! loop — scaffold → `aura exec`, zero Rust toolchain interaction — plus the
//! refusal contract. Scaffolded projects live in per-test temp dirs.
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
mod common;
fn aura(args: &[&str], cwd: &Path) -> Output {
Command::new(env!("CARGO_BIN_EXE_aura"))
.args(args)
@@ -61,17 +62,17 @@ fn init_and_commit(dir: &Path) {
///
/// Documented deviation from the task text: the plan's wording asks this
/// headline to also assert that the run "writes a manifest under `x/runs/`".
/// Verified against `dispatch_run`/`run_signal_r` (main.rs, around the
/// `tx_req`/`rx_req` channel comment): plain `aura run` never touches the
/// trace store — it only prints the `RunReport` to stdout; only
/// `sweep`/`mc`/campaign verbs persist (`env.trace_store()`), a pre-#241
/// split this task's file list does not touch (giving `run` a store write
/// would widen scope beyond `scaffold.rs`/`main.rs`'s `NewCmd`/`dispatch_new`
/// and this test file). There is thus no on-disk manifest for THIS verb to
/// assert against; the assertion below proves that absence directly instead
/// of only asserting it in prose, and the persisted-store property the plan
/// wanted is covered by `data_only_project_sweeps_without_any_build` below,
/// which does assert `proj.join("runs").exists()`.
/// Verified against `dispatch_exec`/`exec_blueprint_leg`/`run_signal_r`
/// (main.rs): a single-blueprint `aura exec` never touches the trace store —
/// it only prints the `RunReport` to stdout; only exec's campaign leg
/// persists (`env.trace_store()`), a pre-#241 split this task's file list
/// does not touch (giving the blueprint leg a store write would widen scope
/// beyond `scaffold.rs`/`main.rs`'s `NewCmd`/`dispatch_new` and this test
/// file). There is thus no on-disk manifest for THIS leg to assert against;
/// the assertion below proves that absence directly instead of only
/// asserting it in prose, and the persisted-store property the plan wanted
/// is covered by `data_only_project_execs_a_campaign_without_any_build`
/// below, which does assert `proj.join("runs").exists()`.
#[test]
fn new_scaffolds_and_runs_the_data_only_loop() {
let base = tmp("loop");
@@ -102,13 +103,13 @@ fn new_scaffolds_and_runs_the_data_only_loop() {
assert!(!proj.join("Cargo.toml").exists(), "data-only project must have no Cargo.toml");
assert!(!proj.join("src/lib.rs").exists(), "data-only project must have no src/lib.rs");
let a = aura(&["run", "blueprints/signal.json"], &proj);
let a = aura(&["exec", "blueprints/signal.json"], &proj);
assert!(
a.status.success(),
"run failed: {}",
"exec failed: {}",
String::from_utf8_lossy(&a.stderr)
);
let b = aura(&["run", "blueprints/signal.json"], &proj);
let b = aura(&["exec", "blueprints/signal.json"], &proj);
assert_eq!(a.stdout, b.stdout, "two runs must be byte-identical (C1)");
let v: serde_json::Value = serde_json::from_slice(&a.stdout).expect("report is JSON");
let p = &v["manifest"]["project"];
@@ -116,50 +117,140 @@ fn new_scaffolds_and_runs_the_data_only_loop() {
assert!(p.get("namespace").is_none(), "no crate was loaded, namespace must be absent: {p}");
assert!(
!proj.join("runs").exists(),
"plain `aura run` must not persist a trace store (only sweep/mc/campaign do) — \
the task text's \"writes a manifest under x/runs/\" does not hold for this verb"
"a plain single-blueprint `aura exec` must not persist a trace store (only exec's \
campaign leg does) — the task text's \"writes a manifest under x/runs/\" does not \
hold for this leg"
);
let _ = std::fs::remove_dir_all(&base);
}
/// #241 headline: a fresh data-only project sweeps a dissolved verb (#218's
/// gate) with zero build step — using the scaffolded (closed) starter
/// blueprint and the exact axis the scaffolded CLAUDE.md advertises.
/// The minimal executable pipeline (one sweep stage) — copied verbatim from
/// `research_docs.rs`'s constant of the same name.
const SWEEP_ONLY_PROCESS_DOC: &str = r#"{
"format_version": 1,
"kind": "process",
"name": "sweep-only",
"pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" } ]
}"#;
/// Writes a tiny, self-contained January-2024 `SYMA` archive into
/// `<proj>/data` and points the scaffolded `Aura.toml`'s `[paths]` table at
/// it. #319: `exec`'s campaign leg (the surviving surface for what `aura
/// sweep --axis` used to do inline) needs a registered instrument, unlike a
/// plain blueprint run — this keeps the scaffolder's "zero host setup"
/// headline intact (no real archive mount) by minting the archive straight
/// into the scaffold instead.
fn seed_synthetic_archive(proj: &Path) {
let data_dir = proj.join("data");
std::fs::create_dir_all(&data_dir).expect("create synthetic archive dir");
common::synthetic_data::write_symbol_archive(&data_dir, "SYMA", (2024, 1), (2024, 1));
let toml_path = proj.join("Aura.toml");
let toml = std::fs::read_to_string(&toml_path).expect("read scaffolded Aura.toml");
let toml = toml.replacen("runs = \"runs\"\n", "runs = \"runs\"\ndata = \"data\"\n", 1);
std::fs::write(&toml_path, toml).expect("point Aura.toml at the synthetic archive");
}
/// The id extracted from a `registered <kind> <id> (<path>)` line (#194: bare
/// id, no `content:` prefix — tolerant of the pre-#194 prefix all the same).
fn registered_id(stdout: &str, kind: &str) -> String {
let needle = format!("registered {kind} ");
stdout
.lines()
.find(|l| l.starts_with(&needle))
.unwrap_or_else(|| panic!("no \"{needle}\" line: {stdout}"))
.trim_start_matches(&needle)
.split(' ')
.next()
.expect("id")
.trim_start_matches("content:")
.to_string()
}
/// A one-strategy, one-axis campaign document over `bp_id`/`proc_id` — the
/// surviving surface for what `aura sweep --axis` used to do inline over the
/// scaffolded starter's bound `fast.length` (#246: a bound param is a default
/// an axis overrides). Windowed fully inside `seed_synthetic_archive`'s
/// January-2024 `SYMA` archive.
fn one_axis_campaign_doc(bp_id: &str, proc_id: &str, axis_name: &str, axis_values: &str) -> String {
format!(
r#"{{
"format_version": 1,
"kind": "campaign",
"name": "quickstart",
"data": {{ "instruments": ["SYMA"], "windows": [ {{ "from_ms": 1704067200000, "to_ms": 1706745599999 }} ] }},
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
"axes": {{ "{axis_name}": {{ "kind": "I64", "values": [{axis_values}] }} }} }} ],
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
"seed": 1,
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
}}"#
)
}
/// #241 headline: a fresh data-only project executes a small campaign — #319's
/// surviving surface for the retired `aura sweep` — with zero build step,
/// using the scaffolded (closed) starter blueprint and the exact axis the
/// scaffolded CLAUDE.md advertises. Registering the blueprint straight (`aura
/// graph register`, not a sweep side-effect) and seeding a tiny synthetic
/// archive (`seed_synthetic_archive`) keeps the loop free of any host data
/// mount, matching the pre-retirement headline.
///
/// #246: the closed starter IS the sweep target — a bound param is a default
/// an axis overrides.
/// #246: the closed starter IS the campaign axis target — a bound param is a
/// default an axis overrides.
#[test]
fn data_only_project_sweeps_without_any_build() {
fn data_only_project_execs_a_campaign_without_any_build() {
let base = tmp("sweep");
let new = aura(&["new", "scratch"], &base);
assert!(new.status.success(), "{}", String::from_utf8_lossy(&new.stderr));
let proj = base.join("scratch");
let out = aura(
&["sweep", "blueprints/signal.json", "--axis", "fast.length=2,4"],
&proj,
);
seed_synthetic_archive(&proj);
let reg = aura(&["graph", "register", "blueprints/signal.json"], &proj);
assert!(reg.status.success(), "graph register stderr: {}", String::from_utf8_lossy(&reg.stderr));
let bp_id = registered_id(&String::from_utf8_lossy(&reg.stdout), "blueprint");
std::fs::write(proj.join("quickstart.process.json"), SWEEP_ONLY_PROCESS_DOC)
.expect("write process doc");
let preg = aura(&["process", "register", "quickstart.process.json"], &proj);
assert!(preg.status.success(), "process register stderr: {}", String::from_utf8_lossy(&preg.stderr));
let proc_id = registered_id(&String::from_utf8_lossy(&preg.stdout), "process");
let doc = one_axis_campaign_doc(&bp_id, &proc_id, "fast.length", "2, 4");
std::fs::write(proj.join("quickstart.campaign.json"), &doc).expect("write campaign doc");
let out = aura(&["exec", "quickstart.campaign.json"], &proj);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(proj.join("runs").exists(), "store must land inside the project");
let _ = std::fs::remove_dir_all(&base);
}
/// Property (#246): the scaffold's sweep quickstart genuinely re-opens the
/// Property (#246): the scaffold's campaign quickstart genuinely re-opens the
/// bound `fast.length` param — the persisted family has exactly one member
/// per axis value, not a single collapsed member. A silent regression where
/// the bound-param override is dropped (member always built from the
/// blueprint's own default) would still exit 0 and still create a `runs`
/// store, so `data_only_project_sweeps_without_any_build` above cannot catch
/// it; only the member count can.
/// store, so `data_only_project_execs_a_campaign_without_any_build` above
/// cannot catch it; only the member count can.
#[test]
fn data_only_project_sweep_over_the_starter_opens_one_member_per_axis_value() {
fn data_only_project_campaign_over_the_starter_opens_one_member_per_axis_value() {
let base = tmp("sweep-members");
let new = aura(&["new", "scratch"], &base);
assert!(new.status.success(), "{}", String::from_utf8_lossy(&new.stderr));
let proj = base.join("scratch");
let out = aura(
&["sweep", "blueprints/signal.json", "--axis", "fast.length=2,4,8"],
&proj,
);
seed_synthetic_archive(&proj);
let reg = aura(&["graph", "register", "blueprints/signal.json"], &proj);
assert!(reg.status.success(), "graph register stderr: {}", String::from_utf8_lossy(&reg.stderr));
let bp_id = registered_id(&String::from_utf8_lossy(&reg.stdout), "blueprint");
std::fs::write(proj.join("members.process.json"), SWEEP_ONLY_PROCESS_DOC)
.expect("write process doc");
let preg = aura(&["process", "register", "members.process.json"], &proj);
assert!(preg.status.success(), "process register stderr: {}", String::from_utf8_lossy(&preg.stderr));
let proc_id = registered_id(&String::from_utf8_lossy(&preg.stdout), "process");
let doc = one_axis_campaign_doc(&bp_id, &proc_id, "fast.length", "2, 4, 8");
std::fs::write(proj.join("members.campaign.json"), &doc).expect("write campaign doc");
let out = aura(&["exec", "members.campaign.json"], &proj);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let fams = aura(&["runs", "families"], &proj);
assert!(fams.status.success(), "runs families exit: {:?}", fams.status);
@@ -301,10 +392,10 @@ fn new_outside_a_work_tree_leaves_a_resolvable_head() {
// ...so the first quickstart run stamps a commit into the manifest, not
// an empty provenance block.
let run = aura(&["run", "blueprints/signal.json"], &proj);
let run = aura(&["exec", "blueprints/signal.json"], &proj);
assert!(
run.status.success(),
"run failed: {}",
"exec failed: {}",
String::from_utf8_lossy(&run.stderr)
);
let v: serde_json::Value = serde_json::from_slice(&run.stdout).expect("report is JSON");
+71 -35
View File
@@ -149,23 +149,26 @@ fn fresh_project() -> (&'static PathBuf, (RunsCleanup, MutexGuard<'static, ()>))
}
/// Acceptance box 1 (#235): a project-authored node with an OPEN param is
/// discoverable as a sweep axis, sweeps over real data into a persisted family,
/// and that family reproduces bit-identically — the defining research loop of a
/// genuine external project ("sweep my OWN node's knob"), proven end to end.
/// discoverable as an axis, execs over real data (a campaign — #319: the
/// surviving surface for what `aura sweep --axis` used to do inline) into a
/// persisted family, and that family reproduces bit-identically — the
/// defining research loop of a genuine external project ("vary my OWN node's
/// knob"), proven end to end.
#[test]
fn project_node_open_param_sweeps_and_reproduces() {
let (dir, _g) = fresh_project();
// (a) `--list-axes` discovers the PROJECT node's own open knob, followed by
// the fixture's three BOUND params as `default=`-lines (#246: every bound
// param is an equally re-openable `--axis`, listed regardless of how many
// knobs happen to be open alongside it). NOT gated: enumerating axes needs
// no data, so the discoverability half runs on every host. The name is RAW
// `<node>.<param>` (#328: the blueprint name stays out of axis paths).
let axes = aura_in(dir, &["sweep", "blueprints/scaled_open.json", "--list-axes"]);
// (a) `graph introspect --params` discovers the PROJECT node's own open
// knob, followed by the fixture's three BOUND params as `default=`-lines
// (#246: every bound param is an equally re-openable axis, listed
// regardless of how many knobs happen to be open alongside it). NOT
// gated: enumerating axes needs no data, so the discoverability half runs
// on every host. The name is RAW `<node>.<param>` (#328: the blueprint
// name stays out of axis paths).
let axes = aura_in(dir, &["graph", "introspect", "--params", "blueprints/scaled_open.json"]);
assert!(
axes.status.success(),
"--list-axes stderr: {}",
"graph introspect --params stderr: {}",
String::from_utf8_lossy(&axes.stderr)
);
assert_eq!(
@@ -179,37 +182,70 @@ fn project_node_open_param_sweeps_and_reproduces() {
String::from_utf8_lossy(&axes.stderr)
);
// The real-data sweep/reproduce legs are archive-gated.
// The real-data campaign/reproduce legs are archive-gated.
if !local_data_present() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
}
// (b) sweep the project node's knob over real GER40 data -> two members.
let sweep = aura_in(
dir,
&[
"sweep",
"blueprints/scaled_open.json",
"--real",
"GER40",
"--from",
GER40_FROM_MS,
"--to",
GER40_TO_MS,
"--axis",
"gain.factor=0.5,1.0",
"--name",
"knob",
],
// (b) exec a campaign over the project node's knob, real GER40 data ->
// two members. Register the blueprint straight (not a sweep side-effect)
// and a minimal sweep-only process, then reference both by content id.
let reg = aura_in(dir, &["graph", "register", "blueprints/scaled_open.json"]);
assert!(reg.status.success(), "graph register stderr: {}", String::from_utf8_lossy(&reg.stderr));
let reg_out = String::from_utf8_lossy(&reg.stdout).into_owned();
let bp_id = reg_out
.lines()
.find(|l| l.starts_with("registered blueprint "))
.expect("register line")
.trim_start_matches("registered blueprint ")
.split(' ')
.next()
.expect("blueprint id")
.to_string();
std::fs::write(dir.join("knob-reproduce.process.json"), SWEEP_ONLY_PROCESS)
.expect("write process doc");
let preg = aura_in(dir, &["process", "register", "knob-reproduce.process.json"]);
assert!(preg.status.success(), "process register stderr: {}", String::from_utf8_lossy(&preg.stderr));
let preg_out = String::from_utf8_lossy(&preg.stdout).into_owned();
let proc_id = preg_out
.lines()
.find(|l| l.starts_with("registered process "))
.expect("register line")
.trim_start_matches("registered process ")
.split(' ')
.next()
.expect("process id")
.to_string();
let campaign = format!(
r#"{{
"format_version": 1,
"kind": "campaign",
"name": "knob-reproduce",
"data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
"strategies": [ {{ "ref": {{ "content_id": "{bp}" }},
"axes": {{ "gain.factor": {{ "kind": "F64", "values": [0.5, 1.0] }} }} }} ],
"process": {{ "ref": {{ "content_id": "{proc}" }} }},
"seed": 7,
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
}}"#,
from = GER40_FROM_MS,
to = GER40_TO_MS,
bp = bp_id,
proc = proc_id,
);
std::fs::write(dir.join("knob-reproduce.campaign.json"), &campaign).expect("write campaign doc");
let exec = aura_in(dir, &["exec", "knob-reproduce.campaign.json"]);
assert!(
sweep.status.success(),
"sweep stderr: {}",
String::from_utf8_lossy(&sweep.stderr)
exec.status.success(),
"exec stderr: {}",
String::from_utf8_lossy(&exec.stderr)
);
let stdout = String::from_utf8_lossy(&sweep.stdout).into_owned();
let lines: Vec<&str> = stdout.lines().collect();
let stdout = String::from_utf8_lossy(&exec.stdout).into_owned();
let lines: Vec<&str> = stdout.lines().filter(|l| l.starts_with(r#"{"family_id":"#)).collect();
assert_eq!(lines.len(), 2, "one member line per factor point: {stdout}");
// Each member's manifest carries the swept PROJECT-node param binding
@@ -324,7 +360,7 @@ fn project_node_open_param_runs_one_campaign_cell() {
);
std::fs::write(dir.join("knob.campaign.json"), &campaign).expect("write campaign doc");
let run = aura_in(dir, &["campaign", "run", "knob.campaign.json"]);
let run = aura_in(dir, &["exec", "knob.campaign.json"]);
let out = format!(
"{}{}",
String::from_utf8_lossy(&run.stdout),
+129 -125
View File
@@ -319,34 +319,9 @@ fn campaign_validate_in_project_reports_referential_tier_end_to_end() {
]);
// Seed one real, content-addressed blueprint into the project's own
// store via a real sweep over its bound params, which the axes reopen
// as overridable defaults (#246) (mirrors `project_load.rs`'s anchor
// test).
let closed_bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let (sweep_out, sweep_code) = run_code_in(
&dir,
&[
"sweep",
&closed_bp,
"--axis",
"fast.length=2,4",
"--axis",
"slow.length=8,16",
"--name",
"campaign-ref-seed",
],
);
assert_eq!(sweep_code, Some(0), "seed sweep failed: {sweep_out}");
let bp_id = std::fs::read_dir(runs_dir.join("blueprints"))
.expect("blueprints dir")
.next()
.expect("one stored blueprint")
.expect("dir entry")
.path()
.file_stem()
.expect("stem")
.to_string_lossy()
.into_owned();
// store via `aura graph register` (#319 — the retired `sweep`
// side-effect seeding this used to ride).
let bp_id = seed_blueprint(&dir, "campaign-ref-seed");
// Register a valid AND executable process into the same project store:
// this test's OK campaign must pass all three validate tiers, and
@@ -441,32 +416,8 @@ fn campaign_validate_resolves_identity_ref_via_index_first_lookup_then_index_hit
]);
// Seed one real, content-addressed blueprint (mirrors the content_id
// sibling test above).
let closed_bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let (sweep_out, sweep_code) = run_code_in(
&dir,
&[
"sweep",
&closed_bp,
"--axis",
"fast.length=2,4",
"--axis",
"slow.length=8,16",
"--name",
"campaign-identity-seed",
],
);
assert_eq!(sweep_code, Some(0), "seed sweep failed: {sweep_out}");
let bp_id = std::fs::read_dir(runs_dir.join("blueprints"))
.expect("blueprints dir")
.next()
.expect("one stored blueprint")
.expect("dir entry")
.path()
.file_stem()
.expect("stem")
.to_string_lossy()
.into_owned();
// sibling test above; #319 — the retired `sweep` side-effect seeding).
let bp_id = seed_blueprint(&dir, "campaign-identity-seed");
let bp_path = runs_dir.join("blueprints").join(format!("{bp_id}.json"));
// The identity id of the SAME stored bytes — `--content-id FILE
@@ -711,7 +662,7 @@ fn campaign_validate_refuses_bad_risk_regime_prose_exit_1() {
let (out, code) = run_code_in(&dir, &["campaign", "validate", "risk-bad.campaign.json"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(
out.contains("risk[0]: stop length must be >= 1 and k must be > 0"),
out.contains("risk[0]: stop length must be >= 1, k must be > 0, and a fixed-stop distance must be > 0"),
"stdout/stderr: {out}"
);
assert!(!out.contains("BadRegime"), "Debug leak: {out}");
@@ -1241,7 +1192,7 @@ fn campaign_register_refuses_invalid_risk_section_and_writes_nothing() {
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(out.contains("refusing to register:"), "stdout/stderr: {out}");
assert!(
out.contains("risk[0]: stop length must be >= 1 and k must be > 0"),
out.contains("risk[0]: stop length must be >= 1, k must be > 0, and a fixed-stop distance must be > 0"),
"stdout/stderr: {out}"
);
assert!(
@@ -1278,35 +1229,26 @@ fn campaign_register_refuses_invalid_cost_section_and_writes_nothing() {
);
}
/// Seed one blueprint (its params bound, reopened by the sweep axes per
/// #246) into the built demo project's store via a real sweep and return
/// its content id (the referential test's recipe).
/// Seed one blueprint into the built demo project's store via `aura graph
/// register` and return its content id (the referential test's recipe).
/// `#319` retired the `sweep`-side-effect seeding this used to ride (its
/// `--axis`/reopen mechanics played no role in the returned content id,
/// which is a pure function of the loaded blueprint's canonical bytes);
/// `name` labels the registered id for readability/uniqueness across call
/// sites, mirroring the old per-call sweep family name.
fn seed_blueprint(dir: &Path, name: &str) -> String {
let closed_bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let (out, code) = run_code_in(
dir,
&[
"sweep",
&closed_bp,
"--axis",
"fast.length=2,4",
"--axis",
"slow.length=8,16",
"--name",
name,
],
);
assert_eq!(code, Some(0), "seed sweep failed: {out}");
std::fs::read_dir(dir.join("runs").join("blueprints"))
.expect("blueprints dir")
let (out, code) = run_code_in(dir, &["graph", "register", &closed_bp, "--name", name]);
assert_eq!(code, Some(0), "seed register failed: {out}");
out.lines()
.find(|l| l.starts_with("registered blueprint "))
.expect("register line")
.trim_start_matches("registered blueprint ")
.split(' ')
.next()
.expect("one stored blueprint")
.expect("dir entry")
.path()
.file_stem()
.expect("stem")
.to_string_lossy()
.into_owned()
.expect("id")
.trim_start_matches("content:")
.to_string()
}
/// Register `doc` as a process document in the project store; returns its id.
@@ -1494,7 +1436,7 @@ const WF_PROCESS_DOC: &str = r#"{
fn campaign_run_outside_project_refuses() {
let dir = temp_cwd("campaign-run-outside-project");
write_doc(&dir, "c.campaign.json", CAMPAIGN_DOC);
let (out, code) = run_code_in(&dir, &["campaign", "run", "c.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "c.campaign.json"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(out.contains("campaign run needs a project"), "stdout/stderr: {out}");
assert!(
@@ -1508,7 +1450,7 @@ fn campaign_run_outside_project_refuses() {
#[test]
fn campaign_run_bogus_target_refuses() {
let (dir, _fixture) = fresh_project();
let (out, code) = run_code_in(&dir, &["campaign", "run", "no-such-target"]);
let (out, code) = run_code_in(&dir, &["exec", "no-such-target"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(
out.contains("'no-such-target' is neither a readable .json file nor a 64-hex content id"),
@@ -1521,7 +1463,7 @@ fn campaign_run_bogus_target_refuses() {
fn campaign_run_unknown_id_refuses() {
let (dir, _fixture) = fresh_project();
let id = "0".repeat(64);
let (out, code) = run_code_in(&dir, &["campaign", "run", &id]);
let (out, code) = run_code_in(&dir, &["exec", &id]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(
out.contains(&format!("no campaign {id} in the project store")),
@@ -1639,7 +1581,7 @@ fn campaign_run_refuses_mc_before_walk_forward() {
let bp_id = seed_blueprint(&dir, "campaign-run-mcwf-seed");
let proc_id = register_process_doc(&dir, "mcwf.process.json", MC_BEFORE_WF_PROCESS_DOC);
write_doc(&dir, "mcwf.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let (out, code) = run_code_in(&dir, &["campaign", "run", "mcwf.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "mcwf.campaign.json"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(out.contains("process pipeline is not executable:"), "stdout/stderr: {out}");
assert!(
@@ -1689,7 +1631,7 @@ fn campaign_run_refuses_a_non_terminal_selection_free_sweep() {
let proc_id =
register_process_doc(&dir, "selfree.process.json", SELECTION_FREE_SWEEP_THEN_GATE_PROCESS_DOC);
write_doc(&dir, "selfree.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let (out, code) = run_code_in(&dir, &["campaign", "run", "selfree.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "selfree.campaign.json"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(
out.contains(
@@ -1803,7 +1745,7 @@ fn campaign_run_refuses_a_grid_stage_not_immediately_before_walk_forward() {
"gridgate.campaign.json",
&campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""),
);
let (out, code) = run_code_in(&dir, &["campaign", "run", "gridgate.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "gridgate.campaign.json"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(out.contains("process pipeline is not executable:"), "stdout/stderr: {out}");
assert!(
@@ -1856,7 +1798,7 @@ fn campaign_run_synthetic_e2e_grid_then_wf_persists_no_sweep_family() {
"",
),
);
let (out, code) = run_code_in(&dir, &["campaign", "run", "gridwf.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "gridwf.campaign.json"]);
assert_eq!(code, Some(0), "campaign run failed: {out}");
let line = out
@@ -1918,7 +1860,7 @@ fn campaign_run_synthetic_e2e_cell_order_is_document_order_under_parallel_instru
for k in ["1", "2"] {
let (out, code) = run_code_in(
&dir,
&["campaign", "run", "cellorder.campaign.json", "--parallel-instruments", k],
&["exec", "cellorder.campaign.json", "--parallel-instruments", k],
);
assert_eq!(code, Some(0), "K={k}: campaign run failed: {out}");
let line = out
@@ -1979,7 +1921,7 @@ fn campaign_run_synthetic_e2e_parallel_instruments_contains_a_real_per_cell_faul
let (out, code) = run_code_in(
&dir,
&["campaign", "run", "parfault.campaign.json", "--parallel-instruments", "2"],
&["exec", "parfault.campaign.json", "--parallel-instruments", "2"],
);
assert_eq!(code, Some(3), "one contained fault, one success: {out}");
let line = out
@@ -2034,7 +1976,7 @@ fn campaign_run_synthetic_e2e_gate_emptied_cell_carries_the_note_marker() {
write_doc(&dir, "gateempty.campaign.json", &doc);
let (out, code) =
run_code_in(&dir, &["campaign", "run", "gateempty.campaign.json"]);
run_code_in(&dir, &["exec", "gateempty.campaign.json"]);
assert_eq!(code, Some(0), "a zero-survivor cell is a valid result, not a fault: {out}");
assert!(
out.contains("aura: note: cell "),
@@ -2066,7 +2008,7 @@ fn campaign_run_refuses_single_instrument_generalize() {
let bp_id = seed_blueprint(&dir, "campaign-run-gen1-seed");
let proc_id = register_process_doc(&dir, "gen1.process.json", GENERALIZE_PROCESS_DOC);
write_doc(&dir, "gen1.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let (out, code) = run_code_in(&dir, &["campaign", "run", "gen1.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "gen1.campaign.json"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(
out.contains("std::generalize needs at least 2"),
@@ -2127,7 +2069,7 @@ fn campaign_run_refuses_generalize_non_r_metric() {
}}"#
);
write_doc(&dir, "genpip.campaign.json", &campaign_doc);
let (out, code) = run_code_in(&dir, &["campaign", "run", "genpip.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "genpip.campaign.json"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
// #207 (fieldtest 0108 F8): the refusal names the REAL rule (the rankable
// R-expectancy family), never the mislabel "pip metric" — max_r_drawdown is
@@ -2191,7 +2133,7 @@ fn process_validate_permits_wf_after_mc_but_campaign_run_refuses_it() {
let bp_id = seed_blueprint(&dir, "campaign-run-wfmc-seed");
let proc_id = register_process_doc(&dir, "wfmc.process.json", WF_AFTER_MC_PROCESS_DOC);
write_doc(&dir, "wfmc.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let (out, code) = run_code_in(&dir, &["campaign", "run", "wfmc.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "wfmc.campaign.json"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(
out.contains("std::walk_forward cannot follow std::monte_carlo"),
@@ -2400,7 +2342,7 @@ fn campaign_run_refuses_zero_resamples_monte_carlo_before_any_member_runs() {
let bp_id = seed_blueprint(&dir, "campaign-run-zeromc-seed");
let proc_id = register_process_doc(&dir, "zeromc.process.json", ZERO_RESAMPLES_MC_PROCESS_DOC);
write_doc(&dir, "zeromc.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let (out, code) = run_code_in(&dir, &["campaign", "run", "zeromc.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "zeromc.campaign.json"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(
out.contains("process stage 1: monte_carlo resamples must be > 0"),
@@ -2435,7 +2377,7 @@ fn campaign_run_refuses_unknown_tap_at_validate() {
"badtap.campaign.json",
&campaign_doc_json(&bp_id, &proc_id, (1, 2), "\"r_record\"", ""),
);
let (out, code) = run_code_in(&dir, &["campaign", "run", "badtap.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "badtap.campaign.json"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(out.contains("campaign document invalid:"), "stdout/stderr: {out}");
assert!(
@@ -2478,7 +2420,7 @@ fn campaign_run_valid_tap_reaches_the_member_data_seam() {
"taps.campaign.json",
&campaign_doc_json(&bp_id, &proc_id, (1, 2), "\"equity\"", ""),
);
let (out, code) = run_code_in(&dir, &["campaign", "run", "taps.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "taps.campaign.json"]);
assert_eq!(code, Some(3), "stdout/stderr: {out}");
assert!(
!out.contains("unknown tap"),
@@ -2548,7 +2490,7 @@ fn campaign_run_real_e2e_sweep_gate_walkforward() {
"\"family_table\", \"selection_report\"",
),
);
let (out, code) = run_code_in(&dir, &["campaign", "run", "wf.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "wf.campaign.json"]);
// Skip on a data-less machine: the member-data refusal, never a panic.
if code == Some(3)
@@ -2725,7 +2667,7 @@ fn campaign_run_synthetic_e2e_cost_block_nets_the_pooled_oos_bootstrap() {
// Both runs share the store (the cost-e2e sibling pattern) — the record
// line is read from each run's own stdout, so no isolation is needed.
let pooled = |doc: &str| -> serde_json::Value {
let (out, code) = run_code_in(&dir, &["campaign", "run", doc]);
let (out, code) = run_code_in(&dir, &["exec", doc]);
assert_eq!(code, Some(0), "campaign run failed: {out}");
let line = out
.lines()
@@ -2802,7 +2744,7 @@ fn campaign_run_synthetic_e2e_cost_block_nets_the_per_survivor_bootstrap() {
write_doc(&dir, "net_ps.campaign.json", &with_cost);
let per_survivor = |doc: &str| -> Vec<(u64, serde_json::Value)> {
let (out, code) = run_code_in(&dir, &["campaign", "run", doc]);
let (out, code) = run_code_in(&dir, &["exec", doc]);
assert_eq!(code, Some(0), "campaign run failed: {out}");
let line = out
.lines()
@@ -2864,7 +2806,7 @@ fn campaign_run_synthetic_e2e_per_instrument_cost_resolves_per_cell() {
(1709251200000, 1717199999999),
);
write_doc(&dir, "percost.campaign.json", &doc);
let (out, code) = run_code_in(&dir, &["campaign", "run", "percost.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "percost.campaign.json"]);
assert_eq!(code, Some(0), "stdout/stderr: {out}");
// Member emit lines carry report.manifest; group the stamped resolved
@@ -2925,7 +2867,7 @@ fn campaign_run_synthetic_e2e_vol_tf_regime_realizes_and_stamps() {
);
assert_ne!(with_risk, base, "replacen must actually match the seed field");
write_doc(&dir, "voltf.campaign.json", &with_risk);
let (out, code) = run_code_in(&dir, &["campaign", "run", "voltf.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "voltf.campaign.json"]);
assert_eq!(code, Some(0), "stdout/stderr: {out}");
let record_line = out
@@ -3050,7 +2992,7 @@ fn campaign_run_real_e2e_bindings_override_rebinds_price_to_open() {
write_doc(&dir, "close.campaign.json", &base);
write_doc(&dir, "open.campaign.json", &rebound);
let (out_close, code_close) = run_code_in(&dir, &["campaign", "run", "close.campaign.json"]);
let (out_close, code_close) = run_code_in(&dir, &["exec", "close.campaign.json"]);
if code_close == Some(3)
&& (out_close.contains("no recorded geometry") || out_close.contains("no data for instrument"))
{
@@ -3058,7 +3000,7 @@ fn campaign_run_real_e2e_bindings_override_rebinds_price_to_open() {
return;
}
assert_eq!(code_close, Some(0), "stdout/stderr: {out_close}");
let (out_open, code_open) = run_code_in(&dir, &["campaign", "run", "open.campaign.json"]);
let (out_open, code_open) = run_code_in(&dir, &["exec", "open.campaign.json"]);
assert_eq!(code_open, Some(0), "stdout/stderr: {out_open}");
let first_member = |out: &str| -> serde_json::Value {
@@ -3113,7 +3055,7 @@ fn campaign_run_real_e2e_non_default_regime_stamps_its_own_stop() {
);
assert_ne!(with_regime, base, "replacen must actually match the fixture's seed field");
write_doc(&dir, "regime.campaign.json", &with_regime);
let (out, code) = run_code_in(&dir, &["campaign", "run", "regime.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "regime.campaign.json"]);
if code == Some(3)
&& (out.contains("no recorded geometry") || out.contains("no data for instrument"))
@@ -3144,6 +3086,68 @@ fn campaign_run_real_e2e_non_default_regime_stamps_its_own_stop() {
}
}
/// Property (#338, the `campaign_run_real_e2e_non_default_regime_stamps_its_own_stop`
/// precedent above, `Fixed` edition): a campaign document's `RiskRegime::Fixed`
/// reaches the real `CliMemberRunner::run_member` path and IS the stop actually
/// stamped into each emitted member's manifest as `stop_distance` — making the
/// shipped `FixedStop` composite campaign-reachable end to end, not merely
/// intrinsically valid.
#[test]
fn campaign_run_real_e2e_fixed_regime_stamps_its_own_distance() {
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
ScratchPath::Dir(runs_dir.clone()),
ScratchPath::File(dir.join("fixed_regime.process.json")),
ScratchPath::File(dir.join("fixed_regime.campaign.json")),
]);
let bp_id = seed_blueprint(&dir, "campaign-run-fixed-regime-seed");
let proc_id = register_process_doc(&dir, "fixed_regime.process.json", SWEEP_ONLY_PROCESS_DOC);
let base = campaign_doc_json(
&bp_id,
&proc_id,
(1725148800000, 1727740799999),
"",
"\"family_table\"",
);
let with_regime = base.replacen(
"\"seed\": 7,",
"\"seed\": 7,\n \"risk\": [ { \"fixed\": { \"distance\": 12.0 } } ],",
1,
);
assert_ne!(with_regime, base, "replacen must actually match the fixture's seed field");
write_doc(&dir, "fixed_regime.campaign.json", &with_regime);
let (out, code) = run_code_in(&dir, &["exec", "fixed_regime.campaign.json"]);
if code == Some(3)
&& (out.contains("no recorded geometry") || out.contains("no data for instrument"))
{
eprintln!("skip: no local GER40 data for the fixed-regime campaign e2e");
return;
}
assert_eq!(code, Some(0), "stdout/stderr: {out}");
let member_lines: Vec<&str> =
out.lines().filter(|l| l.starts_with("{\"family_id\":")).collect();
assert!(!member_lines.is_empty(), "family_table member lines emitted: {out}");
for line in &member_lines {
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
let params = v["report"]["manifest"]["params"]
.as_array()
.expect("manifest.params is an array");
let get = |name: &str| params.iter().find(|p| p[0].as_str() == Some(name));
assert_eq!(
get("stop_distance").and_then(|p| p[1]["F64"].as_f64()),
Some(12.0),
"the campaign's own fixed distance is stamped, not the R_SMA vol-stop default: {line}"
);
assert!(
get("stop_length").is_none() && get("stop_k").is_none(),
"a Fixed stop stamps no vol knobs: {line}"
);
}
}
/// Property (#210 T3/T4 — per-cell regime resolution, not a shared last-write):
/// a campaign with TWO distinct non-default risk regimes stamps EACH regime's
/// OWN stop into ITS OWN cell's members, never the other regime's values and
@@ -3183,7 +3187,7 @@ fn campaign_run_real_e2e_two_regimes_each_stamp_their_own_stop() {
);
assert_ne!(with_regimes, base, "replacen must actually match the fixture's seed field");
write_doc(&dir, "tworegimes.campaign.json", &with_regimes);
let (out, code) = run_code_in(&dir, &["campaign", "run", "tworegimes.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "tworegimes.campaign.json"]);
if code == Some(3)
&& (out.contains("no recorded geometry") || out.contains("no data for instrument"))
@@ -3270,7 +3274,7 @@ fn campaign_persist_non_default_regime_does_not_false_fail_c1() {
);
assert_ne!(with_regime, base, "replacen must actually match the fixture's seed field");
write_doc(&dir, "regime-persist.campaign.json", &with_regime);
let (out, code) = run_code_in(&dir, &["campaign", "run", "regime-persist.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "regime-persist.campaign.json"]);
// Skip on a data-less machine: the member-data refusal, never a panic.
if code == Some(3)
@@ -3350,7 +3354,7 @@ fn campaign_run_real_e2e_costed_campaign_persists_without_drift_alarm() {
);
assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field");
write_doc(&dir, "cost-persist.campaign.json", &with_cost);
let (out, code) = run_code_in(&dir, &["campaign", "run", "cost-persist.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "cost-persist.campaign.json"]);
// Skip on a data-less machine: the member-data refusal, never a panic.
if code == Some(3)
@@ -3445,7 +3449,7 @@ fn campaign_run_real_e2e_net_r_equity_tap_persists_the_cost_adjusted_curve() {
);
assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field");
write_doc(&dir, "net-persist.campaign.json", &with_cost);
let (out, code) = run_code_in(&dir, &["campaign", "run", "net-persist.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "net-persist.campaign.json"]);
// Skip on a data-less machine: the member-data refusal, never a panic.
if code == Some(3)
@@ -3559,7 +3563,7 @@ fn campaign_run_real_e2e_vol_slippage_cost_persists_without_drift_alarm() {
);
assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field");
write_doc(&dir, "volcost-persist.campaign.json", &with_cost);
let (out, code) = run_code_in(&dir, &["campaign", "run", "volcost-persist.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "volcost-persist.campaign.json"]);
// Skip on a data-less machine: the member-data refusal, never a panic.
if code == Some(3)
@@ -3648,7 +3652,7 @@ fn campaign_persist_two_regimes_land_in_distinct_trace_dirs() {
);
assert_ne!(with_regimes, base, "replacen must actually match the fixture's seed field");
write_doc(&dir, "tworegimes-persist.campaign.json", &with_regimes);
let (out, code) = run_code_in(&dir, &["campaign", "run", "tworegimes-persist.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "tworegimes-persist.campaign.json"]);
// Skip on a data-less machine: the member-data refusal, never a panic.
if code == Some(3)
@@ -3726,7 +3730,7 @@ fn campaign_run_real_e2e_skips_unproducible_tap_gracefully() {
"",
),
);
let (out, code) = run_code_in(&dir, &["campaign", "run", "mixedtap.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "mixedtap.campaign.json"]);
// Skip on a data-less machine: the member-data refusal, never a panic.
if code == Some(3)
@@ -3842,7 +3846,7 @@ fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() {
// (a) the cost-less baseline: net == gross, and net_r_equity skips with
// the remedy notice while equity still persists.
let (out_gross, code_gross) = run_code_in(&dir, &["campaign", "run", "costless.campaign.json"]);
let (out_gross, code_gross) = run_code_in(&dir, &["exec", "costless.campaign.json"]);
if code_gross == Some(3)
&& (out_gross.contains("no recorded geometry") || out_gross.contains("no data for instrument"))
{
@@ -3864,7 +3868,7 @@ fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() {
// (b) the costed run: gross identical member-for-member, net strictly
// below gross, no skip notice, net_r_equity persisted on disk.
let (out_net, code_net) = run_code_in(&dir, &["campaign", "run", "costnet.campaign.json"]);
let (out_net, code_net) = run_code_in(&dir, &["exec", "costnet.campaign.json"]);
assert_eq!(code_net, Some(0), "costed run: {out_net}");
assert!(
!out_net.contains("skipped"),
@@ -3963,7 +3967,7 @@ fn campaign_run_real_e2e_carry_cost_persists_without_drift_alarm() {
);
assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field");
write_doc(&dir, "carrycost-persist.campaign.json", &with_cost);
let (out, code) = run_code_in(&dir, &["campaign", "run", "carrycost-persist.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "carrycost-persist.campaign.json"]);
// Skip on a data-less machine: the member-data refusal, never a panic.
if code == Some(3)
@@ -4035,7 +4039,7 @@ fn campaign_run_real_e2e_sweep_monte_carlo_per_survivor() {
"mc.campaign.json",
&campaign_doc_json(&bp_id, &proc_id, (1725148800000, 1727740799999), "", ""),
);
let (out, code) = run_code_in(&dir, &["campaign", "run", "mc.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "mc.campaign.json"]);
// Skip on a data-less machine: the member-data refusal, never a panic.
if code == Some(3)
@@ -4106,7 +4110,7 @@ fn campaign_run_by_content_id_matches_file_sugar_refusal() {
let proc_id = register_process_doc(&dir, "mc2.process.json", MC_PROCESS_DOC);
write_doc(&dir, "mc2.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let (file_out, file_code) = run_code_in(&dir, &["campaign", "run", "mc2.campaign.json"]);
let (file_out, file_code) = run_code_in(&dir, &["exec", "mc2.campaign.json"]);
assert_eq!(file_code, Some(3), "stdout/stderr: {file_out}");
let (reg_out, reg_code) = run_code_in(&dir, &["campaign", "register", "mc2.campaign.json"]);
@@ -4121,7 +4125,7 @@ fn campaign_run_by_content_id_matches_file_sugar_refusal() {
.expect("id")
.trim_start_matches("content:")
.to_string();
let (id_out, id_code) = run_code_in(&dir, &["campaign", "run", &id]);
let (id_out, id_code) = run_code_in(&dir, &["exec", &id]);
assert_eq!(id_code, Some(3), "stdout/stderr: {id_out}");
let file_line = file_out.lines().find(|l| l.starts_with("aura: ")).expect("file refusal line");
@@ -4169,8 +4173,8 @@ fn campaign_run_tolerates_content_prefix_on_target() {
.trim_start_matches("content:")
.to_string();
let (bare_out, _) = run_code_in(&dir, &["campaign", "run", &id]);
let (pfx_out, _) = run_code_in(&dir, &["campaign", "run", &format!("content:{id}")]);
let (bare_out, _) = run_code_in(&dir, &["exec", &id]);
let (pfx_out, _) = run_code_in(&dir, &["exec", &format!("content:{id}")]);
let pfx_line = pfx_out.lines().find(|l| l.starts_with("aura: ")).expect("prefixed refusal line");
assert!(
@@ -4222,7 +4226,7 @@ fn campaign_run_accepts_a_graph_register_seeded_strategy() {
let proc_id = register_process_doc(&dir, "onramp.process.json", SWEEP_ONLY_PROCESS_DOC);
write_doc(&dir, "onramp.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let (out, code) = run_code_in(&dir, &["campaign", "run", "onramp.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "onramp.campaign.json"]);
assert_eq!(code, Some(3), "stdout/stderr: {out}");
assert!(
out.contains("no recorded geometry") || out.contains("no data for instrument"),
@@ -4251,7 +4255,7 @@ fn campaign_run_invalid_file_refuses_before_touching_store() {
let bad_path = write_doc(&dir, "bad.campaign.json", &bad);
let _cleanup = ScratchGuard(vec![ScratchPath::Dir(runs_dir.clone()), ScratchPath::File(bad_path)]);
let (out, code) = run_code_in(&dir, &["campaign", "run", "bad.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "bad.campaign.json"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(
out.contains("aura: campaign document invalid:"),
@@ -4304,7 +4308,7 @@ fn campaign_over_a_gapped_archive_records_the_uncovered_cell_and_continues() {
}}"#,
);
write_doc(&dir, "gap.campaign.json", &doc);
let (out, code) = run_code_in(&dir, &["campaign", "run", "gap.campaign.json"]);
let (out, code) = run_code_in(&dir, &["exec", "gap.campaign.json"]);
assert_eq!(code, Some(3), "stdout/stderr: {out}");
let record_line = out
@@ -4346,7 +4350,7 @@ fn campaign_over_a_gapped_archive_records_the_uncovered_cell_and_continues() {
/// clap refuses it as a usage error (exit 2) before any target resolution.
#[test]
fn campaign_run_rejects_a_zero_parallel_instruments_bound() {
let (out, code) = run_code(&["campaign", "run", "unused", "--parallel-instruments", "0"]);
let (out, code) = run_code(&["exec", "unused", "--parallel-instruments", "0"]);
assert_eq!(code, Some(2), "clap usage error expected, got: {out}");
// Domain prose, not clap's raw NonZeroUsize wording ("number would be
// zero for non-zero type") — the fieldtest friction finding.
+59 -2
View File
@@ -34,7 +34,7 @@ fn measurement_blueprint_runs_bare_and_emits_its_tap() {
std::fs::write(&bp_path, measurement_blueprint_json()).expect("write measurement blueprint");
let out = Command::new(BIN)
.args(["run", bp_path.to_str().expect("utf-8 path")])
.args(["exec", bp_path.to_str().expect("utf-8 path")])
.current_dir(&cwd)
.output()
.expect("spawn aura run");
@@ -65,7 +65,7 @@ fn measurement_run_honours_the_tap_selector() {
let bp_path = cwd.join("measurement.json");
std::fs::write(&bp_path, measurement_blueprint_json()).expect("write measurement blueprint");
let out = Command::new(BIN)
.args(["run", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=last"])
.args(["exec", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=last"])
.current_dir(&cwd)
.output()
.expect("spawn aura run");
@@ -81,3 +81,60 @@ fn measurement_run_honours_the_tap_selector() {
let expected = (1.0097_f64 + 1.0092) / 2.0;
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(["exec", 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"
);
}
@@ -38,7 +38,7 @@ fn assert_clean_refusal(bp_json: &str, tag: &str, what: &str) {
std::fs::write(&bp_path, bp_json).expect("write blueprint");
let out = Command::new(BIN)
.args(["run", bp_path.to_str().expect("utf-8 path")])
.args(["exec", bp_path.to_str().expect("utf-8 path")])
.current_dir(&cwd)
.output()
.expect("spawn aura run");
+12 -12
View File
@@ -53,7 +53,7 @@ fn single_run_persists_a_declared_tap_series() {
std::fs::write(&bp_path, tap_blueprint_json()).expect("write tapped blueprint");
let out = Command::new(BIN)
.args(["run", bp_path.to_str().expect("utf-8 path")])
.args(["exec", bp_path.to_str().expect("utf-8 path")])
.current_dir(&cwd)
.output()
.expect("spawn aura run");
@@ -100,7 +100,7 @@ fn tap_free_run_writes_no_trace_store() {
let cwd = temp_cwd("tap-free-writes-nothing");
let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let out = Command::new(BIN)
.args(["run", &bp])
.args(["exec", &bp])
.current_dir(&cwd)
.output()
.expect("spawn aura run");
@@ -152,7 +152,7 @@ fn single_run_refuses_a_duplicate_tap_name_before_persisting_anything() {
std::fs::write(&bp_path, duplicate_tap_blueprint_json()).expect("write dup-tap blueprint");
let out = Command::new(BIN)
.args(["run", bp_path.to_str().expect("utf-8 path")])
.args(["exec", bp_path.to_str().expect("utf-8 path")])
.current_dir(&cwd)
.output()
.expect("spawn aura run");
@@ -178,7 +178,7 @@ fn an_unwritable_store_root_exits_through_the_tap_trace_register() {
std::fs::write(cwd.join("runs"), b"not a directory").expect("occupy runs as a file");
let out = Command::new(BIN)
.args(["run", bp_path.to_str().expect("utf-8 path")])
.args(["exec", bp_path.to_str().expect("utf-8 path")])
.current_dir(&cwd)
.output()
.expect("spawn aura run");
@@ -210,7 +210,7 @@ fn a_read_only_run_dir_surfaces_terminally_through_the_tap_trace_register() {
.expect("make run dir read-only");
let out = Command::new(BIN)
.args(["run", bp_path.to_str().expect("utf-8 path")])
.args(["exec", bp_path.to_str().expect("utf-8 path")])
.current_dir(&cwd)
.output()
.expect("spawn aura run");
@@ -239,7 +239,7 @@ fn run_tap_selector_persists_a_fold_summary_row() {
std::fs::write(&bp_path, tap_blueprint_json()).expect("write tapped blueprint");
let out = Command::new(BIN)
.args(["run", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=mean"])
.args(["exec", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=mean"])
.current_dir(&cwd)
.output()
.expect("spawn aura run");
@@ -276,7 +276,7 @@ fn run_explicit_record_plan_matches_the_record_all_default() {
.expect("write two-tap blueprint");
}
let run = |cwd: &std::path::Path, extra: &[&str]| {
let mut args = vec!["run", "two_tap.json"];
let mut args = vec!["exec", "two_tap.json"];
args.extend_from_slice(extra);
let out = Command::new(BIN)
.args(&args)
@@ -309,7 +309,7 @@ fn run_tap_selector_refuses_an_unknown_label_with_the_roster() {
let bp_path = cwd.join("tapped_r_sma.json");
std::fs::write(&bp_path, tap_blueprint_json()).expect("write tapped blueprint");
let out = Command::new(BIN)
.args(["run", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=medain"])
.args(["exec", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=medain"])
.current_dir(&cwd)
.output()
.expect("spawn aura run");
@@ -334,7 +334,7 @@ fn run_tap_selector_refuses_an_undeclared_tap() {
let bp_path = cwd.join("two_tap.json");
std::fs::write(&bp_path, two_tap_blueprint_json()).expect("write two-tap blueprint");
let out = Command::new(BIN)
.args(["run", bp_path.to_str().expect("utf-8 path"), "--tap", "nope=mean"])
.args(["exec", bp_path.to_str().expect("utf-8 path"), "--tap", "nope=mean"])
.current_dir(&cwd)
.output()
.expect("spawn aura run");
@@ -359,7 +359,7 @@ fn run_tap_selector_notes_unbound_declared_taps_on_stderr() {
std::fs::write(&bp_path, two_tap_blueprint_json()).expect("write two-tap blueprint");
let out = Command::new(BIN)
.args(["run", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=mean"])
.args(["exec", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=mean"])
.current_dir(&cwd)
.output()
.expect("spawn aura run");
@@ -384,7 +384,7 @@ fn run_with_no_tap_flag_emits_no_unbound_note() {
std::fs::write(&bp_path, two_tap_blueprint_json()).expect("write two-tap blueprint");
let out = Command::new(BIN)
.args(["run", bp_path.to_str().expect("utf-8 path")])
.args(["exec", bp_path.to_str().expect("utf-8 path")])
.current_dir(&cwd)
.output()
.expect("spawn aura run");
@@ -407,7 +407,7 @@ fn run_tap_selector_refuses_malformed_and_duplicate_pairs() {
vec!["--tap", "fast_tapmean"],
vec!["--tap", "fast_tap=mean", "--tap", "fast_tap=last"],
] {
let mut full = vec!["run", bp_path.to_str().expect("utf-8 path")];
let mut full = vec!["exec", bp_path.to_str().expect("utf-8 path")];
full.extend(args);
let out = Command::new(BIN)
.args(&full)
+148 -2
View File
@@ -195,6 +195,38 @@ pub struct GangMember {
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 [`Node`]: it is never `eval`'d. It holds interior items (local indices),
/// interior edges (local indices), input roles (role `r` fans into the interior
@@ -329,6 +361,24 @@ impl Composite {
out
}
/// Every declared measurement tap across the whole blueprint, depth-first
/// through nested composites — the taps-as-data twin of `param_space()`
/// (#337, the positive discovery view #333's refusal roster deliberately
/// deferred). Each entry is `(tap name, source wire, column kind)`, the
/// wire rendered `<node>.<field>` in the LOCAL frame the tap was declared
/// in. Unlike a param's path, neither the tap's own name nor its wire's
/// node identifier is prefixed by an enclosing composite: a tap hoists
/// BARE to the flat graph (`inline_composite` keeps `tap.name` verbatim),
/// so this view renders exactly the load-bearing name `compile` keeps.
/// Bounds-total like `derive_signature`: a structurally-invalid wire (out
/// of a producer's output arity) yields no row rather than a panic — the
/// real gate is `compile`'s own `resolve_tap_wire`.
pub fn declared_taps(&self) -> Vec<(String, String, ScalarKind)> {
let mut out = Vec::new();
collect_taps(self, &mut out);
out
}
/// Re-open ONE bound param at its `param_space()`-style path (#246): the
/// param returns to the open surface at the slot `collect_params` order
/// dictates; its bound value is forgotten by this value (the authored
@@ -542,7 +592,8 @@ pub enum ReopenError {
/// One entry of the aggregated BOUND param surface (#246): the path-qualified
/// twin of [`ParamSpec`] carrying the bound value — the default a sweep axis
/// may override, and what `--list-axes` renders as `default=`.
/// may override, and what `aura graph introspect --params` renders as
/// `default=`.
#[derive(Debug, Clone, PartialEq)]
pub struct BoundSpec {
pub name: String,
@@ -1098,6 +1149,31 @@ fn collect_params(items: &[BlueprintNode], gangs: &[Gang], prefix: &str, out: &m
}
}
/// Recursive walk for `Composite::declared_taps` (#337): this level's OWN
/// declared taps first (resolved against this level's OWN `nodes` — a tap's
/// `from` wire is always local to the frame it was declared in), then
/// recurse into every nested composite's own tap list. No prefix threading
/// (unlike `collect_params`): a tap's name and its wire's node identifier
/// stay bare at every depth, mirroring `inline_composite`'s own hoist.
fn collect_taps(c: &Composite, out: &mut Vec<(String, String, ScalarKind)>) {
for tap in &c.taps {
if let Some(item) = c.nodes.get(tap.from.node) {
let node_name = match item {
BlueprintNode::Primitive(b) => b.node_name(),
BlueprintNode::Composite(inner) => inner.name().to_string(),
};
if let Some(field) = item.signature().output.get(tap.from.field) {
out.push((tap.name.clone(), format!("{node_name}.{}", field.name), field.kind));
}
}
}
for item in &c.nodes {
if let BlueprintNode::Composite(inner) = item {
collect_taps(inner, out);
}
}
}
/// Recursive mutable walk for `Composite::reopen` (#246): mirrors
/// `collect_params`' prefix rules (lockstep with `expansion_map`) — a leaf owns
/// `<node>.<param>`, a composite prefixes its `name()` and recurses. Returns
@@ -1519,6 +1595,19 @@ mod tests {
use aura_strategy::Bias;
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
/// value carries the gang table.
#[test]
@@ -3331,7 +3420,8 @@ mod tests {
}
/// #246: bound_param_space mirrors param_space's path qualification and
/// carries the bound value (the default the CLI renders in --list-axes).
/// carries the bound value (the default the CLI renders in
/// `aura graph introspect --params`).
#[test]
fn bound_param_space_is_path_qualified_with_values() {
use aura_strategy::Bias;
@@ -3907,4 +3997,60 @@ mod tests {
// the inner Sub lowered to flat node 0; the hoisted tap points at it
assert_eq!(flat.taps, vec![crate::harness::FlatTap { name: "inner_d".into(), node: 0, field: 0 }]);
}
/// The build-free discovery twin (#337) of the compile-time hoist above:
/// `declared_taps()` finds the SAME nested-composite tap without ever
/// compiling — its wire is rendered in the LOCAL frame it was declared in
/// (`sub.value`, the un-named Sub's default node name + its own output
/// field), never composite-path-prefixed (unlike a param's path).
#[test]
fn declared_taps_finds_a_nested_composites_tap_without_compiling() {
let inner = Composite::new(
"inner",
vec![Sub::builder().into()],
vec![],
vec![Role {
name: "x".into(),
targets: vec![Target { node: 0, slot: 0 }, Target { node: 0, slot: 1 }],
source: None,
}],
vec![OutField { node: 0, field: 0, name: "o".into() }],
)
.with_taps(vec![Tap { name: "inner_d".into(), from: TapWire { node: 0, field: 0 } }]);
let root = Composite::new(
"root",
vec![BlueprintNode::Composite(inner)],
vec![],
vec![Role {
name: "a".into(),
targets: vec![Target { node: 0, slot: 0 }],
source: Some(ScalarKind::F64),
}],
vec![],
);
assert_eq!(
root.declared_taps(),
vec![("inner_d".to_string(), "sub.value".to_string(), ScalarKind::F64)]
);
}
/// Bounds-total (mirrors `derive_signature`): a tap wire naming a field
/// beyond its producer's output arity yields no row rather than a panic —
/// the real gate stays `compile`'s own `TapWireOutOfRange`.
#[test]
fn declared_taps_is_bounds_total_over_an_out_of_range_wire() {
let bp = Composite::new(
"m",
vec![Sub::builder().into()],
vec![],
vec![Role {
name: "a".into(),
targets: vec![Target { node: 0, slot: 0 }, Target { node: 0, slot: 1 }],
source: Some(ScalarKind::F64),
}],
vec![],
)
.with_taps(vec![Tap { name: "bad".into(), from: TapWire { node: 0, field: 9 } }]);
assert_eq!(bp.declared_taps(), Vec::new(), "an out-of-range wire yields no row, not a panic");
}
}
+90 -13
View File
@@ -103,9 +103,33 @@ pub struct ArgData {
#[derive(Debug)]
pub enum SerializeError {
Json(serde_json::Error),
/// #341: an arg-bearing (`PrimitiveBuilder::pending`) node reached the
/// serialize seam still unconfigured. Only the Rust `GraphBuilder`/
/// `Composite::new` path can build this state — the data path
/// (`add_node`) always runs `try_args` first — but emitting it as an
/// args-free v1 document would produce a document `blueprint_from_json`
/// can only refuse as `LoadError::BadArg(ArgOpError::MissingArg(..))` at
/// LOAD time; refusing here instead names the fault at its origin.
/// `missing_args` is the pending recipe's full declared `ArgSpec` list
/// (none configured yet, so every declared arg is missing).
PendingBuilder { node_type: String, missing_args: Vec<String> },
}
fn project(c: &Composite) -> CompositeData {
impl std::fmt::Display for SerializeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SerializeError::Json(e) => write!(f, "{e}"),
SerializeError::PendingBuilder { node_type, missing_args } => write!(
f,
"node \"{node_type}\" is an unconfigured arg-bearing builder (missing arg(s): \
{}); call try_args to configure it before it enters a graph",
missing_args.join(", ")
),
}
}
}
fn project(c: &Composite) -> Result<CompositeData, SerializeError> {
// Canonical gang order (bind-order-independent, mirroring `project_node`'s
// bound-param canonicalization below): each gang's members ascending
// `(node, pos)`, then the gangs themselves ordered by their (now-sorted)
@@ -116,21 +140,30 @@ fn project(c: &Composite) -> CompositeData {
}
gangs.sort_by_key(|g| (g.members[0].node, g.members[0].pos));
CompositeData {
Ok(CompositeData {
name: c.name().to_string(),
doc: c.doc().map(str::to_string),
nodes: c.nodes().iter().map(project_node).collect(),
nodes: c.nodes().iter().map(project_node).collect::<Result<Vec<_>, _>>()?,
edges: c.edges().to_vec(),
input_roles: c.input_roles().to_vec(),
output: c.output().to_vec(),
taps: c.taps().to_vec(),
gangs,
}
})
}
fn project_node(n: &BlueprintNode) -> NodeData {
fn project_node(n: &BlueprintNode) -> Result<NodeData, SerializeError> {
match n {
BlueprintNode::Primitive(b) => {
// #341: refuse a still-pending arg-bearing recipe outright — an
// args-free v1 document built from it would only surface its
// fault at load (`LoadError::BadArg(MissingArg)`), never here.
if b.is_pending() {
return Err(SerializeError::PendingBuilder {
node_type: b.label(),
missing_args: b.arg_specs().iter().map(|s| s.name.to_string()).collect(),
});
}
// Canonical: bound params in ascending original-slot order, mirroring
// the loader's re-bind canonicalization, so serialization is
// bind-order-independent (a precondition for content-addressing, #158).
@@ -147,14 +180,14 @@ fn project_node(n: &BlueprintNode) -> NodeData {
.iter()
.map(|ConstructionArg { name, value }| ArgData { name: name.clone(), value: value.clone() })
.collect();
NodeData::Primitive(PrimitiveData {
Ok(NodeData::Primitive(PrimitiveData {
type_id: b.label(),
name: b.instance_name().map(str::to_string),
args,
bound,
})
}))
}
BlueprintNode::Composite(c) => NodeData::Composite(project(c)),
BlueprintNode::Composite(c) => Ok(NodeData::Composite(project(c)?)),
}
}
@@ -180,10 +213,10 @@ fn document_version(b: &CompositeData) -> u32 {
}
}
fn build_doc(c: &Composite) -> BlueprintDoc {
let blueprint = project(c);
fn build_doc(c: &Composite) -> Result<BlueprintDoc, SerializeError> {
let blueprint = project(c)?;
let format_version = document_version(&blueprint);
BlueprintDoc { format_version, blueprint }
Ok(BlueprintDoc { format_version, blueprint })
}
fn serialize_doc(doc: &BlueprintDoc) -> Result<String, SerializeError> {
@@ -194,7 +227,7 @@ fn serialize_doc(doc: &BlueprintDoc) -> Result<String, SerializeError> {
/// field order, defaults omitted (`skip_serializing_if`). An absent optional is
/// byte-identical to the pre-extension form.
pub fn blueprint_to_json(c: &Composite) -> Result<String, SerializeError> {
serialize_doc(&build_doc(c))
serialize_doc(&build_doc(c)?)
}
/// The identity-canonical form (#171): the canonical document with every
@@ -207,7 +240,7 @@ pub fn blueprint_to_json(c: &Composite) -> Result<String, SerializeError> {
/// form ONLY — never a load path and never the reproduction store's byte form
/// (`reproduce` re-binds params by name, so instance names are load-bearing there).
pub fn blueprint_identity_json(c: &Composite) -> Result<String, SerializeError> {
let mut doc = build_doc(c);
let mut doc = build_doc(c)?;
strip_debug_symbols(&mut doc.blueprint);
serialize_doc(&doc)
}
@@ -1017,4 +1050,48 @@ mod tests {
"an arg-bearing type with no args refuses as MissingArg, got {err:?}"
);
}
/// #341 (refuse at the serialize seam, not just at load): a Rust-built
/// graph holding a still-`Session::builder()`-pending node (no
/// `try_args` applied) refuses to serialize outright — the twin of
/// `loading_arg_bearing_type_without_args_refuses` at the OTHER end of
/// the wire, the fault named at its origin instead of surfacing only
/// once the emitted document is reloaded. Only the Rust `GraphBuilder`
/// path can build this state; `add_node`'s data path always runs
/// `try_args` first (fenced separately).
#[test]
fn project_node_refuses_a_pending_arg_bearing_builder() {
let mut gb = crate::GraphBuilder::new("has_pending");
gb.add(Session::builder());
let composite = gb.build().expect("an unwired single-node graph builds");
let err = blueprint_to_json(&composite).expect_err("a pending builder must not serialize");
assert!(
err.to_string().contains("try_args"),
"the refusal prose hints at the fix: {err}"
);
match err {
SerializeError::PendingBuilder { node_type, missing_args } => {
assert_eq!(node_type, "Session", "names the pending node's type: {missing_args:?}");
assert_eq!(
missing_args,
vec!["tz".to_string(), "open".to_string()],
"names every declared arg as missing (none configured yet)"
);
}
SerializeError::Json(e) => panic!("wrong error variant: {e:?}"),
}
}
/// The fenced happy path (#341, negative twin): a `try_args`-configured
/// `Session` (the SAME recipe, just no longer pending) serializes fine —
/// `session_arg_fixture`'s existing round-trip coverage
/// (`args_round_trip_preserves_pairs_and_version_2`) already proves
/// this; re-asserted here beside the refusal for the reader who lands on
/// one and wants the other in view.
#[test]
fn project_node_serializes_a_configured_arg_bearing_builder() {
let c = session_arg_fixture(chrono_tz::Europe::Berlin);
assert!(blueprint_to_json(&c).is_ok(), "a configured builder serializes");
}
}
+170 -19
View File
@@ -16,8 +16,8 @@ use std::collections::{HashMap, HashSet};
use aura_core::{ArgOpError, BindOpError, NodeSchema, PrimitiveBuilder, Scalar, ScalarKind};
use crate::blueprint::{
check_param_namespace_injective, edge_kind_check, validate_wiring, BlueprintNode, Composite,
Gang, GangMember, OutField, Role, Tap, TapWire,
check_param_namespace_injective, edge_kind_check, name_gate, validate_wiring, BlueprintNode,
Composite, Gang, GangMember, NameGateFault, OutField, Role, Tap, TapWire,
};
use crate::builder::{resolve_input_slot, resolve_output_field, PortResolveError};
use crate::harness::{Edge, Target};
@@ -64,6 +64,8 @@ pub enum Op {
/// `bind` is applied path-qualified (e.g. `"sma.length"`), after the
/// splice.
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.
@@ -114,6 +116,14 @@ pub enum OpError {
AlreadyGanged { node: String, param: String },
/// A gang needs at least two members.
GangArity { gang: String },
/// A gang member path resolves onto a spliced instance (`Op::Use`'s
/// `Composite` node), not a primitive (#339 item 1 harvest): a gang
/// fuses a PRIMITIVE's raw `(name, pos)` param slot, and an instance's
/// params are nested/path-qualified, so ganging a used instance's
/// member params is unsupported today. Distinct from
/// `BadParam::UnknownParam` so the refusal names the rule instead of
/// reading like a typo hint on the leaf case.
GangOfSplicedInstance { node: String, member: String },
/// A holistic finalize fault (totality / injectivity / unbound root role),
/// wrapping the unchanged engine gate's `CompileError`.
Incomplete(CompileError),
@@ -124,6 +134,13 @@ pub enum OpError {
/// resolves and fetches before replay, refusing there on a miss), but
/// total: a bare `replay`/`GraphSession` caller can still hit it.
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
@@ -137,7 +154,11 @@ pub struct GraphSession<'v> {
/// The engine stays store-free — CLI-side resolution (label/prefix,
/// C29 doc gate, the store fetch itself) happens at DTO conversion,
/// before replay; this closure is a pure lookup into an already-fetched
/// id->`Composite` cache. Build-free introspection paths pass `&|_| None`.
/// id->`Composite` cache. The engine places no obligation on a caller
/// either way: a build-free CLI path MAY still resolve `use` refs
/// through the store first and pass a real cache-backed closure here
/// (#339 item 4 harvest: `introspect --unwired` now does); a truly
/// resolver-less caller (a bare `replay`, most tests) passes `&|_| None`.
subgraph: &'v dyn Fn(&str) -> Option<Composite>,
nodes: Vec<BlueprintNode>,
schemas: Vec<NodeSchema>,
@@ -153,6 +174,11 @@ pub struct GraphSession<'v> {
coverage: HashMap<(usize, usize), usize>,
gangs: Vec<Gang>,
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> {
@@ -182,6 +208,7 @@ impl<'v> GraphSession<'v> {
coverage: HashMap::new(),
gangs: Vec::new(),
doc: None,
name_authored: false,
}
}
@@ -205,9 +232,29 @@ impl<'v> GraphSession<'v> {
Ok(())
}
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
/// `subgraph` resolver, rename it to the instance identifier, apply its
/// path-qualified `bind` entries, and push it as an ordinary
@@ -455,12 +502,11 @@ impl<'v> GraphSession<'v> {
// Composite, not just a Primitive — reachable now, not a
// defect. A gang fuses a PRIMITIVE's raw (name, pos) param
// slot; an instance has no such flat slot (its params are
// nested/path-qualified), so this is the ordinary
// no-such-open-param refusal, not a panic.
return Err(OpError::BadParam {
node: node_name,
err: BindOpError::UnknownParam(param_name),
});
// nested/path-qualified). #339 item 1 harvest: name the
// rule here instead of falling into the ordinary
// no-such-open-param shape, which reads as a typo hint on
// an otherwise correct member path.
return Err(OpError::GangOfSplicedInstance { node: node_name, member: param_name });
};
let hits: Vec<usize> = b
.params()
@@ -1002,6 +1048,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]
fn finish_reports_incomplete_on_unwired_slot() {
// a fully-named scaffold missing the sub.rhs connection -> holistic 0-arm
@@ -1501,6 +1613,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
/// instance faults through the existing bind-fault shape (`BadParam`),
/// naming the instance and the qualified within-instance path (#317).
@@ -1575,12 +1711,14 @@ mod tests {
);
}
/// Documented residue pin (#317 audit): a `gang` op cannot fuse a spliced
/// instance's member param — a gang fuses a PRIMITIVE's raw `(name, pos)`
/// slot, and an instance's params are nested/path-qualified. The refusal
/// is the ordinary no-such-open-param shape on the instance node, so the
/// authoring-guide/C24 claim "the gang op refuses a composite instance's
/// member path" stays an observed fact, not prose.
/// Documented residue pin (#317 audit, prose updated #339 item 1
/// harvest): a `gang` op cannot fuse a spliced instance's member param —
/// a gang fuses a PRIMITIVE's raw `(name, pos)` slot, and an instance's
/// params are nested/path-qualified. The refusal now names the rule
/// (`OpError::GangOfSplicedInstance`) instead of the bare
/// no-such-open-param shape, so it reads as "instance-member ganging is
/// unsupported", not as a typo hint — matching the authoring-guide/C24
/// claim "the gang op refuses a composite instance's member path".
#[test]
fn gang_of_a_spliced_instance_member_path_refuses() {
let subgraph = |_: &str| Some(use_fixture());
@@ -1594,10 +1732,23 @@ mod tests {
as_name: "fused".into(),
into: vec!["gate.sma.length".into(), "solo.length".into()],
}),
Err(OpError::BadParam {
node: "gate".into(),
err: BindOpError::UnknownParam("sma.length".into()),
})
Err(OpError::GangOfSplicedInstance { node: "gate".into(), member: "sma.length".into() })
);
}
/// Sibling pin to `gang_of_a_spliced_instance_member_path_refuses`: the
/// plain-typo case — a `gang` member naming a param that never existed
/// on a PRIMITIVE (leaf) node — stays the ordinary no-such-open-param
/// shape (`BindOpError::UnknownParam`), unaffected by the instance-path
/// rule above (#339 item 1 harvest).
#[test]
fn gang_of_a_leaf_node_with_a_wrong_param_name_refuses_as_unknown_param() {
let mut s = session("g");
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("a".into()), args: vec![], bind: vec![] }).unwrap();
s.apply(Op::Add { type_id: "SMA".into(), as_name: Some("b".into()), args: vec![], bind: vec![] }).unwrap();
assert_eq!(
s.apply(Op::Gang { as_name: "typo".into(), into: vec!["a.lenght".into(), "b.length".into()] }),
Err(OpError::BadParam { node: "a".into(), err: BindOpError::UnknownParam("lenght".into()) })
);
}
+3 -2
View File
@@ -52,8 +52,9 @@ mod sweep;
mod walkforward;
pub use blueprint::{
BindError, Binder, BlueprintNode, BoundSpec, CompileError, Composite, Gang, GangFault,
GangMember, OutField, RandomBinder, ReopenError, Role, SweepBinder, Tap, TapWire,
name_gate, BindError, Binder, BlueprintNode, BoundSpec, CompileError, Composite, Gang,
GangFault, GangMember, NameGateFault, OutField, RandomBinder, ReopenError, Role, SweepBinder,
Tap, TapWire,
};
pub use blueprint_serde::{
blueprint_from_json, blueprint_identity_json, blueprint_to_json, ArgData, BlueprintDoc,
+5
View File
@@ -31,6 +31,11 @@ pub enum FamilyKind {
Sweep,
MonteCarlo,
WalkForward,
/// Dead machinery since the #319 sugar retirement (C18): no producer
/// mints a merged cross-instrument family anymore — the campaign-run
/// record's `generalizations[]` carries the data. The variant stays for
/// store read-back of historically minted families (never retroactively
/// invalidated, C29).
CrossInstrument,
}
+96 -8
View File
@@ -513,16 +513,17 @@ pub struct Axis {
}
/// A protective-stop regime: a serializable, content-addressable mirror of the
/// runtime `StopRule` structural axis (C10/C20). The sole implemented variant is
/// the vol-stop; the shipped fixed-stop rule is admitted as a future additive
/// variant (it runs as a composite today but is not yet campaign-reachable).
/// Externally tagged so adding `Fixed` is additive — no content-id churn on
/// stored `Vol` regimes.
/// runtime `StopRule` structural axis (C10/C20). `Fixed{distance}` (#338) makes
/// the shipped `FixedStop` composite campaign-reachable, beside the vol-stop
/// family — `distance` is the same price-unit knob `FixedStop`'s own `distance`
/// param carries. Externally tagged so each variant was additive on arrival —
/// no content-id churn on stored `Vol`/`VolTf` regimes.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "snake_case")]
pub enum RiskRegime {
Vol { length: i64, k: f64 },
VolTf { period_minutes: i64, length: i64, k: f64 },
Fixed { distance: f64 },
}
/// One component of the campaign's cost model (#234): a closed, additive
@@ -973,6 +974,14 @@ pub fn validate_campaign(doc: &CampaignDoc) -> Vec<DocFault> {
faults.push(DocFault::BadRegime { index: i });
}
}
RiskRegime::Fixed { distance } => {
// Mirrors `FixedStop::new`'s own `distance > 0.0` assert
// (`aura-strategy`) — refused gracefully here instead of
// reaching that assert and panicking.
if *distance <= 0.0 || distance.is_nan() {
faults.push(DocFault::BadRegime { index: i });
}
}
}
}
for (i, c) in doc.cost.iter().enumerate() {
@@ -1309,14 +1318,31 @@ pub fn open_slots_campaign(text: &str) -> Result<Vec<OpenSlot>, DocError> {
if v.get("seed").and_then(|s| s.as_u64()).is_none() {
slots.push(open("seed", "required, non-negative integer"));
}
if v.get("presentation").is_none() {
slots.push(open(
match v.get("presentation") {
None => slots.push(open(
"presentation",
format!(
"required section: persist_taps ({}) + emit",
tap_vocabulary().iter().map(|t| t.id).collect::<Vec<_>>().join(" | ")
),
));
)),
Some(p) => {
if p.get("persist_taps").and_then(|x| x.as_array()).is_none() {
slots.push(open(
"presentation.persist_taps",
format!(
"required, list of: {}",
tap_vocabulary().iter().map(|t| t.id).collect::<Vec<_>>().join(" | ")
),
));
}
if p.get("emit").and_then(|x| x.as_array()).is_none() {
slots.push(open(
"presentation.emit",
format!("required, list of: {}", emit_vocabulary().join(" | ")),
));
}
}
}
Ok(slots)
}
@@ -1649,6 +1675,18 @@ mod tests {
assert_eq!(back, r);
}
/// #338: the third variant's wire form follows the same externally-tagged
/// convention as `vol`/`vol_tf` — `fixed` snake_case tag, one named field
/// (the `FixedStop` composite's own `distance` param).
#[test]
fn risk_regime_round_trips_as_externally_tagged_fixed() {
let r = RiskRegime::Fixed { distance: 10.0 };
let j = serde_json::to_string(&r).unwrap();
assert_eq!(j, r#"{"fixed":{"distance":10.0}}"#);
let back: RiskRegime = serde_json::from_str(&j).unwrap();
assert_eq!(back, r);
}
#[test]
fn campaign_absent_and_empty_risk_omit_from_canonical_bytes() {
let doc: CampaignDoc = serde_json::from_str(CAMPAIGN_FIXTURE).unwrap();
@@ -1707,6 +1745,26 @@ mod tests {
assert!(!faults.contains(&DocFault::BadRegime { index: 0 }), "valid regime not flagged: {faults:?}");
}
/// #338 (harvest audit item 11 extends the NaN branch): a non-positive OR
/// NaN fixed-stop distance must be refused as a graceful
/// `DocFault::BadRegime` at the doc tier — otherwise it reaches
/// `FixedStop::new`'s `distance > 0.0` assert and panics instead of a
/// validation error (the `validate_campaign_flags_non_positive_period_minutes_regime`
/// precedent above, `Fixed` edition).
#[test]
fn validate_campaign_flags_non_positive_fixed_distance_regime() {
let mut doc: CampaignDoc = serde_json::from_str(CAMPAIGN_FIXTURE).unwrap();
doc.risk = vec![
RiskRegime::Fixed { distance: 10.0 }, // 0: valid
RiskRegime::Fixed { distance: 0.0 }, // 1: distance <= 0
RiskRegime::Fixed { distance: f64::NAN }, // 2: NaN (unreachable via JSON; defensive)
];
let faults = validate_campaign(&doc);
assert!(faults.contains(&DocFault::BadRegime { index: 1 }), "{faults:?}");
assert!(faults.contains(&DocFault::BadRegime { index: 2 }), "{faults:?}");
assert!(!faults.contains(&DocFault::BadRegime { index: 0 }), "valid regime not flagged: {faults:?}");
}
#[test]
fn validate_campaign_accepts_a_valid_risk_section() {
let mut doc: CampaignDoc = serde_json::from_str(CAMPAIGN_FIXTURE).unwrap();
@@ -2294,6 +2352,36 @@ mod tests {
);
}
/// #319 fieldtest bug 1: a present-but-empty `presentation: {}` must drill
/// down into its two required sub-slots exactly as a present-but-empty
/// `data: {}` drills into `instruments`/`windows` — the walker must not
/// stop at "the section exists" without checking what's inside it.
#[test]
fn presentation_drills_into_an_empty_section_like_data_does() {
let draft = r#"{ "format_version": 1, "kind": "campaign", "name": "draft",
"data": { "instruments": ["GER40"], "windows": [ { "from_ms": 1, "to_ms": 2 } ] },
"strategies": [ { "ref": { "content_id": "9f3a" },
"axes": { "slow": { "kind": "I64", "values": [10] } } } ],
"process": { "ref": { "content_id": "4e2d" } },
"seed": 1,
"presentation": {} }"#;
let slots = open_slots_campaign(draft).unwrap();
assert!(
!slots.iter().any(|s| s.path == "presentation"),
"an empty-but-present presentation is not the same open slot as an absent one: {slots:?}"
);
assert!(
slots.iter().any(|s| s.path == "presentation.persist_taps"
&& s.hint == "required, list of: equity | exposure | r_equity | net_r_equity"),
"presentation.persist_taps must be named as its own open slot: {slots:?}"
);
assert!(
slots.iter().any(|s| s.path == "presentation.emit"
&& s.hint == "required, list of: family_table | selection_report"),
"presentation.emit must be named as its own open slot: {slots:?}"
);
}
/// #256 fork B: the fieldless enumerate-only stage round-trips through
/// the schema-strict parser, and any slot on it is refused (its slot
/// list is empty, so the generic unknown-slot check covers every key).
+2 -1
View File
@@ -64,7 +64,8 @@ pub fn raw_matches_wrapped(raw: &str, wrapped: &str) -> bool {
/// instead of refusing it.
#[derive(Debug, PartialEq, Eq)]
pub enum AxisIntake {
/// A legal RAW name (`--list-axes`'s own namespace, #328) — accept as-is.
/// A legal RAW name (`aura graph introspect --params`'s own namespace,
/// #328) — accept as-is.
Raw,
/// A retired WRAPPED name; the carried string is its translated RAW
/// candidate — never applied silently, only ever surfaced in a refusal.
+10 -105
View File
@@ -1,8 +1,8 @@
//! Coverage reporting: the single interior-gap-month walk, shared by a
//! campaign cell's coverage annotation
//! (`DefaultMemberRunner::window_coverage`) and `aura data coverage`'s
//! archive-wide report — two independent walk implementations before this
//! module existed (#295 dedup).
//! The interior-gap-month walk behind a campaign cell's coverage annotation
//! (`DefaultMemberRunner::window_coverage`) — originally shared with `aura
//! data coverage`'s archive-wide report (#295 dedup: two independent walk
//! implementations before this module existed) before that verb retired in
//! favor of per-cell fault isolation (#272, #273).
/// `"YYYY-MM"` rendering of a `(year, month)` pair.
pub fn fmt_year_month((y, m): (u16, u8)) -> String {
@@ -18,11 +18,11 @@ pub fn next_year_month((y, m): (u16, u8)) -> (u16, u8) {
/// fall inside `window_ms` (Unix-ms) — one `"YYYY-MM"` entry per missing
/// month, never a collapsed range: the registry's `CellCoverage::gap_months`
/// is a flat list an aggregate counts directly. `months` is assumed sorted
/// ([`aura_ingest::list_m1_months`]'s own contract). The single gap-walk
/// implementation (#295): both `DefaultMemberRunner::window_coverage`
/// (a swept cell's own window) and [`data_coverage_report`] (the full
/// archive span, via [`FULL_ARCHIVE_WINDOW_MS`]) call this one walk instead
/// of maintaining independent copies.
/// ([`aura_ingest::list_m1_months`]'s own contract). `DefaultMemberRunner::
/// window_coverage` (a swept cell's own window) is the sole caller since
/// `aura data coverage`'s archive-wide report retired (#273); the walk stays
/// its own function because it was single-sourced with that verb before then
/// (#295).
pub fn interior_gap_months(months: &[(u16, u8)], window_ms: (i64, i64)) -> Vec<String> {
let from_ym = data_server::records::unix_ms_to_year_month(window_ms.0);
let to_ym = data_server::records::unix_ms_to_year_month(window_ms.1);
@@ -40,68 +40,6 @@ pub fn interior_gap_months(months: &[(u16, u8)], window_ms: (i64, i64)) -> Vec<S
out
}
/// A Unix-ms window that decodes (via `unix_ms_to_year_month`) to year/month
/// bounds — 0001-01 and 9999-01, exact epoch-ms for those UTC instants, not
/// an approximation — comfortably outside any realistic archive month, so an
/// unbounded, archive-wide [`interior_gap_months`] walk never ms-filters out
/// a real gap. Safely inside `chrono`'s valid calendar range, so the
/// conversion inside `interior_gap_months` never panics.
pub const FULL_ARCHIVE_WINDOW_MS: (i64, i64) = (-62_135_596_800_000, 253_370_764_800_000);
/// `aura data coverage <SYMBOL>`'s pure report body (#264): render `symbol`'s
/// coverage report from its sorted `(year, month)` file list — one `span:`
/// line framing the first/last present month, then either a `no gaps` line
/// or one `missing: YYYY-MM..YYYY-MM` line per interior contiguous gap (a
/// ten-month hole is one line, not ten). `Err` exactly when `months` is
/// empty — no archive file exists for `symbol` at all (the caller's "unknown
/// symbol" refusal, stderr + exit 1). The gap detection itself is
/// [`interior_gap_months`] over the full archive span
/// ([`FULL_ARCHIVE_WINDOW_MS`]); collapsing its flat per-month list into
/// contiguous ranges is presentation-only regrouping, done here since it
/// stays byte-identical to the pre-dedup report.
pub fn data_coverage_report(symbol: &str, months: &[(u16, u8)]) -> Result<Vec<String>, String> {
let Some(&first) = months.first() else {
return Err(format!("no archive files found for symbol \"{symbol}\""));
};
let last = *months.last().expect("non-empty checked above");
let mut lines =
vec![format!("{symbol} span: {}..{}", fmt_year_month(first), fmt_year_month(last))];
let gap_months = interior_gap_months(months, FULL_ARCHIVE_WINDOW_MS);
if gap_months.is_empty() {
lines.push(format!("{symbol} no gaps"));
} else {
for (from, to) in collapse_contiguous_year_months(&gap_months) {
lines.push(format!("{symbol} missing: {from}..{to}"));
}
}
Ok(lines)
}
/// Collapse [`interior_gap_months`]' flat, ascending `"YYYY-MM"` list into
/// contiguous inclusive `(from, to)` ranges — a ten-month hole collapses to
/// one pair, not ten. Presentation-only regrouping: the gap WALK itself
/// already ran in `interior_gap_months`.
fn collapse_contiguous_year_months(months: &[String]) -> Vec<(String, String)> {
let mut out: Vec<(String, String)> = Vec::new();
for m in months {
let ym = parse_year_month(m);
match out.last_mut() {
Some((_, to)) if next_year_month(parse_year_month(to)) == ym => {
*to = m.clone();
}
_ => out.push((m.clone(), m.clone())),
}
}
out
}
/// Inverse of [`fmt_year_month`] — parses back the `"YYYY-MM"` shape
/// [`interior_gap_months`] always emits.
fn parse_year_month(s: &str) -> (u16, u8) {
let (y, m) = s.split_once('-').expect("interior_gap_months emits YYYY-MM");
(y.parse().expect("YYYY digits"), m.parse().expect("MM digits"))
}
#[cfg(test)]
mod tests {
use super::*;
@@ -125,37 +63,4 @@ mod tests {
"both interior gap months must be named, in order"
);
}
/// An unknown symbol (no archive files at all — an empty month list)
/// refuses rather than reporting a bogus empty-span coverage (#264): the
/// caller eprintln's the message and exits 1.
#[test]
fn data_coverage_report_refuses_an_unknown_symbol() {
let err = data_coverage_report("GHOST", &[]).unwrap_err();
assert!(err.contains("GHOST"), "names the unknown symbol: {err}");
}
/// A symbol with a fully contiguous file index reports its span plus an
/// explicit `no gaps` line — never a bare span with no gap-status line at
/// all, which would leave "no gaps" indistinguishable from "gaps not yet
/// checked" (#264).
#[test]
fn data_coverage_report_of_a_gapless_symbol_is_span_plus_no_gaps() {
let months = [(2024, 1), (2024, 2), (2024, 3)];
let lines = data_coverage_report("SYMA", &months).expect("known symbol");
assert_eq!(lines, vec!["SYMA span: 2024-01..2024-03", "SYMA no gaps"]);
}
/// The Copper failure shape itself: one interior gap collapses to a single
/// `missing: YYYY-MM..YYYY-MM` line naming the whole contiguous hole, not
/// one line per missing month (#264).
#[test]
fn data_coverage_report_collapses_an_interior_gap_to_one_range_line() {
let months = [(2024, 1), (2024, 2), (2024, 5), (2024, 6)];
let lines = data_coverage_report("GAPSYM", &months).expect("known symbol");
assert_eq!(
lines,
vec!["GAPSYM span: 2024-01..2024-06", "GAPSYM missing: 2024-03..2024-04"]
);
}
}
+30 -720
View File
@@ -1,60 +1,23 @@
//! Family assembly and orchestration (#295).
//!
//! The blueprint sweep / walk-forward / Monte-Carlo family builders — pure
//! member-run recipes driven off a [`DataSource`] (the shared synthetic/real
//! data provider every family builder threads, plus the `--select` objective
//! [`Selection`] walk-forward resolves its winner under) — together with the
//! winner-selection (`select_winner`) and axis-grid validation
//! (`validate_axis_grid`) machinery they share. No `aura` binary is needed to
//! drive a family: the shell (`aura-cli`) wraps these builders with
//! persistence + stdout rendering (`run_blueprint_sweep`,
//! `run_blueprint_walkforward`, `run_blueprint_mc`), which stay in the shell,
//! along with the `--select`/`--real` argv grammar (`parse_select`,
//! `select_rule_of`) and the `topology_hash`/`content_id` naming primitive
//! (inlined here instead, mirroring `member::run_signal_r`'s own copy — the
//! CLI shell still needs its own for call sites outside any family builder).
//! The blueprint sweep / walk-forward / Monte-Carlo family builders that used
//! to live here are retired (#319 — the research-verb quintet's own
//! machinery; the campaign document path, `aura-campaign::exec`, is the
//! surviving family-execution surface, with its own independent axis/window/
//! winner-selection machinery). What remains is the shared [`DataSource`]
//! data provider — the synthetic/real price-source abstraction
//! `aura_runner::reproduce` drives its own bit-identical re-derivation
//! over — and its synthetic stream primitives (`showcase_prices`,
//! `walkforward_prices`, `walkforward_window_source`, `synthetic_walk_sources`).
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use std::sync::Arc;
use aura_campaign::{catch_member_panic, member_fault_prose, MemberFault};
use aura_composites::StopRule;
use aura_core::{Cell, ParamSpec, Scalar, Timestamp};
use aura_engine::{
blueprint_from_json, walk_forward, window_of, BindError, Composite, FamilySelection,
RollMode, RunManifest, SyntheticSpec, VecSource, WindowBounds, WindowRoller,
};
use aura_registry::{
optimize_deflated, optimize_plateau, PlateauMode, DEFLATION_BLOCK_LEN, DEFLATION_N_RESAMPLES,
};
use aura_backtest::{
monte_carlo, McFamily, RunMetrics, RunReport, SweepFamily, SweepPoint, WalkForwardResult,
WindowRun, WF_REAL_IS_NS, WF_REAL_OOS_NS, WF_REAL_STEP_NS,
};
use aura_core::{Scalar, Timestamp};
use aura_engine::{window_of, SyntheticSpec, VecSource};
use aura_backtest::{WF_REAL_IS_NS, WF_REAL_OOS_NS, WF_REAL_STEP_NS};
use crate::binding::ResolvedBinding;
use crate::member::{
blueprint_axis_probe, blueprint_axis_probe_reopened, no_real_data, override_paths,
pip_or_refuse, probe_window, reopen_all, run_blueprint_member, wrapped_bound_overrides_of,
SYNTHETIC_PIP_SIZE,
};
use crate::member::{no_real_data, pip_or_refuse, probe_window, SYNTHETIC_PIP_SIZE};
use crate::project::Env;
use crate::translate::{R_SMA_STOP_K, R_SMA_STOP_LENGTH};
/// The demo default stop regime, shared by every family builder in this module.
const DEFAULT_STOP: StopRule = StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K };
/// The in-sample winner-selection objective for walk-forward's per-window IS
/// refit (cycle 0077). `Argmax` is the bare-best pick deflated for trials
/// (#144, the default); `Plateau` argmaxes the neighbourhood-smoothed surface
/// instead (opt-in via `--select`). The CLI shell's `--select` grammar
/// (`parse_select`) and campaign-rule mapping (`select_rule_of`) build this
/// value; they stay in the shell since only this type crosses the boundary.
#[derive(Clone, Copy)]
pub enum Selection {
Argmax,
Plateau(PlateauMode),
}
/// A warm-up-adequate synthetic stream (~18 ticks rising, falling, then rising
/// again) used as `DataSource::Synthetic`'s full-window stream for the built-in
@@ -103,19 +66,18 @@ pub enum DataChoice {
Real { symbol: String, from_ms: Option<i64>, to_ms: Option<i64> },
}
/// The source provider threaded into the family builders: synthetic built-in
/// streams, or real M1 close bars from the data-server archive. Replaces the
/// hardcoded `VecSource` so a member's source, pip, window, and roller sizes come
/// from one place (Fork B/D/F).
/// The source provider `aura_runner::reproduce` threads through its
/// re-derivation: synthetic built-in streams, or real M1 close bars from the
/// data-server archive. Replaces the hardcoded `VecSource` so a member's
/// source, pip, window, and roller sizes come from one place (Fork B/D/F).
///
/// `Synthetic` denotes a *consumer-dependent* built-in stream, not one fixed
/// series: the full-window consumers (`full_window` / `run_sources`, used by
/// sweep / MC) draw the 18-bar `showcase_prices()`, while the windowed consumers
/// (`windowed_sources` / `wf_window_sizes`, used by walk-forward) draw the 60-bar
/// series: the full-window consumers (`full_window` / `run_sources`) draw the
/// 18-bar `showcase_prices()`, while the windowed consumers
/// (`windowed_sources` / `wf_window_sizes`) draw the 60-bar
/// `walkforward_prices()` so the `(24,12,12)`-bar roller fits its span. The two
/// faces never reach one consumer (a family is either full-window or windowed), so
/// the split is invisible per call site but real across the type — read both
/// family builders to see it whole.
/// faces never reach one consumer (full-window vs. windowed reproduction), so
/// the split is invisible per call site but real across the type.
pub enum DataSource {
Synthetic,
Real {
@@ -223,666 +185,14 @@ impl DataSource {
}
}
/// #260: the r-sma sugar/MC paths below run with either no cost model (empty
/// `cost` slice) or CLI-flag cost specs (scalar-only — `cost_specs_from_params`
/// only ever wraps `CostValue::Scalar`), so an instrument-keyed map can never
/// originate on these paths and the instrument context is genuinely inert.
/// Named once so every such call site states its intent by reference instead
/// of repeating the justifying comment.
const NO_INSTRUMENT_CONTEXT: &str = "";
/// The winner-selection objective for walk-forward's per-window IS refit — the
/// deflation-aware SQN variant (#144 default). Named once so `select_winner`
/// and `blueprint_walkforward_family` (the only two family-builder-side call
/// sites) cannot drift apart on the token; the CLI shell's campaign-sugar
/// bridge keeps its own copy of this token (main.rs `WINNER_SELECTION_METRIC`)
/// since it is not itself a family builder.
const WINNER_SELECTION_METRIC: &str = "sqn_normalized";
/// Fixed RNG seed for the trials-deflation reality-check bootstrap in walk-forward
/// winner selection. Recorded on each winner's manifest (so `overfit_probability`
/// is reproducible by re-run); a CLI flag for it is a deferred refinement. The
/// resample count and block length are the shared `aura_registry::DEFLATION_*`.
const DEFLATION_SEED: u64 = 0xDEF1_A7ED;
/// Renders a [`BindError`] as one-line prose in `member::override_paths`' sibling
/// register above (#247) — never the raw Rust `Debug` struct name
/// (`KindMismatch { .. }` / `MissingKnob("..")`), the two variants the sweep
/// terminal actually raises past `override_paths`' own pre-flight (which
/// already rejects an unresolvable axis name as prose before either call
/// site below ever reaches the terminal). `UnknownKnob` already carries a
/// fully-prosed message string (wrapped from `override_paths`' own
/// `Result<_, String>`) — unwrapped here rather than Debug-framed (#269), so
/// the walkforward path's rejection reaches stderr as bare prose too.
///
/// #328 render-seam fix: `BindError::MissingKnob`/`KindMismatch` carry the
/// WRAPPED knob name (the terminal's own bind-time frame — untouched by this
/// cycle, per the minuted deviation on acceptance-criterion 2). No
/// user-facing surface may print a wrapped axis name, so this renderer strips
/// the one leading node segment (`wrapped_to_raw_axis`, the same convention
/// `axes.rs` defines) before the name ever reaches prose — the fix lives at
/// the RENDER seam only, not in the error type or the resolution machinery.
pub fn render_bind_error(e: &BindError) -> String {
match e {
BindError::MissingKnob(name) => {
let name = crate::axes::wrapped_to_raw_axis(name);
format!(
"axis {name}: an open param with no axis and no bound default — \
bind it with `--axis {name}=<value>` see `aura sweep <bp> --list-axes`"
)
}
BindError::KindMismatch { knob, expected, got } => {
let knob = crate::axes::wrapped_to_raw_axis(knob);
format!(
"axis {knob}: expected {expected:?}, supplied {got:?} — \
see `aura sweep <bp> --list-axes`"
)
}
BindError::UnknownKnob(msg) => msg.clone(),
BindError::DuplicateBinding(name) => format!(
"axis {name}: bound twice — each param takes exactly one axis — \
see `aura sweep <bp> --list-axes`"
),
BindError::EmptyAxis(name) => format!(
"axis {name}: supplies no values — give at least one, e.g. `--axis {name}=2,4`"
),
BindError::EmptyRange(name) => format!(
"axis {name}: the named range is empty — give it at least one value"
),
// A blueprint defect, not an axis usage error: the point passed name
// resolution but its bootstrap failed. Prose frame with the compile
// detail explicitly labelled as internal — per-variant prose for
// CompileError belongs to the graph-build surface, not this boundary.
BindError::Compile(e) => format!(
"the resolved axis point failed to bootstrap — the blueprint is \
defective at this point, re-validate it with `aura graph build` \
(internal detail: {e:?})"
),
}
}
/// Resolve the in-sample winner under the chosen selection objective. `Argmax`
/// defers to the trials-deflation pick (#144). `Plateau` argmaxes the smoothed grid
/// surface — it needs the grid lattice, so a sweep with no lattice (a future random
/// walk-forward producer) is refused rather than silently argmaxed. The metric is
/// always known at the call sites, so a metric error is unreachable (`expect`); the
/// only fallible outcome is the plateau-without-lattice refusal, returned as
/// `Err(message)` for the caller to print and exit 2.
pub fn select_winner(
family: &SweepFamily, metric: &str, select: Selection, lattice: Option<&[usize]>,
) -> Result<(SweepPoint, FamilySelection), String> {
match select {
Selection::Argmax => Ok(optimize_deflated(
family, metric, DEFLATION_N_RESAMPLES, DEFLATION_BLOCK_LEN, DEFLATION_SEED,
).expect("walk-forward metrics are known")),
Selection::Plateau(mode) => match lattice {
Some(lens) => Ok(optimize_plateau(family, lens, metric, mode)
.expect("walk-forward metrics are known")),
None => Err(
"--select plateau requires a grid sweep; a random sweep has no parameter lattice"
.to_string(),
),
},
}
}
/// A constant, zero-compute `RunReport` for [`validate_axis_grid`]'s sweep-terminal
/// probe. `SweepBinder::sweep_with_lattice`'s own `resolve_axes`/arity/kind checks all
/// run BEFORE this closure is invoked per grid point, so its body never influences the
/// validation outcome — only its signature (`Fn(&[Cell]) -> RunReport`) needs to
/// satisfy the terminal, at O(1) cost per point instead of a full member run. Reused
/// (#278) as the discarded slot value for a member run [`catch_member_panic`]
/// contains: the caller always resolves the captured fault and exits before this
/// value is ever rendered or persisted, so its content is equally irrelevant there.
fn axis_grid_probe_report() -> RunReport {
RunReport {
manifest: RunManifest {
commit: String::new(),
params: Vec::new(),
defaults: Vec::new(),
window: (Timestamp(0), Timestamp(0)),
seed: 0,
broker: "wf-axis-preflight-placeholder".to_string(),
selection: None,
instrument: None,
topology_hash: None,
project: None,
},
metrics: RunMetrics { total_pips: 0.0, max_drawdown: 0.0, bias_sign_flips: 0, r: None },
}
}
/// The LOWEST enumeration index's captured member-panic message, found by
/// walking `family.points` — guaranteed by `assemble_sweep`'s own
/// `collect()` to be in grid-enumeration order regardless of which worker
/// finished first (C1) — for the first point whose params match a captured
/// fault. Thread-order-independent by construction (mirrors the lowest-
/// index-fault convention `aura-campaign::exec::run_members` establishes for
/// the identical #272 seam), without needing a separate numeric-index
/// side-channel: the returned family already carries the points in order.
fn lowest_point_fault(family: &SweepFamily, faults: Mutex<Vec<(Vec<Cell>, String)>>) -> Option<String> {
let captured = faults.into_inner().expect("fault capture lock");
family
.points
.iter()
.find_map(|pt| captured.iter().find(|(p, _)| *p == pt.params).map(|(_, m)| m.clone()))
}
/// The LOWEST seed's captured member-panic message (#278), found by walking
/// `family.draws` — `monte_carlo`'s own contract guarantees seed-**input**
/// order regardless of thread completion (C1) — for the first draw whose
/// seed matches a captured fault. Thread-order-independent by construction,
/// mirroring [`lowest_point_fault`]'s and `lowest_window_fault`'s identical
/// convention one level over (per-point sweep / per-window walk-forward).
fn lowest_seed_fault(family: &McFamily, faults: Mutex<Vec<(u64, String)>>) -> Option<String> {
let captured = faults.into_inner().expect("fault capture lock");
family
.draws
.iter()
.find_map(|d| captured.iter().find(|(s, _)| *s == d.seed).map(|(_, m)| m.clone()))
}
/// Render + exit(3) on a contained member panic (#278: the deliberate
/// failed-cells exit, C14, never the raw 101 an uncontained panic would
/// yield) — reuses `aura_campaign::member_fault_prose`'s established
/// "a member panicked: …" wording rather than re-wording it, and the
/// `aura: warning: ` class marker (#278 decision) the CLI's own `diag`
/// macro would emit, hand-written here since that macro is presentation-
/// crate-private (#295 boundary) and this fires from `aura-runner`.
fn exit_on_member_panic(msg: &str) -> ! {
eprintln!("aura: warning: {}", member_fault_prose(&MemberFault::Panic(msg.to_string())));
std::process::exit(3);
}
/// Validate the `--axis` grid against `doc`'s wrapped param space WITHOUT running any
/// member (#253). Reuses the SAME strict, erroring axis-name check
/// `blueprint_sweep_over` performs (`override_paths` — single-sourced, so the two
/// paths cannot drift to differently-worded rejections) to derive the #246 override
/// set, then drives the sweep terminal's own `resolve_axes`/arity/kind checks
/// (`SweepBinder::sweep_with_lattice`) with [`axis_grid_probe_report`] standing in for
/// the run closure — no data access, no sim engine tick. Axis resolution is
/// window-agnostic, so the caller derives or passes no window at all.
fn validate_axis_grid(
doc: &str, axes: &[(String, Vec<Scalar>)], raw_space: &[ParamSpec], probe_signal: &Composite,
env: &Env,
) -> Result<(), BindError> {
let overrides = override_paths(axes, raw_space, probe_signal).map_err(BindError::UnknownKnob)?;
let probe = blueprint_axis_probe_reopened(doc, env, &overrides);
let space = probe.param_space();
// #328: `axes` names are RAW (refused at the walkforward dispatch-boundary
// preflight ahead of this call, mirroring the plain sweep verb) — translate
// each onto the ONE wrapped `space` slot it suffix-matches
// (`wrapped_name_of`, the same convention `blueprint_sweep_family` feeds its
// own `SweepBinder`) before this terminal's exact-name resolution.
let mut iter = axes.iter();
let (first_name, first_vals) = iter.next().expect("a blueprint walk-forward declares >= 1 axis");
let mut binder = probe.axis(&crate::axes::wrapped_name_of(first_name, &space), first_vals.clone());
for (n, vals) in iter {
binder = binder.axis(&crate::axes::wrapped_name_of(n, &space), vals.clone());
}
binder.sweep_with_lattice(|_| axis_grid_probe_report()).map(|_| ())
}
/// Sweep a serialized signal `doc` over user-named param-space axes. Structurally it
/// keeps the shape of the retired `r_sma_sweep_family` demo builder (#159), with three
/// deviations. (1) The signal source is
/// `wrap_r(blueprint_from_json(doc))` — a loaded blueprint, not the Rust-built
/// r-sma graph. (2) The signal is RE-loaded from `doc` per member (a `Composite` is
/// `!Clone`, so the throwaway param-space probe and each grid point each reload). (3)
/// The axes are taken verbatim BY NAME (not the four suffix-resolved r-sma knobs):
/// each `(name, vals)` is fed straight to the `SweepBinder`, so an unknown name or a
/// kind mismatch surfaces as the sweep terminal's [`BindError`], rendered to a message
/// string — a named error, never a panic. An axis naming a bound param re-opens it
/// (#246: bound value = default); an axis matching neither space is refused by
/// `override_paths` before any run. Every member manifest carries the shared
/// `topology_hash` of the loaded signal; reduce-mode fold, identical to the retired
/// mirror's default (no-trace) arm.
pub fn blueprint_sweep_family(
doc: &str,
axes: &[(String, Vec<Scalar>)],
data: &DataSource,
env: &Env,
) -> Result<SweepFamily, String> {
// Identity + binding read the AUTHORED doc, raw (no override re-open):
// topology and the resolved role plan are properties of the document, not
// of any one sweep's axis choices.
let probe_signal = blueprint_from_json(doc, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary; reload is infallible");
// topology_hash's own two-line body, inlined (mirrors member::run_signal_r):
// `content_id_of` over the canonical (#164) blueprint JSON — the CLI shell's
// `topology_hash` helper is the same primitive, kept single-sourced at
// `aura_research`.
let topo = aura_research::content_id_of(
&aura_engine::blueprint_to_json(&probe_signal).expect("a buildable signal serializes"),
);
// Strict binding resolution (name defaults — the verb path carries no
// campaign overrides): the family's open plan and wrap plan in one value.
let binding = crate::binding::resolve_binding(probe_signal.name(), probe_signal.input_roles(), &BTreeMap::new())?;
if matches!(data, DataSource::Synthetic) && !binding.close_only() {
return Err(crate::binding::synthetic_refusal(probe_signal.name(), &binding));
}
let pip = data.pip_size();
let window = data.full_window(env);
// The un-reopened wrapped OPEN space (#246): derives the override set (which
// named axes re-open a bound param) before the real, reopened probe is built —
// probe and per-member reloads must re-open identically so points resolve
// against one space. A name matching neither space is the error here.
let raw_space = blueprint_axis_probe(doc, env).param_space();
let overrides = override_paths(axes, &raw_space, &probe_signal)?;
let probe = blueprint_axis_probe_reopened(doc, env, &overrides);
let space = probe.param_space();
// The doc is parse-validated at the dispatch boundary (with file-path context),
// so every reload here is infallible: the builder has a single error contract —
// the `BindError` returned by the sweep terminal — and no hidden process exit.
// Member reloads re-open the SAME override set derived above, so every member
// resolves its axes against the identical (reopened) param space the probe used.
let reload = |d: &str| {
reopen_all(
blueprint_from_json(d, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary; reload is infallible"),
&overrides,
)
};
// seed the named axes: `axes` names are RAW (#328 — refused at intake before
// this fn ever runs), so each is translated to the ONE wrapped `space` slot
// it suffix-matches (`raw_matches_wrapped`, the same convention
// `override_paths` just resolved the override set through) before feeding
// `Composite::axis`/`SweepBinder::axis`, which still key by the exact
// wrapped `param_space()` name. `resolve_axes` name- and kind-checks the
// translated axes at the sweep terminal, so an UnknownKnob / KindMismatch
// is returned, not panicked (translation cannot itself introduce one: every
// incoming name was already validated raw-or-bound at the intake / override
// preflight above).
let wrapped_name_of = |raw: &str| -> String {
space
.iter()
.find(|p| crate::axes::raw_matches_wrapped(raw, &p.name))
.map(|p| p.name.clone())
.unwrap_or_else(|| raw.to_string())
};
let mut iter = axes.iter();
let (first_name, first_vals) = iter.next().expect("a blueprint sweep declares >= 1 axis");
let mut binder = probe.axis(&wrapped_name_of(first_name), first_vals.clone());
for (n, vals) in iter {
binder = binder.axis(&wrapped_name_of(n), vals.clone());
}
// manifest.params records RAW names (#328): a name-only reshaping of `space`
// (order/kind untouched) fed to `run_blueprint_member`, which only zips it
// against `point` BY POSITION (`zip_params`) — never re-resolves by name —
// so renaming here is purely the manifest's own namespace, with no effect
// on member resolution.
let manifest_space: Vec<ParamSpec> = space
.iter()
.map(|p| ParamSpec { name: crate::axes::wrapped_to_raw_axis(&p.name).to_string(), kind: p.kind })
.collect();
// #278: `run_blueprint_member` runs bare here (no #272 fault boundary of its
// own), so a member-compile panic (e.g. an `Sma::new` length assert) would
// otherwise unwind straight through this sweep to an uncaught exit 101 —
// contained the same way the real/campaign path contains it, via
// `catch_member_panic` + a per-point capture, resolved to the lowest
// enumeration index's message after the sweep joins (thread-order-
// independent, C1).
let faults: Mutex<Vec<(Vec<Cell>, String)>> = Mutex::new(Vec::new());
let family = binder
.sweep(|point| {
// fresh per-member graph (Composite is !Clone, reload per member) run through
// the shared reduce-mode member path — the same fn reproduction re-runs.
match catch_member_panic(|| {
run_blueprint_member(reload(doc), point, &manifest_space, data.run_sources(env, &binding.columns()), window, 0, pip, &topo, env, DEFAULT_STOP, &binding, &[], NO_INSTRUMENT_CONTEXT)
}) {
Ok(report) => report,
Err(msg) => {
faults.lock().expect("fault capture lock").push((point.to_vec(), msg));
axis_grid_probe_report()
}
}
})
// render the sweep terminal's BindError to prose (#247), the fn's String error
// contract — never the raw Debug struct.
.map_err(|e| render_bind_error(&e))?;
if let Some(msg) = lowest_point_fault(&family, faults) {
exit_on_member_panic(&msg);
}
Ok(family)
}
/// Sweep the LOADED blueprint over the user `--axis` grid on an in-sample window
/// `[from,to]` — the windowed, lattice-carrying twin of
/// `blueprint_sweep_family`. `sweep_with_lattice` gives the grid lattice `--select
/// plateau` needs. An unknown/kind-mismatched axis surfaces as `BindError` at the
/// sweep terminal (no panic, no hidden exit) for the caller to render. An axis
/// naming a bound param re-opens it (#246: bound value = default, same
/// `override_paths`/`reopen_all` recipe as `blueprint_sweep_family` — this is
/// its walk-forward in-sample twin); an axis matching neither space is refused
/// (wrapped as `BindError::UnknownKnob`, the honest replacement for the retired
/// "fully bound; nothing to sweep" refusal) before any member runs.
///
/// The returned `Option<String>` (#278) is this fn's OWN resolved member-panic
/// capture: this fn's nested sweep runs inside `blueprint_walkforward_family`'s
/// per-window parallel closure, so a contained member panic here must NOT
/// `eprintln!`+`exit` on the spot (that would race across windows, the exact
/// `#177` duplicated-rejection class the dispatch-boundary axis pre-flight
/// above already avoids) — captured internally, resolved to the lowest-index
/// message via [`lowest_point_fault`] before returning, for the caller to push
/// onto its own per-window fault list only after every window has joined.
pub fn blueprint_sweep_over(
doc: &str, axes: &[(String, Vec<Scalar>)], from: Timestamp, to: Timestamp, data: &DataSource,
env: &Env, binding: &ResolvedBinding,
) -> Result<(SweepFamily, Vec<usize>, Option<String>), BindError> {
let reload = |d: &str| {
blueprint_from_json(d, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary; reload is infallible")
};
let pip = data.pip_size();
let probe_signal = reload(doc);
// topology_hash's own two-line body, inlined (see `blueprint_sweep_family`).
let topo = aura_research::content_id_of(
&aura_engine::blueprint_to_json(&probe_signal).expect("a buildable signal serializes"),
);
// The un-reopened wrapped OPEN space (#246), against a raw probe + raw strategy
// load: derives the override set (which named axes re-open a bound param) before
// the reopened probe is built — probe and per-member reloads must re-open
// identically so points resolve against one space, exactly like
// `blueprint_sweep_family`.
let raw_space = blueprint_axis_probe(doc, env).param_space();
let overrides = override_paths(axes, &raw_space, &probe_signal).map_err(BindError::UnknownKnob)?;
let probe = blueprint_axis_probe_reopened(doc, env, &overrides);
let space = probe.param_space();
// #328: `axes` names are RAW (refused at the walkforward dispatch-boundary
// preflight ahead of the caller, mirroring the plain sweep verb) —
// translate each onto the ONE wrapped `space` slot it suffix-matches
// (`wrapped_name_of`, the same convention `blueprint_sweep_family` feeds
// its own `SweepBinder`) before this terminal's exact-name resolution.
let mut iter = axes.iter();
let (first_name, first_vals) = iter.next().expect("a blueprint walk-forward declares >= 1 axis");
let mut binder = probe.axis(&crate::axes::wrapped_name_of(first_name, &space), first_vals.clone());
for (n, vals) in iter {
binder = binder.axis(&crate::axes::wrapped_name_of(n, &space), vals.clone());
}
let faults: Mutex<Vec<(Vec<Cell>, String)>> = Mutex::new(Vec::new());
let (family, lattice) = binder.sweep_with_lattice(|point| {
let sources = data.windowed_sources(from, to, env, &binding.columns());
let window = window_of(&sources).expect("non-empty in-sample window");
match catch_member_panic(|| {
run_blueprint_member(reopen_all(reload(doc), &overrides), point, &space, sources, window, 0, pip, &topo, env, DEFAULT_STOP, binding, &[], NO_INSTRUMENT_CONTEXT)
}) {
Ok(report) => report,
Err(msg) => {
faults.lock().expect("fault capture lock").push((point.to_vec(), msg));
axis_grid_probe_report()
}
}
})?;
let fault = lowest_point_fault(&family, faults);
Ok((family, lattice, fault))
}
/// Run the winner params over an out-of-sample window `[from,to]` on the loaded
/// blueprint. The reduce-mode member
/// (`run_blueprint_member`) retains R-metrics, not a raw pip curve, so the stitching
/// segment is empty (an empty segment leaves the stitched curve unbroken). `overrides`
/// (#246) is the SAME family-wide set `blueprint_walkforward_family` derived once and
/// resolved `space`/`params` against — the OOS reload must re-open it too, or a
/// bound-param axis's winner point (kind-checked against the REOPENED space) fails
/// `bootstrap_with_cells`'s arity check against this still-closed reload.
#[allow(clippy::too_many_arguments)]
pub fn run_oos_blueprint(
doc: &str, params: &[Cell], space: &[ParamSpec], from: Timestamp, to: Timestamp,
topo: &str, data: &DataSource, env: &Env, binding: &ResolvedBinding,
overrides: &[String],
) -> (Vec<(Timestamp, f64)>, RunReport) {
let reload = reopen_all(
blueprint_from_json(doc, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary; reload is infallible"),
overrides,
);
let pip = data.pip_size();
let sources = data.windowed_sources(from, to, env, &binding.columns());
let window = window_of(&sources).expect("non-empty out-of-sample window");
let report = run_blueprint_member(reload, params, space, sources, window, 0, pip, topo, env, DEFAULT_STOP, binding, &[], NO_INSTRUMENT_CONTEXT);
(Vec::new(), report)
}
/// A discarded placeholder [`WindowRun`] for a window whose IS refit or OOS
/// run was contained after a member panic (#278): `chosen_params` matches
/// `space`'s arity with inert zero cells (the `assemble_walk_forward` arity
/// invariant), `oos_equity` is empty (an empty segment leaves the stitched
/// curve unbroken, same convention as a genuinely equity-less window), and
/// the report reuses [`axis_grid_probe_report`]'s zero-cost placeholder.
/// Never rendered: the caller always resolves the captured window fault and
/// exits before touching a faulted window's run.
fn placeholder_window_run(space: &[ParamSpec]) -> WindowRun {
WindowRun {
chosen_params: space.iter().map(|_| Cell::from_i64(0)).collect(),
oos_equity: Vec::new(),
oos_report: axis_grid_probe_report(),
}
}
/// The LOWEST roll-order window's captured member-panic message (#278),
/// found by walking `result.windows` — guaranteed by `assemble_walk_forward`
/// to be in roll order regardless of thread completion (C1) — for the first
/// window whose bounds match a captured fault. Thread-order-independent by
/// construction, mirroring [`lowest_point_fault`]'s identical convention one
/// level down (per-point within one window's IS sweep) and
/// `aura-campaign::exec`'s own lowest-index walk-forward fault attribution.
fn lowest_window_fault(result: &WalkForwardResult, faults: Mutex<Vec<(WindowBounds, String)>>) -> Option<String> {
let captured = faults.into_inner().expect("fault capture lock");
result
.windows
.iter()
.find_map(|w| captured.iter().find(|(b, _)| *b == w.bounds).map(|(_, m)| m.clone()))
}
/// The loaded-blueprint IS-refit walk-forward: per IS window, re-optimize the
/// blueprint over the user `--axis` grid, select by `sqn_normalized`, run the
/// winner OOS, reusing the generic `walk_forward` driver + `select_winner`;
/// only the per-window sweep/OOS source the loaded blueprint. In-closure errors
/// (a bad `--axis`) `exit(2)` with the sweep terminal's message.
pub fn blueprint_walkforward_family(
doc: &str, axes: &[(String, Vec<Scalar>)], data: &DataSource, select: Selection,
env: &Env,
) -> WalkForwardResult {
let span = data.wf_full_span(env);
let (is_len, oos_len, step) = data.wf_window_sizes();
let roller = match WindowRoller::new(span, is_len, oos_len, step, RollMode::Rolling) {
Ok(r) => r,
Err(e) => {
eprintln!("aura: walk-forward window too short for one IS+OOS span: {e:?}");
std::process::exit(2);
}
};
let probe_signal = blueprint_from_json(doc, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary; reload is infallible");
// topology_hash's own two-line body, inlined (see `blueprint_sweep_family`).
let topo = aura_research::content_id_of(
&aura_engine::blueprint_to_json(&probe_signal).expect("a buildable signal serializes"),
);
// The un-reopened wrapped OPEN space (#246), against a raw probe + raw strategy
// load: derives the override set ONCE for the whole family — every per-window
// sweep AND the OOS reload re-open the SAME set, mirroring
// `blueprint_sweep_family`/`blueprint_sweep_over`. The SILENT variant
// (`wrapped_bound_overrides_of`, not the validating `override_paths`): an axis
// matching neither space is simply not an override here — the strict check + its
// established error message stay single-sourced in `blueprint_sweep_over`'s own
// pre-flight call below, so this derivation cannot double-validate with a
// differently-worded rejection.
let axis_names: Vec<String> = axes.iter().map(|(n, _)| n.clone()).collect();
let raw_space = blueprint_axis_probe(doc, env).param_space();
let overrides = wrapped_bound_overrides_of(&axis_names, &raw_space, &probe_signal);
let space = blueprint_axis_probe_reopened(doc, env, &overrides).param_space();
// Strict binding resolution, once per family; refusal is the established
// `aura: ` + exit-1 register (the roller's usage refusals stay exit 2).
let binding = crate::binding::resolve_binding(probe_signal.name(), probe_signal.input_roles(), &BTreeMap::new())
.unwrap_or_else(|m| {
eprintln!("aura: {m}");
std::process::exit(1);
});
if matches!(data, DataSource::Synthetic) && !binding.close_only() {
eprintln!("aura: {}", crate::binding::synthetic_refusal(probe_signal.name(), &binding));
std::process::exit(1);
}
// Validate the `--axis` grid ONCE at the dispatch boundary, mirroring `aura sweep`
// (which resolves its axes a single time before any member runs). `walk_forward` fans
// the per-window closure out across the windows in parallel, so a `BindError` raised
// *inside* the closure would `eprintln!`+`exit(2)` from several windows before any one
// exit lands — a racy, duplicated rejection (#177). Axis resolution is window-agnostic
// (#253): `validate_axis_grid` resolves the SAME grid the sweep terminal would, without
// running a single member — no IS window (or a second roller) is needed here at all.
if let Err(e) = validate_axis_grid(doc, axes, &raw_space, &probe_signal, env) {
eprintln!("aura: {}", render_bind_error(&e));
std::process::exit(2);
}
// #278: `blueprint_sweep_over`'s inner sweep and `run_oos_blueprint`'s single
// member run both drive `run_blueprint_member` bare, so either could
// otherwise unwind an uncaught member-compile panic straight through this
// window closure. Both are contained (`blueprint_sweep_over` resolves its
// own capture and returns it; `run_oos_blueprint` via `catch_member_panic`
// directly) and captured PER WINDOW here rather than printed on the spot —
// this closure runs across windows in parallel (`walk_forward`), so an
// inline `eprintln!`+`exit` would race the same way the axis pre-flight
// above was written to avoid (#177). The lowest roll-order window's fault
// is resolved once, after every window has joined.
let window_faults: Mutex<Vec<(WindowBounds, String)>> = Mutex::new(Vec::new());
let result = walk_forward(roller, space.clone(), |w: WindowBounds| {
let (is_family, lattice, is_fault) = blueprint_sweep_over(doc, axes, w.is.0, w.is.1, data, env, &binding)
.expect("axes validated in the dispatch-boundary pre-flight");
if let Some(msg) = is_fault {
window_faults.lock().expect("fault capture lock").push((w, msg));
return placeholder_window_run(&space);
}
let (best, selection) = match select_winner(&is_family, WINNER_SELECTION_METRIC, select, Some(&lattice)) {
Ok(v) => v,
Err(msg) => { eprintln!("aura: {msg}"); std::process::exit(2); }
};
match catch_member_panic(|| {
run_oos_blueprint(doc, &best.params, &space, w.oos.0, w.oos.1, &topo, data, env, &binding, &overrides)
}) {
Ok((oos_equity, mut oos_report)) => {
oos_report.manifest.selection = Some(selection);
WindowRun { chosen_params: best.params, oos_equity, oos_report }
}
Err(msg) => {
window_faults.lock().expect("fault capture lock").push((w, msg));
placeholder_window_run(&space)
}
}
});
if let Some(msg) = lowest_window_fault(&result, window_faults) {
exit_on_member_panic(&msg);
}
result
}
/// A fresh seeded synthetic price walk for one Monte-Carlo draw — `blueprint_mc_family`'s
/// pattern (a distinct realization per seed). A FIXED `SyntheticSpec` shared by the
/// `aura mc <blueprint.json>` persist path AND the reproduce MonteCarlo branch, so the
/// seed->walk reconstruction is bit-exact (C1). Length 60 comfortably warms the loaded
/// r-sma graph (SMA slow=4 + the len-3 vol stop) so draws produce differing trades.
/// A fresh seeded synthetic price walk for one Monte-Carlo draw (a distinct
/// realization per seed). A FIXED `SyntheticSpec` shared by the reproduce
/// MonteCarlo branch (`aura_runner::reproduce`, its only remaining
/// production caller — #319 retired the family builder that used to mint
/// such a family), so the seed->walk reconstruction is bit-exact (C1).
/// Length 60 comfortably warms the loaded r-sma graph (SMA slow=4 + the
/// len-3 vol stop) so draws produce differing trades.
pub fn synthetic_walk_sources(seed: u64) -> Vec<Box<dyn aura_engine::Source>> {
let spec = SyntheticSpec { start: 1.0, len: 60, step: 1 };
vec![Box::new(spec.source(seed))]
}
/// Build a Monte-Carlo family from a loaded CLOSED signal blueprint: run the fixed
/// blueprint across `n_seeds` seeds, each seed drawing a distinct synthetic walk. The
/// blueprint must be CLOSED (empty wrapped `param_space`) — MC binds no axis, so a free
/// knob has no binder; an OPEN blueprint yields a named `Err` (exit-free like the sibling
/// [`blueprint_sweep_family`]: the IO wrapper `run_blueprint_mc` renders it to stderr +
/// exit 2) before any run, pre-empting the `compile_with_params` arity panic. Each draw
/// runs the shared reduce-mode member path (`run_blueprint_member`, the same fn reproduce
/// re-runs), so reproduction is bit-identical (C1); every member carries the shared
/// `topology_hash`.
pub fn blueprint_mc_family(
doc: &str, n_seeds: u64, data: &DataSource, env: &Env,
) -> Result<McFamily, String> {
let reload = |d: &str| {
blueprint_from_json(d, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary; reload is infallible")
};
let probe_signal = reload(doc);
// topology_hash's own two-line body, inlined (see `blueprint_sweep_family`).
let topo = aura_research::content_id_of(
&aura_engine::blueprint_to_json(&probe_signal).expect("a buildable signal serializes"),
);
// Strict binding resolution (name defaults — mc's synthetic family binds
// no campaign overrides); the exit-free Err contract of this builder.
let binding = crate::binding::resolve_binding(probe_signal.name(), probe_signal.input_roles(), &BTreeMap::new())?;
if !binding.close_only() {
// MC draws ALWAYS run the seeded synthetic close walk (real-data mc
// routes through the campaign sugar and never reaches this builder).
return Err(crate::binding::synthetic_refusal(probe_signal.name(), &binding));
}
let pip = data.pip_size();
// probe the wrapped param_space (the same probe the sweep resolves against);
// MC needs it empty. `blueprint_axis_probe` is the single source of that wrap.
let space = blueprint_axis_probe(doc, env).param_space();
if !space.is_empty() {
// Exit-free like blueprint_sweep_family: the builder's single error contract is this
// returned message (no hidden process exit), so the rejection is unit-testable; the IO
// wrapper run_blueprint_mc renders it to stderr + exit 2 at the boundary.
return Err(format!(
"mc requires a closed blueprint (no free parameters); {} free knob(s) — \
bind them or use `aura sweep --axis`",
space.len()
));
}
// Closed blueprint -> an empty base point (as `aura run <blueprint.json>`); the MC
// draws vary the SEED, not a tuning param (C12 axis 4). Delegate the disjoint C1 draws
// to the shared `monte_carlo` helper — it runs them in parallel across sims (invariant 1),
// deterministic in seed-input order. Each draw
// re-runs the shared reduce-mode member path over its own seeded synthetic walk.
let seeds: Vec<u64> = (1..=n_seeds).collect();
let base_point: Vec<Scalar> = Vec::new();
// #278: `run_blueprint_member` ran bare here, the one family builder without
// the #272 fault boundary its sweep/walk-forward siblings gained in 51096a3
// — a member-compile panic (e.g. an `Sma::new` length assert) would otherwise
// unwind straight through `monte_carlo`'s `run_indexed` to an uncaught exit
// 101. Contained the same way: `catch_member_panic` + a per-seed capture,
// resolved to the LOWEST seed's message after the join (thread-order-
// independent, C1) via `lowest_seed_fault`.
let faults: Mutex<Vec<(u64, String)>> = Mutex::new(Vec::new());
let family = monte_carlo(&base_point, &seeds, |seed, _base| {
let sources = synthetic_walk_sources(seed);
let window = window_of(&sources).expect("non-empty synthetic walk");
match catch_member_panic(|| {
run_blueprint_member(reload(doc), &[], &space, sources, window, seed, pip, &topo, env, DEFAULT_STOP, &binding, &[], NO_INSTRUMENT_CONTEXT)
}) {
Ok(report) => report,
Err(msg) => {
faults.lock().expect("fault capture lock").push((seed, msg));
axis_grid_probe_report()
}
}
});
// The captured fault must win BEFORE the vacuous-mc guard below: a faulted
// draw's placeholder report is metrics-identical across every faulted seed,
// so a run with >= 2 faulted seeds (or one faulted + one real draw sharing
// its placeholder's zero metrics) could otherwise trip the vacuous refusal
// instead of surfacing the real member fault.
if let Some(msg) = lowest_seed_fault(&family, faults) {
exit_on_member_panic(&msg);
}
// Silent-vacuous MC guard (refuse-don't-guess, C10): with >= 2 seeds, if every draw's
// metrics are bit-identical to the first, no seed reached a distinguishable realization —
// the strategy never warmed over the fixed synthetic walk (e.g. a lookback as deep as the
// walk is long), so the "distribution" is a single point masquerading as a family: a wrong
// result with no error. Compare `metrics`, not the whole `RunReport` — the manifest's
// `seed` differs per draw by construction, so a whole-report compare could never detect the
// collapse; the metrics are the realization the seed is meant to move. A single-draw MC
// (n == 1) is trivially "all identical" and is NOT this cross-seed condition, so it passes.
if family.draws.len() >= 2
&& family
.draws
.iter()
.all(|d| d.report.metrics == family.draws[0].report.metrics)
{
return Err(
"mc is vacuous: every seed produced an identical result — the strategy never warmed \
over the synthetic walk, so no seed reached a distinguishable realization; use a \
shallower-lookback blueprint or a longer walk"
.to_string(),
);
}
Ok(family)
}
+34 -2
View File
@@ -10,7 +10,7 @@
use std::collections::BTreeMap;
use aura_core::{Scalar, Timestamp};
use aura_engine::{Composite, Harness, MeasurementReport, RunManifest};
use aura_engine::{CompileError, Composite, Harness, MeasurementReport, RunManifest};
use crate::member::{key_supply, raw_bound_defaults, resolve_run_data, RunData};
use crate::project::Env;
@@ -51,6 +51,31 @@ pub fn measurement_manifest(
}
}
/// #339 item 3 (a #317 follow-up): `CompileError::UnboundRootRole { role }`
/// carries a flat root-role index — meaningless to a caller who authored a
/// NAMED open role (`{"op":"input","role":"price"}`). `role_names` is
/// `signal.input_roles()`'s own names, read before `compile_with_params`
/// consumes `signal` (mirrors `member::compile_error_prose`'s pre-consumption
/// `names` capture for its `ParamKindMismatch` prose). Every other
/// `CompileError` variant keeps the existing Debug fallback deliberately —
/// they ARE reachable on this direct-compile path (a hand-authored
/// measurement envelope with an out-of-range declared-tap wire reaches
/// `TapWireOutOfRange` here exactly as `run_signal_r`'s own compile call
/// does, see `run_refuses_unrunnable_blueprint.rs`), so the fallback stays
/// total rather than partial; only `UnboundRootRole` gets dedicated prose
/// above.
fn compile_error_prose(e: &CompileError, role_names: &[String]) -> String {
let CompileError::UnboundRootRole { role } = e else {
return format!("this blueprint does not compile to a runnable harness: {e:?}");
};
let name = role_names.get(*role).map(String::as_str).unwrap_or("<unknown>");
format!(
"this blueprint does not compile to a runnable harness: root role \"{name}\" is \
unbound it is declared open (an `input` role) but there is no enclosing graph \
to wire it when run standalone"
)
}
/// The bare measurement run (C28 phase 3): `run_signal_r` MINUS `wrap_r` and the
/// eq/ex/r R-evaluation, KEEPING the declared-tap bind → drain → persist (C27).
/// No broker, no risk executor, no per-cycle equity/exposure/r recorders — this
@@ -79,12 +104,19 @@ pub fn run_measurement(
std::process::exit(1);
}
let names: Vec<String> = signal.param_space().iter().map(|p| p.name.clone()).collect();
// #339 item 3: `signal`'s own root-role names, captured before
// `compile_with_params` consumes it below — the bare-tap path compiles
// the signal DIRECTLY (unlike the bias/strategy arm's `wrap_r` nesting),
// so an unbound open root role surfaces here as `CompileError::
// UnboundRootRole { role }`, a flat index with no name attached at the
// engine boundary. `role_names` lets `compile_error_prose` resolve it.
let role_names: Vec<String> = signal.input_roles().iter().map(|r| r.name.clone()).collect();
let defaults = raw_bound_defaults(&signal);
let (sources, window, _pip_size) = resolve_run_data(&data, env, &binding);
// Compile the signal DIRECTLY — no wrap_r, no broker/executor/eq-ex-r sinks.
let mut flat = signal.compile_with_params(params).unwrap_or_else(|e| {
eprintln!("aura: this blueprint does not compile to a runnable harness: {e:?}");
eprintln!("aura: {}", compile_error_prose(&e, &role_names));
std::process::exit(1);
});
+131 -23
View File
@@ -15,8 +15,8 @@ use std::sync::{mpsc, Arc, LazyLock};
use aura_composites::{cost_graph, risk_executor, StopRule};
use aura_core::{zip_params, Cell, Firing, ParamSpec, PrimitiveBuilder, Scalar, ScalarKind, Timestamp};
use aura_engine::{
blueprint_from_json, window_of, BlueprintNode, Composite, GraphBuilder, Harness, RunManifest,
VecSource,
blueprint_from_json, window_of, BlueprintNode, CompileError, Composite, GraphBuilder, Harness,
RunManifest, VecSource,
};
use aura_backtest::{
summarize, summarize_r, RunMetrics, RunReport, SimBroker, PM_FIELD_NAMES, PM_RECORD_KINDS,
@@ -471,22 +471,81 @@ pub fn resolve_run_data(
}
}
/// The variant-name string for a `ScalarKind`, matching the wire/CLI spelling
/// used across the codebase (`F64`/`I64`/`Bool`/`Timestamp`).
fn scalar_kind_name(kind: ScalarKind) -> &'static str {
match kind {
ScalarKind::F64 => "F64",
ScalarKind::I64 => "I64",
ScalarKind::Bool => "Bool",
ScalarKind::Timestamp => "Timestamp",
}
}
/// Bug 3 (#319 fieldtest cycle 2): an `--override` value whose lexed kind
/// does not match the param's declared kind (e.g. the bare literal `2` for
/// an F64 param) reaches this compile boundary as a raw
/// `CompileError::ParamKindMismatch { slot, expected, got }` — `slot` is a
/// flat param-space index, meaningless to a caller. `names`/`params` are the
/// SAME order `compile_with_params` was called with (built from `signal`
/// before it was consumed by `wrap_r`, `C11`: composites inline, preserving
/// order), so `slot` indexes both. This names the param path and both kinds,
/// mirroring the campaign leg's own kind-fault prose
/// (`ref_fault_prose`'s `AxisKindMismatch` arm, `research_docs.rs`). Every
/// other `CompileError` variant keeps the existing Debug fallback — out of
/// this bug's scope, and `names`/`params` only reconstruct the one variant
/// that carries an injected value.
fn compile_error_prose(e: &CompileError, names: &[String], params: &[Scalar]) -> String {
let CompileError::ParamKindMismatch { slot, expected, got } = e else {
return format!("this blueprint does not compile to a runnable harness: {e:?}");
};
let path = names.get(*slot).map(String::as_str).unwrap_or("<unknown>");
let example = match (expected, params.get(*slot)) {
(ScalarKind::F64, Some(Scalar::I64(v))) => format!("{v}.0"),
(ScalarKind::F64, _) => "a decimal literal, e.g. 2.0".to_string(),
(ScalarKind::I64, _) => "a bare integer literal, e.g. 2".to_string(),
(ScalarKind::Bool, _) => "true or false".to_string(),
(ScalarKind::Timestamp, _) => "an integer epoch-ns literal".to_string(),
};
format!(
"--override {path}: expects {}, got {} — write {example}",
scalar_kind_name(*expected),
scalar_kind_name(*got)
)
}
/// Run a signal blueprint through the R scaffolding: hash the signal,
/// wrap it (broker + equity/exposure/R sinks), compile with `params`, bootstrap,
/// run over `data`, and build the RunReport (manifest carries topology_hash).
/// The single construction+run path shared by the `aura run <blueprint.json>` CLI
/// arm and its bit-identical test.
///
/// `topo`: `None` computes `topology_hash` inline from `signal` as always
/// (every existing caller); `Some(hash)` overrides it with the caller's own
/// **reference-semantics** hash (#343, revised) — the exec blueprint leg's
/// `--override` branch passes the loaded base document's content id, computed
/// BEFORE `reopen_all`, so the manifest reaching every consumer inside this
/// function (the record line AND the trace-store persistence below) carries
/// the base document's id, never the reopened topology's own, exactly as the
/// campaign leg's `run_blueprint_member` has always taken its `topo` as a
/// caller-supplied reference (`runner.rs`'s `&cell.strategy_id`).
#[allow(clippy::type_complexity)]
pub fn run_signal_r(
signal: Composite, params: &[Scalar], data: RunData, seed: u64, env: &Env,
plan: TapPlan,
plan: TapPlan, topo: Option<&str>,
) -> RunReport {
// topology_hash's own two-line body, inlined: `content_id_of` over the
// canonical (#164) blueprint JSON — the CLI shell's `topology_hash`
// helper is the same primitive, kept single-sourced at `aura_research`.
let topo = aura_research::content_id_of(
&aura_engine::blueprint_to_json(&signal).expect("a buildable signal serializes"),
); // before signal is consumed
// Skipped when the caller already supplies a reference-semantics hash
// (`topo: Some(..)`) — no need to hash a signal whose own topology_hash
// will be overridden anyway.
let topo: String = match topo {
Some(t) => t.to_string(),
None => aura_research::content_id_of(
&aura_engine::blueprint_to_json(&signal).expect("a buildable signal serializes"),
), // before signal is consumed
};
let run_name = signal.name().to_string(); // before signal is consumed by `wrap_r`
// The default binding (name defaults; `aura run` carries no campaign
// overrides). Refusals are the established `aura: ` + exit-1 register.
@@ -514,7 +573,7 @@ pub fn run_signal_r(
let (sources, window, pip_size) = resolve_run_data(&data, env, &binding);
let wrapped = wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, false, pip_size, &binding, None);
let mut flat = wrapped.compile_with_params(params).unwrap_or_else(|e| {
eprintln!("aura: this blueprint does not compile to a runnable harness: {e:?}");
eprintln!("aura: {}", compile_error_prose(&e, &names, params));
std::process::exit(1);
});
// Bind each declared tap per the plan's subscription, before bootstrap
@@ -606,9 +665,10 @@ pub fn run_blueprint_member(
h.run_bound(key_supply(binding, sources))
.expect("sources opened against `binding` key-match that binding's own roles by construction");
let mut named = zip_params(space, point); // by-name params for the manifest record
// `match` (not an irrefutable `let`): `StopRule` also has a `Fixed` variant,
// which stamps no vol knobs. The campaign/single-run paths only pass `Vol`
// or `VolTf`, so the `Fixed` arm is inert here.
// One arm's knobs per member (#233 Vol/VolTf, #338 Fixed) — the manifest
// stamp `stop_rule_from_params` reads back (`translate.rs`), so a
// campaign's `RiskRegime::Fixed` cell reproduces its own distance, not
// the baked default vol-stop.
match stop {
StopRule::Vol { length, k } => {
named.push(("stop_length".to_string(), Scalar::i64(length)));
@@ -619,7 +679,9 @@ pub fn run_blueprint_member(
named.push(("stop_length".to_string(), Scalar::i64(length)));
named.push(("stop_k".to_string(), Scalar::f64(k)));
}
StopRule::Fixed(_) => {}
StopRule::Fixed(distance) => {
named.push(("stop_distance".to_string(), Scalar::f64(distance)));
}
}
// Stamp the cost model the member ran under, beside the stop knobs (#234,
// the #233 pattern): one `cost[k].<knob>` param per component, in
@@ -659,8 +721,9 @@ pub fn run_blueprint_member(
/// against: the loaded signal wrapped in the r-sma scaffolding (stop bound,
/// reduce, no cost), taps discarded. `param_space()` on it is the axis
/// namespace `--axis` binds; `.axis()` consumes it to seed a sweep. Single
/// source for the sweep terminal, the MC closed-check, AND `--list-axes`, so
/// the listed names track the swept names by construction (incl. across #159's
/// source for the sweep terminal, the MC closed-check, AND
/// `aura graph introspect --params`, so the listed names track the swept
/// names by construction (incl. across #159's
/// harness retirement). The reload is infallible under the SAME
/// dispatch-boundary contract the callers already rely on: the doc is
/// `blueprint_from_json`-validated at the `["sweep", ..]` / `["mc", ..]`
@@ -673,7 +736,8 @@ pub fn blueprint_axis_probe(doc: &str, env: &Env) -> Composite {
/// The axis probe with a #246 override set re-opened on the strategy BEFORE
/// wrapping — probe and per-member reloads must re-open identically so points
/// resolve against one space. The empty-set form is byte-equal to the old
/// probe (mc/run closed-checks and the open `--list-axes` lines read that).
/// probe (mc/run closed-checks and the open `aura graph introspect --params`
/// lines read that).
pub fn blueprint_axis_probe_reopened(doc: &str, env: &Env, overrides: &[String]) -> Composite {
let signal = blueprint_from_json(doc, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary; reload is infallible");
@@ -767,7 +831,7 @@ pub fn override_paths(
None => {
return Err(format!(
"axis {name}: names no param of this blueprint (open or bound) — \
see `aura sweep <bp> --list-axes`"
see `aura graph introspect --params <bp>`"
));
}
}
@@ -1160,8 +1224,8 @@ mod tests {
let env = Env::std();
let loaded = blueprint_from_json(include_str!("../../aura-cli/examples/r_breakout.json"), &|t| std_vocabulary(t))
.expect("shipped r_breakout example loads");
let via_file = run_signal_r(loaded, &[], RunData::Synthetic, 0, &env, TapPlan::record_all());
let via_carve = run_signal_r(r_breakout_signal(Some(R_BREAKOUT_CHANNEL)), &[], RunData::Synthetic, 0, &env, TapPlan::record_all());
let via_file = run_signal_r(loaded, &[], RunData::Synthetic, 0, &env, TapPlan::record_all(), None);
let via_carve = run_signal_r(r_breakout_signal(Some(R_BREAKOUT_CHANNEL)), &[], RunData::Synthetic, 0, &env, TapPlan::record_all(), None);
assert_eq!(via_file.metrics, via_carve.metrics, "loaded example grades identically to the carve");
}
@@ -1313,7 +1377,7 @@ mod tests {
let env = Env::std();
let loaded = blueprint_from_json(include_str!("../../aura-cli/examples/r_meanrev.json"), &|t| std_vocabulary(t))
.expect("shipped r_meanrev example loads");
let via_file = run_signal_r(loaded, &[], RunData::Synthetic, 0, &env, TapPlan::record_all());
let via_file = run_signal_r(loaded, &[], RunData::Synthetic, 0, &env, TapPlan::record_all(), None);
let via_carve = run_signal_r(
r_meanrev_signal(Some(R_MEANREV_WINDOW), Some(R_MEANREV_BAND_K)),
&[],
@@ -1321,6 +1385,7 @@ mod tests {
0,
&env,
TapPlan::record_all(),
None,
);
assert_eq!(via_file.metrics, via_carve.metrics, "loaded example grades identically to the carve");
}
@@ -1649,6 +1714,49 @@ mod tests {
);
}
/// #338 write-side: `run_blueprint_member` given `StopRule::Fixed` actually
/// bootstraps and runs the member (the `risk_executor`/`FixedStop` arm,
/// end-to-end over a real synthetic realization) and stamps `stop_distance`
/// into the manifest under the key `stop_rule_from_params`'s round-trip
/// reads back — the `run_blueprint_member_stamps_the_vol_tf_stop_knobs`
/// precedent above, `Fixed` edition.
#[test]
fn run_blueprint_member_stamps_the_fixed_stop_distance() {
let env = Env::std();
let doc = blueprint_to_json(&load_closed_r_sma()).expect("serializes");
let reload = || blueprint_from_json(&doc, &|t| std_vocabulary(t)).expect("loads");
let space = blueprint_axis_probe(&doc, &env).param_space();
let binding = crate::binding::resolve_binding("fixedstamp", reload().input_roles(), &BTreeMap::new())
.expect("the price role resolves");
let stop = StopRule::Fixed(10.0);
let (sources, window, pip) = resolve_run_data(&RunData::Synthetic, &env, &binding);
let run = run_blueprint_member(
reload(),
&[],
&space,
sources,
window,
0,
pip,
"topo",
&env,
stop,
&binding,
&[],
"GER40",
);
assert!(
run.manifest.params.contains(&("stop_distance".to_string(), Scalar::f64(10.0))),
"manifest must stamp stop_distance: {:?}",
run.manifest.params
);
assert!(
!run.manifest.params.iter().any(|(k, _)| k == "stop_length" || k == "stop_k"),
"a Fixed stop stamps no vol knobs: {:?}",
run.manifest.params
);
}
/// A fresh project root so a run's `runs/` lands in a temp dir.
fn temp_project_env(name: &str) -> (Env, PathBuf) {
let root = std::env::temp_dir()
@@ -1692,7 +1800,7 @@ mod tests {
fn fold_and_live_plans_agree_with_the_recorded_series() {
// Run A: record (the charting question).
let (env_a, root_a) = temp_project_env("record");
run_signal_r(tapped_r_sma(), &[], RunData::Synthetic, 0, &env_a, TapPlan::record_all());
run_signal_r(tapped_r_sma(), &[], RunData::Synthetic, 0, &env_a, TapPlan::record_all(), None);
let series = read_tap_json(&root_a);
let ts: Vec<i64> =
series["ts"].as_array().unwrap().iter().map(|v| v.as_i64().unwrap()).collect();
@@ -1704,7 +1812,7 @@ mod tests {
let (env_b, root_b) = temp_project_env("fold");
let mut plan_b = TapPlan::record_all();
plan_b.subscribe("fast_tap", TapSubscription::named("mean"));
run_signal_r(tapped_r_sma(), &[], RunData::Synthetic, 0, &env_b, plan_b);
run_signal_r(tapped_r_sma(), &[], RunData::Synthetic, 0, &env_b, plan_b, None);
let row = read_tap_json(&root_b);
let mean = vals.iter().sum::<f64>() / vals.len() as f64; // sequential — FoldState's order
assert_eq!(row["ts"], serde_json::json!([*ts.last().unwrap()]));
@@ -1720,7 +1828,7 @@ mod tests {
let _ = live_tx.send((ts.0, cell.f64()));
}),
);
run_signal_r(tapped_r_sma(), &[], RunData::Synthetic, 0, &env_c, plan_c);
run_signal_r(tapped_r_sma(), &[], RunData::Synthetic, 0, &env_c, plan_c, None);
let got: Vec<(i64, f64)> = live_rx.try_iter().collect();
let want: Vec<(i64, f64)> = ts.iter().copied().zip(vals.iter().copied()).collect();
assert_eq!(got, want, "the live closure saw exactly the recorded series (C1)");
@@ -1771,8 +1879,8 @@ mod tests {
fn two_identical_record_runs_produce_byte_identical_tap_files() {
let (env_a, root_a) = temp_project_env("det-a");
let (env_b, root_b) = temp_project_env("det-b");
run_signal_r(tapped_r_sma(), &[], RunData::Synthetic, 0, &env_a, TapPlan::record_all());
run_signal_r(tapped_r_sma(), &[], RunData::Synthetic, 0, &env_b, TapPlan::record_all());
run_signal_r(tapped_r_sma(), &[], RunData::Synthetic, 0, &env_a, TapPlan::record_all(), None);
run_signal_r(tapped_r_sma(), &[], RunData::Synthetic, 0, &env_b, TapPlan::record_all(), None);
let a = std::fs::read(
root_a.join("runs").join("traces").join("sma_signal").join("fast_tap.json"),
)
+40 -12
View File
@@ -4,10 +4,11 @@
//! the cost model) must still round-trip through a manifest, so a stored
//! member (family, reproduce) re-derives bit-identically. These translators
//! are the single source for each such write<->read pair — the manifest
//! `stop_length`/`stop_k`/`stop_period_minutes` <-> [`StopRule`] binding, and
//! the `cost[k].<knob>` <-> [`aura_research::CostSpec`] binding — so the two
//! halves of each pair cannot drift out of sync between the run/campaign
//! paths and reproduce/persist.
//! `stop_length`/`stop_k`/`stop_period_minutes`/`stop_distance` <->
//! [`StopRule`] binding, and the `cost[k].<knob>` <->
//! [`aura_research::CostSpec`] binding — so the two halves of each pair
//! cannot drift out of sync between the run/campaign paths and
//! reproduce/persist.
use aura_composites::StopRule;
use aura_core::{PrimitiveBuilder, Scalar};
@@ -23,11 +24,14 @@ pub const R_SMA_STOP_LENGTH: i64 = 3;
pub const R_SMA_STOP_K: f64 = 2.0;
/// Re-derive the `StopRule` a member was minted under from its manifest params
/// (`stop_length`/`stop_k`/`stop_period_minutes`, stamped by `run_blueprint_member`):
/// if `stop_period_minutes` is present alongside `stop_length`/`stop_k`, this
/// re-derives `VolTf`; otherwise falls back to `Vol`, and to the default
/// vol-stop regime when the manifest carries no stop knobs at all (pre-#233
/// members), mirroring [`stop_rule_for_regime`]'s `None` arm for the same
/// (`stop_length`/`stop_k`/`stop_period_minutes`/`stop_distance`, stamped by
/// `run_blueprint_member`): if `stop_period_minutes` is present alongside
/// `stop_length`/`stop_k`, this re-derives `VolTf`; `stop_length`/`stop_k`
/// alone re-derives `Vol`; `stop_distance` alone (#338) re-derives `Fixed` —
/// the three stamps are mutually exclusive (`run_blueprint_member` stamps
/// exactly one arm's knobs per member). Falls back to the default vol-stop
/// regime when the manifest carries no stop knobs at all (pre-#233 members),
/// mirroring [`stop_rule_for_regime`]'s `None` arm for the same
/// one-directional widening `point_from_params` already applies to missing
/// manifest params.
pub fn stop_rule_from_params(params: &[(String, Scalar)]) -> StopRule {
@@ -35,11 +39,13 @@ pub fn stop_rule_from_params(params: &[(String, Scalar)]) -> StopRule {
params.iter().find(|(n, _)| n == "stop_period_minutes").map(|(_, s)| s.as_i64());
let length = params.iter().find(|(n, _)| n == "stop_length").map(|(_, s)| s.as_i64());
let k = params.iter().find(|(n, _)| n == "stop_k").map(|(_, s)| s.as_f64());
match (period_minutes, length, k) {
(Some(period_minutes), Some(length), Some(k)) => {
let distance = params.iter().find(|(n, _)| n == "stop_distance").map(|(_, s)| s.as_f64());
match (period_minutes, length, k, distance) {
(Some(period_minutes), Some(length), Some(k), _) => {
StopRule::VolTf { period_minutes, length, k }
}
(None, Some(length), Some(k)) => StopRule::Vol { length, k },
(None, Some(length), Some(k), _) => StopRule::Vol { length, k },
(None, None, None, Some(distance)) => StopRule::Fixed(distance),
_ => StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K },
}
}
@@ -94,6 +100,7 @@ pub fn cost_specs_from_params(
/// default vol-stop, `Some(RiskRegime::Vol { .. })` binds that regime's own
/// params. Single-sourced so the persist-side re-run structurally cannot
/// diverge from the run-side binding again (the #219 divergence class).
/// `Fixed { distance }` (#338) binds the shipped `FixedStop` composite.
pub fn stop_rule_for_regime(regime: Option<aura_research::RiskRegime>) -> aura_composites::StopRule {
match regime {
None => aura_composites::StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K },
@@ -103,6 +110,7 @@ pub fn stop_rule_for_regime(regime: Option<aura_research::RiskRegime>) -> aura_c
Some(aura_research::RiskRegime::VolTf { period_minutes, length, k }) => {
aura_composites::StopRule::VolTf { period_minutes, length, k }
}
Some(aura_research::RiskRegime::Fixed { distance }) => aura_composites::StopRule::Fixed(distance),
}
}
@@ -178,6 +186,15 @@ mod tests {
assert_eq!(stop_rule_from_params(&vol), StopRule::Vol { length: 3, k: 2.0 });
}
#[test]
/// #338 round-trip: a manifest carrying ONLY `stop_distance` (no vol
/// knobs) re-derives `StopRule::Fixed` — the `stop_rule_from_params_round_trips_the_vol_tf_stamp`
/// precedent above, `Fixed` edition.
fn stop_rule_from_params_round_trips_the_fixed_distance_stamp() {
let fixed = vec![("stop_distance".to_string(), Scalar::f64(10.0))];
assert_eq!(stop_rule_from_params(&fixed), StopRule::Fixed(10.0));
}
#[test]
/// #262: `stop_rule_for_regime` binds `RiskRegime::VolTf` to
/// `StopRule::VolTf` field-for-field — the resolve-side half of the
@@ -193,6 +210,17 @@ mod tests {
);
}
#[test]
/// #338: `stop_rule_for_regime` binds `RiskRegime::Fixed` to
/// `StopRule::Fixed` — the resolve-side half of the write/resolve pair the
/// manifest round-trip test above covers from the stamp side (the
/// `stop_rule_for_regime_binds_vol_tf_field_for_field` precedent, `Fixed`
/// edition).
fn stop_rule_for_regime_binds_fixed_distance() {
let regime = Some(aura_research::RiskRegime::Fixed { distance: 10.0 });
assert_eq!(stop_rule_for_regime(regime), aura_composites::StopRule::Fixed(10.0));
}
#[test]
/// #234: the one CostSpec -> builder binding maps each component to its
/// shipped cost node with the knob BOUND — a bound component adds no open
@@ -185,7 +185,6 @@ fn the_shell_defines_no_domain_modules_and_no_lib_target() {
"render.rs",
"research_docs.rs",
"scaffold.rs",
"verb_sugar.rs",
],
"C28 (#295): the shell holds argv/dispatch, argv->document translation, \
and presentation only a new module needs a library home"
+154 -44
View File
@@ -10,7 +10,7 @@ JSON artifact kinds you author headlessly along that arc:
through `aura process validate|introspect|register|show`;
3. a **campaign document** (role 6b) — experiment intent (instruments ×
windows × strategy × axes × process) through `aura campaign
validate|introspect|register|show|run|runs`.
validate|introspect|register|runs|show`, executed by `aura exec` (#319).
Each section below is a worked, verified example — every command shown was
run against this repo and the output is transcribed, not invented. The *why*
@@ -39,9 +39,11 @@ the same shape every node in `aura-std` already follows.
scaffolds a **data-only** project (`docs/project-layout.md`, "A project
repo (two tiers)") — a strategy over the std vocabulary is a
blueprint/campaign document, not Rust: the scaffold ships **one** closed
`signal.json` starter that serves both verbs`aura run` uses its bound
values as-is, `aura sweep --axis fast.length=2,4,8` overrides them
(bound = default, not fixed); `--list-axes` lists the open knobs and the
`signal.json` starter that serves both legs of `exec` (#319) — `aura exec
signal.json` uses its bound values as-is, `aura exec signal.json --override
fast.length=8` reopens one for that run, and a campaign document's
`strategies[].axes` reopens one across a family (bound = default, not
fixed); `aura graph introspect --params` lists the open knobs and the
bound defaults alike, so every override target is discoverable.
- **A project-specific *native* node type** — the moment §0's three-part
pattern is actually needed — lives in an attached **node crate**:
@@ -227,10 +229,11 @@ $ aura graph build < smacross.json > blueprint.json
Nodes are referenced by an **identifier** (given by `add`, see below); ports
are dotted `<identifier>.<port>` on both sides of a wire.
### The ten ops
### The eleven ops
| 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. |
| `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). |
@@ -239,9 +242,24 @@ are dotted `<identifier>.<port>` on both sides of a wire.
| `feed` | `{"op":"feed","role":<str>,"into":[<port>, …]}` | fan a previously-declared role into one or more interior input slots, all-or-nothing (a failing target leaves none of the batch wired). |
| `connect` | `{"op":"connect","from":<port>,"to":<port>}` | wire one interior output field to one interior input slot. A `connect` that would close a dataflow cycle is rejected immediately — the only legal feedback path is an explicit delay/state node (domain invariant 5). |
| `expose` | `{"op":"expose","from":<port>,"as":<str>}` | promote an interior output field to a boundary output under the alias `as` — a real *alias* (a terminal boundary name, not a referenceable identifier like `add`'s `name`). Together with `tap`, one of the two ops whose `as` key is a terminal name rather than an identifier. |
| `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 exec` constructs a recorder at each declared tap and persists its per-cycle series as a `ColumnarTrace`; a campaign member run 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). |
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 (a
second unnamed strategy's run overwrites the first's), 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
the first and third of these — giving each blueprint its own distinct name
stops *different* blueprints from colliding with each other. It does NOT by
itself dissolve the second: two runs of the *identical*, now uniquely-named
blueprint still share one `traces/<name>/` directory, so a later run still
overwrites an earlier one's trace — naming separates blueprints from each
other, not a blueprint's own repeated runs from each other (that trace-dir
question is tracked separately, not resolved here).
Value forms are the typed-tag representations used everywhere in this
family of artifacts:
@@ -274,9 +292,10 @@ corpus's own example, verified below; byte-identical to the on-disk
A `bind` in an `add` op pins that param to a value and removes it from the
**open** param space (`--params`): a bound param is a **default** (#246) — a
run uses it as-is, while any campaign axis or `aura sweep --axis` naming it
re-opens it for that family and binds it per cell. `--list-axes` lists it
after the open knobs as `<name>:<KIND> default=<value>`. `bind` is for a
plain `exec` uses it as-is, while a campaign axis naming it re-opens it for
that family (bound per cell) and `exec`'s own `--override NODE.PARAM=VALUE`
(#319) re-opens it for that one execution. `graph introspect --params` lists
it after the open knobs as `<name>:<KIND> default=<value>`. `bind` is for a
value the strategy carries by default; leave a param unbound, as all three
are here, to make binding it mandatory for every sweep.
@@ -284,7 +303,7 @@ A param therefore has three states: **open** (an axis every sweep MUST bind),
**bound** (a default any axis MAY override, #246), and **ganged** (open, but
fused with its siblings under ONE public knob declared by a `gang` op; the
member addresses are unbindable and only the gang's own single-segment name —
e.g. `channel_length` — appears in `--list-axes`).
e.g. `channel_length` — appears in `graph introspect --params`).
### Worked example: declaring a measurement tap
@@ -313,13 +332,96 @@ bias scaling) under the name `spread`:
The built blueprint carries a `taps` array naming the resolved wire
(`{"name":"spread","from":{"node":<sub's index>,"field":0}}` — the name is
addressed, the index is resolved *for* you). A single `aura run <blueprint>`
addressed, the index is resolved *for* you). A single `aura exec <blueprint>`
then constructs a recorder at that point and persists the `spread` series as a
`ColumnarTrace` in the run's trace store, chartable by its name. A tap is inert
in a sweep (no per-cell recorder) — it is a single-run observation surface.
in a campaign member run (no per-cell recorder) — it is a single-run
observation surface (or, per campaign, what a nominee's `persist_taps`
re-run records, §3).
(`sub.value` is tapped here purely to illustrate; any interior output field is
a legal tap source, and a producer needs no other consumer to be tapped.)
### Worked example: a `LinComb` op-script
`LinComb` is **arg-bearing** (#271): `graph introspect --node LinComb` shows
only the `arity` arg and the pending note until `args` supplies it — the
ports (`term[0]`, `term[1]`, …) and params (`weights[0]`, `weights[1]`, …)
form only once `arity` is real, one pair per term. A minimal weighted blend
of two sources:
```json
[
{"op": "source", "role": "a", "kind": "F64"},
{"op": "source", "role": "b", "kind": "F64"},
{"op": "add", "type": "LinComb", "name": "combo",
"args": {"arity": "2"}, "bind": {"weights[0]": {"F64": 0.7}}},
{"op": "feed", "role": "a", "into": ["combo.term[0]"]},
{"op": "feed", "role": "b", "into": ["combo.term[1]"]},
{"op": "expose", "from": "combo.value", "as": "blend"}
]
```
`args` fixes the arity BEFORE `bind`/`feed` can address `term[i]`/`weights[i]`
at all — the same closed-args-then-bind order every arg-bearing type follows
(§0/#271). `weights[0]` is bound to `0.7` here, so it becomes a default
(#246); the unbound `weights[1]` stays a required axis, appearing in `graph
introspect --params` as `combo.weights[1]:F64` (alongside
`combo.weights[0]:F64 default=0.7`).
### Worked example: wiring a cost model (`ConstantCost``CostSum`)
`CostSum`'s `cost[k].<field>` inputs (C10) are meaningless fed from a bare
source: they read the co-temporal record a real `PositionManagement` node
emits (`closed`, `open`, `entry_price`, `stop_price`). Every node in that
chain — `Bias`, `FixedStop`, `Sizer`, `PositionManagement`, `ConstantCost`,
`CostSum` — sits in the closed vocabulary, so the whole cost-model wiring is
honestly expressible by hand, one `add`/`connect` pair at a time, mirroring
the Rust `risk_executor` + `cost_graph` composite-builders (`aura-composites`)
field-for-field:
```json
[
{"op": "source", "role": "price", "kind": "F64"},
{"op": "source", "role": "signal", "kind": "F64"},
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 1.0}}},
{"op": "feed", "role": "signal", "into": ["bias.signal"]},
{"op": "add", "type": "FixedStop", "name": "stop", "bind": {"distance": {"F64": 0.01}}},
{"op": "add", "type": "PositionManagement", "name": "pm"},
{"op": "feed", "role": "price", "into": ["stop.price", "pm.price"]},
{"op": "add", "type": "Sizer", "name": "sizer", "bind": {"risk_budget": {"F64": 1.0}}},
{"op": "connect", "from": "bias.bias", "to": "sizer.bias"},
{"op": "connect", "from": "stop.stop_distance", "to": "sizer.stop_distance"},
{"op": "connect", "from": "bias.bias", "to": "pm.bias"},
{"op": "connect", "from": "stop.stop_distance", "to": "pm.stop_distance"},
{"op": "connect", "from": "sizer.size", "to": "pm.size"},
{"op": "add", "type": "ConstantCost", "name": "cc", "bind": {"cost_per_trade": {"F64": 0.5}}},
{"op": "connect", "from": "pm.closed_this_cycle", "to": "cc.closed"},
{"op": "connect", "from": "pm.open", "to": "cc.open"},
{"op": "connect", "from": "pm.entry_price", "to": "cc.entry_price"},
{"op": "connect", "from": "pm.stop_price", "to": "cc.stop_price"},
{"op": "add", "type": "CostSum", "name": "costsum", "args": {"n_costs": "1"}},
{"op": "connect", "from": "cc.cost_in_r", "to": "costsum.cost[0].cost_in_r"},
{"op": "connect", "from": "cc.cum_cost_in_r", "to": "costsum.cost[0].cum_cost_in_r"},
{"op": "connect", "from": "cc.open_cost_in_r", "to": "costsum.cost[0].open_cost_in_r"},
{"op": "expose", "from": "pm.realized_r", "as": "realized_r"},
{"op": "expose", "from": "costsum.cost_in_r", "as": "cost_in_r"}
]
```
`CostSum`'s port names ARE the `cost[k].<field>` shape already
(`cost[0].cost_in_r`, …) — `cost_port` is baked into `CostSum::make`'s schema
itself (#271), no composite-level renaming needed; a second factor is
`args: {"n_costs": "2"}` plus a second cost-node block wired into
`cost[1].*`, and `CostSum` per-field-sums whatever arity it was given (#341
item 4). This closes the "CostSum reachable but pathless" gap: the ports
genuinely connect at the op-script level, using only closed-vocabulary
nodes. It is a standalone wiring reference, though, not the production
attachment path — a campaign strategy's real cost model still comes from
the campaign document's `cost:` block (C10, `cost_nodes_for`), wrapped
around the user's bias-only blueprint entirely in Rust at run time; a
user's op-script-authored blueprint never embeds `PositionManagement` /
cost nodes itself.
### Session anchoring: the `SessionFrankfurt` preset
To anchor logic to the trading session, the closed vocabulary ships a
@@ -402,6 +504,9 @@ SMA
param length:I64 (bind {"I64": <v>})
$ aura graph introspect --unwired < partial.json # open slots of a partial op-script
sub.rhs:F64
$ aura graph introspect --taps tapped.json # one row per declared tap: name, source wire, kind
fast_ma fast.value F64
spread sub.value F64
$ aura graph introspect --content-id smacross.json # SHA-256 of the canonical form
597d719b7ac607158cda3e68cd497387620397a5e93087e23da512876dafba9a
$ aura graph introspect --content-id smacross.json --identity-id # + debug-name-blind identity id, combinable
@@ -413,29 +518,32 @@ slow.length:I64
bias.scale:F64
```
These printed names are the **one** axis namespace (#328): op-script params, a
campaign document's `strategies[].axes` keys (§3), and `aura sweep <blueprint>
--list-axes` / `--axis` (glossary `sweep`) all speak the same raw
`<node>.<param>` form — `--params` and `--list-axes` are line-identical (open
params bare, bound params trailing `default=<value>`), and every discovered
name is verbatim legal as a document axis key, bound params included (#246's
re-open contract):
These printed names are the **one** axis namespace (#328): op-script params
and a campaign document's `strategies[].axes` keys (§3) speak the same raw
`<node>.<param>` form as `graph introspect --params` prints (open params
bare, bound params trailing `default=<value>`) — every discovered name is
verbatim legal as a document axis key, bound params included (#246's
re-open contract), and as the token `exec --override NODE.PARAM=VALUE` (#319)
takes on the single-run leg:
```json
"strategies": [ { "ref": { "content_id": "…" },
"axes": { "fast.length": { "kind": "I64", "values": [3, 5, 7, 9] },
"bias.scale": { "kind": "F64", "values": [0.25, 0.5] } } } ]
```
A single execution reopens the same raw name directly, with no document at all:
```
$ aura sweep smacross.json --axis fast.length=3:9:2 # synthetic: raw works
$ aura sweep smacross.json --axis bias.scale=0.25,0.5 --real ... # real: raw works
$ aura exec smacross.json --override fast.length=5 # single run: raw works
```
The older `<blueprint>.<node>.<param>` wrapped form (e.g. `graph.fast.length`,
what pre-#328 transcripts and `--list-axes` output used to print) is retired
from the surface; naming it refuses with a translation pointer to the raw
candidate, on both intake seams:
what pre-#328 transcripts and the retired `--list-axes` output used to print)
is retired from the surface; naming it in a campaign document refuses with a
translation pointer to the raw candidate, the one remaining intake seam:
```
$ aura sweep smacross.json --axis graph.fast.length=...
aura: axis "graph.fast.length": axis names are raw node.param paths —
use "fast.length" (the wrapped --list-axes form was retired, #328)
$ aura campaign validate wrapped.json # a document quoting an old transcript
aura: campaign references do not resolve:
strategy f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240:
@@ -695,9 +803,9 @@ campaign document valid (executable): pipeline shape and static guards pass
```
$ aura campaign register mra_3_campaign_full_v2.json
registered campaign 42edebd2159de708009ba21e1ed4aea2cffabc373bf761c5765f79c190b677bd (…/runs/campaigns/42ed…json)
$ aura campaign run 42edebd2159de708009ba21e1ed4aea2cffabc373bf761c5765f79c190b677bd
{"family_id":"42edebd2-0-GER40-w0-s0-0","report":{...}}
{"family_id":"42edebd2-0-GER40-w0-s0-0-r1","report":{...}}
$ aura exec 42edebd2159de708009ba21e1ed4aea2cffabc373bf761c5765f79c190b677bd
{"family_id":"42edebd2-0-GER40-w0-r0-s0-0","report":{...}}
{"family_id":"42edebd2-0-GER40-w0-r1-s0-0","report":{...}}
{"campaign_run":{"campaign":"42edebd2…","process":"cd9127…","run":0,"seed":42,
"cells":[{"strategy":"597d719b…","instrument":"GER40","window_ms":[...],
@@ -716,24 +824,26 @@ $ aura campaign run 42edebd2159de708009ba21e1ed4aea2cffabc373bf761c5765f79c190b6
```
With two stop regimes every (instrument, window) cell runs twice — the four
cells above are the "4 cell(s)" the validate summary counted. The second
regime's family ids and trace dirs carry the `-r1` ordinal suffix (the
default/first regime stays unsuffixed), each cell record names its regime,
and generalization is keyed per regime — regimes are compared, never pooled.
cells above are the "4 cell(s)" the validate summary counted. The regime
ordinal is an INFIX segment, `-r{k}`, right after the window segment
(`{id8}-{ordinal}-{instrument}-w{k}-r{k}-s{seed}-{member}`) — the
default/first regime carries `-r0` explicitly, never left unmarked — each
cell record names its regime, and generalization is keyed per regime —
regimes are compared, never pooled.
`aura campaign run` is register-then-run sugar for a `.json` file, but the
`aura exec` (#319) is register-then-run sugar for a `.json` file, but the
canonical address is always the content id — running a bare file the first
time registers it implicitly. `aura campaign runs` lists stored
realizations; `aura campaign runs <id>` dumps the bare stored record(s) (not
the `{"campaign_run": …}` emit wrapper above). If `presentation.persist_taps`
is non-empty, the run also persists the named taps under
`runs/traces/<trace_name>/…`, chartable with `aura chart`. The
`sweep`/`walkforward --trace` analog persists every member's taps as a
family charted the same way, by the handle the run prints (members keyed
`<cell>/<member>`) — also by the `--trace <NAME>` you chose, when that name
uniquely names one recorded run (`aura chart <NAME>` resolves it against the
stored campaign documents; a name reused across runs refuses rather than
guessing which one you mean).
is non-empty, the run also persists every member's taps under
`runs/traces/<trace_name>/…` — the deterministic `{campaign8}-{run}` handle
the `campaign_run` record's `trace_name` field prints, never a user-chosen
name — chartable with `aura chart <trace_name>` (members keyed
`<cell>/<member>`); `aura chart` also resolves a bare campaign id/name
against the stored campaign documents when no literal trace directory
matches, refusing rather than guessing when the name is ambiguous across
recorded runs.
Exit codes: a clean run exits 0; a usage error exits 2; a refusal before any
cell runs (invalid document, missing project, unresolvable strategy) exits 1;
@@ -23,3 +23,7 @@ never line-ordered. Duplicate campaign instruments are refused at both the
validate tier and the executor's preflight: the per-cell family name embeds
the raw instrument string, so uniqueness is what keeps concurrent appends from
racing one name's run-index assignment.
**Current-state cross-command-path example (superseded by the #319 sugar
retirement, 2026-07-25):** "e.g. a swept member's `sqn` vs the same cell
re-run under `aura generalize`".
+3 -2
View File
@@ -5,8 +5,9 @@ loop that reaches a unique state after each input tick. Same input (incl. seed)
→ bit-identical run. Two backtests are fully disjoint and run concurrently
without locking. The bit-identity is *per run*: one backtest of given (inputs,
seed) reproduces byte-for-byte. It does **not** extend to a *derived metric*
recomputed for the same params by two *different command paths* (e.g. a swept
member's `sqn` vs the same cell re-run under `aura generalize`), which may differ
recomputed for the same params by two *different command paths* (e.g. a
campaign cell's `sqn` within its sweep stage vs the same params re-run
through `exec`'s blueprint leg with `--override`, #319), which may differ
by floating-point reassociation (≤1 ULP) because the two paths accumulate the
same logical reduction in a different operation order (IEEE-754 non-associativity).
C1 governs the determinism of a single run, not the cross-command bit-identity of
+5 -4
View File
@@ -284,10 +284,11 @@ with like; costed families reproduce bit-identically (incl. `Carry`).
curves; a cost-less doc requesting it keeps a remedy-naming skip notice. There are
no `--cost-*` run-path flags (removed #221/#234); cost travels in the document.
**Risk regime as a structural campaign axis.** The `StopRule{Fixed, Vol}` axis is
realized at the campaign-document level as `CampaignDoc.risk: [RiskRegime]`
(`aura-research`, variants `Vol{length, k}` and `VolTf{period_minutes, length, k}`
(#262), the fixed-stop rule additive when needed) — a kept-separate matrix axis,
**Risk regime as a structural campaign axis.** The `StopRule{Fixed, Vol, VolTf}`
axis is realized at the campaign-document level as `CampaignDoc.risk:
[RiskRegime]` (`aura-research`, variants `Vol{length, k}`,
`VolTf{period_minutes, length, k}` (#262), and `Fixed{distance}` (#338, binding
the shipped `FixedStop` composite)) — a kept-separate matrix axis,
peer of instruments and windows. The executor keys the nominee map by `(strategy,
window, regime)`, so `generalize` aggregates *within* a regime, never across.
Regimes are **compared** at presentation, never argmax-**selected** across (a
@@ -34,3 +34,15 @@ walk-forward families (axes 24 above) build their members over **one** shared
`Arc<DataServer>` (one `FileCache`), so a window is parsed once and every member's
`M1FieldSource` borrows the same cached `Arc<[M1Parsed]>` chunks
(`crates/aura-ingest/src/lib.rs:316`). The single-pass *parse* cost stays tracked as #95.
**Current-state bound-override + identity-anchor clause (superseded by the
#319 sugar retirement, 2026-07-25):** "so axis 1 (param-sweep) may name a
bound param — the family boundary re-opens it on the probe and on every
member reload, and the axis binds it per cell; `run`/`mc` still require
every param resolved (a truly open param refuses). Identity is untouched:
`content_id_of` (`crates/aura-research/src/lib.rs`) and `topology_hash`
(`crates/aura-cli/src/main.rs`) read the authored document, never a
re-opened probe, and each member manifest records its per-cell bindings
(#246, ratified 2026-07-12; the restriction this amends — axes bind only
open knobs — was an implementation consequence of `bind()` shrinking the
param surface, not a recorded decision)."
+19 -7
View File
@@ -5,13 +5,25 @@ RNG-seed) → deterministic run → metrics`. Parameters are typed, ranged, runt
values injected at graph build — no recompile per param-set; the optimizer sees a
generic vector of typed ranges. A **bound blueprint param is that param's
default**: "open" means *must be bound by an axis*, "bound" means *default,
overridable by an axis*, so axis 1 (param-sweep) may name a bound param — the
family boundary re-opens it on the probe and on every member reload, and the axis
binds it per cell; `run`/`mc` still require every param resolved (a truly open
param refuses). Identity is untouched: `content_id_of`
(`crates/aura-research/src/lib.rs`) and `topology_hash`
(`crates/aura-cli/src/main.rs`) read the authored document, never a re-opened
probe, and each member manifest records its per-cell bindings (#246, ratified
overridable by an axis*, so a campaign axis (param-sweep) may name a bound
param — the family boundary re-opens it on the probe and on every member
reload, and the axis binds it per cell; `exec`'s blueprint leg still requires
every param resolved (a truly open param refuses), and its own `--override
NODE.PARAM=VALUE` (#246 residue, #319) reopens exactly the named bound param
for that one execution — the single-member analogue of a campaign axis. The
reopened execution's `topology_hash` stays the base document's own — reference
semantics, not the reopened topology's (#343, revised; full clause in
[C24](c24-blueprint-data.md)'s "Reproduction identity"). Identity is untouched: `content_id_of` (`crates/aura-research/src/lib.rs`) is
the one hashing primitive, read directly off the authored document by
`aura_runner::member`'s `run_signal_r`/`run_blueprint_member` — where the
manifest's `topology_hash` is computed inline by default (`run_signal_r`'s
`topo: Option<&str>` parameter's `None` arm, every caller but one; #319 Task 9
retired the CLI-side wrapper that used to duplicate this one call), or
supplied by the caller as a ready reference-semantics hash (`Some`, #343
revised — `exec`'s override branch passes the loaded base document's own id,
computed before `reopen_all`, so the SAME value reaches both the record line
and the trace-store persistence) — never a re-opened probe either way, and
each member manifest records its per-cell bindings (#246, ratified
2026-07-12; the restriction this amends — axes bind only open knobs — was an
implementation consequence of `bind()` shrinking the param surface, not a recorded
decision). Raw data is shared read-only across sims via `Arc<[T]>` (the
@@ -27,3 +27,27 @@ every hand-rolled usage line reads `Usage: aura <verb> …` (#179, cycle 0101);
refusal diagnostics stay unprefixed (diagnostics are not usage lines). The
machine-first help surface (JSON/manifest help, stdin op-scripts) stays on the
#157/C21 track, not this cycle (settled as human/GNU convention compliance).
**Current-state "Dual grammar" section (superseded by the #319 sugar
retirement, 2026-07-25):** "The four dual-grammar subcommands
(run/sweep/walkforward/mc) keep both grammars under one token via an
optional `[blueprint]` positional plus a post-parse `is_file()` dispatch; the
execution layer is unchanged (arg-plumbing via thin `*_from` adapters). The
machine-first help surface (JSON/manifest help, stdin op-scripts) is
deferred to the #157 / C21 track, distinct from this human/GNU-convention
compliance."
**Current-state "Single document grammar" section's first-draft routing
sentence (superseded by the #319 sugar retirement, 2026-07-25):** "one verb,
`exec <target>`, dispatches on `is_blueprint_file` (a first-positional
naming an existing `.json` file selects the loaded-blueprint leg; anything
else — a campaign file or a registered content id — the campaign leg)." This
undersold the routing by one layer: the code always also peeked the file's
top-level `"kind"` field (`is_campaign_document_file`) to tell a campaign
document ending in `.json` from a blueprint file — both being ordinary
readable `.json` files `is_blueprint_file` alone cannot distinguish — and,
after an independent review found a malformed campaign file was silently
misrouted to the blueprint leg's own JSON-parse error prose, gained a
neutral not-valid-JSON refusal ahead of the leg choice (review Minor-3,
2026-07-25's fix cycle). The current contract's "Single document grammar"
section names this real routing.
@@ -59,12 +59,60 @@ caller branches on the failure class without parsing stderr.
in `crates/aura-cli/src/main.rs`, threaded from the run registry —
[C18](c18-registry.md)).
**Dual grammar.** The four dual-grammar subcommands (run/sweep/walkforward/mc)
keep both grammars under one token via an optional `[blueprint]` positional plus
a post-parse `is_file()` dispatch; the execution layer is unchanged (arg-plumbing
via thin `*_from` adapters). The machine-first help surface (JSON/manifest help,
stdin op-scripts) is deferred to the #157 / [C21](c21-world.md) track, distinct
from this human/GNU-convention compliance.
**Single document grammar (#319, 2026-07-25).** The five dual-grammar
subcommands (run/sweep/walkforward/mc/generalize) that used to keep two
grammars under one token are retired; one verb, `exec <target>`, dispatches
on `is_blueprint_file` layered with a "kind"-field peek
(`classify_exec_document_file`): a first-positional naming an existing
`.json` file that parses as JSON and carries no top-level `"kind"` selects
the loaded-blueprint leg; a file carrying `"kind"`, or a target that is not a
readable `.json` file at all (a registered content id), selects the campaign
leg. A file that does not parse as JSON at all refuses neutrally (exit 2)
before either leg's own document-shape validation runs, rather than
silently choosing a leg (review Minor-3, 2026-07-25). The execution layer
this collapses onto is unchanged in kind (arg-plumbing via thin adapters).
The machine-first help surface (JSON/manifest help, stdin op-scripts) is
deferred to the #157 / [C21](c21-world.md) track, distinct from this
human/GNU-convention compliance.
**`exec`'s target-classification exit partition is pinned, precisely scoped
(#342 item 5).** The partition below governs exec's TARGET-CLASSIFICATION
refusals at the routing seam — the shape-peek (`classify_exec_document_file`)
that decides which leg, if any, a target reaches, strictly before either
leg's own document-shape validation ever runs. A target fault at this seam
splits along the same usage/runtime line the exit-code partition above
already draws, keyed on whether a *real, existing document's content* is
being judged (usage, exit 2 — the content of an argv-named file is itself
part of the argument) or the target simply fails to resolve to any recorded
state at all (runtime, exit 1 — the needed environment/recorded state is
missing). Missing-state faults (exit 1): a target naming no readable file and
no plausible content id (`'{target}' is neither a readable .json file nor a
64-hex content id`), and a well-formed-but-unregistered 64-hex content id
(`no campaign {id} in the project store`) — no file or store entry exists to
have content in either case. File-content faults at the routing seam (exit
2): a file that does not parse as JSON at all, a valid-JSON op-script array
(not a document exec can execute at all — it names the `graph build`
escalation instead), and a kind-bearing document whose `"kind"` is not
`"campaign"` (e.g. a process document handed to `exec`) — all three are a
real, readable file whose content the ROUTING seam itself cannot classify
into a leg. The wrong-kind case is the one realignment this pin made (#342
item 5): it shipped at exit 1 (mirroring the campaign leg's own missing-state
refusals) until this pass, despite being a routing-seam content fault like
its op-script and not-valid-JSON siblings, not a missing-state fault like the
file-vs-id cases — realigned to exit 2 to match its true class, the one
sanctioned behaviour change of this pin.
Once a target classifies as campaign or blueprint, it falls under that leg's
**own** established validation partition, not this one — and the two legs'
content-validation exits differ today, named honestly rather than harmonized:
a `kind:"campaign"` document whose *content* then fails the campaign
doc-tier gate (e.g. an empty axis) exits 1, runtime-class, per the
established campaign contract (pinned by
`campaign_run_invalid_file_refuses_before_touching_store`), while the
blueprint leg's own content faults (a blueprint envelope that fails to parse
or build) exit 2, usage-class. No behaviour changes here — only this pin's
scope is named precisely: it is the routing seam's partition, not a
uniform content-validation rule across both legs.
**Two artifact classes, two redundancy budgets** (#249, ratified 2026-07-13).
The data layer the programmatic face emits is a public interface read raw (by
@@ -271,3 +271,27 @@ and both callers are untouched; maintenance is lazy-only — put-time indexing
was rejected because it would need a roster-free doc-level identity function
whose equivalence to the loaded-composite path no green test ratifies (decision
log: #191 comments).
**Current-state `runs.jsonl`/`families.jsonl` CLI clauses (superseded by the
#319 sugar retirement, 2026-07-25):** "No live producer writes it today —
sweep / walk-forward / mc persist to the family store and `aura run` does not
persist — so the flat lib API is retained but selectively live: `rank_by`
backs `aura runs family … rank`, `optimize` backs walk-forward's in-sample
step (`aura-campaign` and the registry's own selection helper both call it),
while `append`/`load` remain public API with no in-tree caller (a latent
surface for external consumers)." … "CLI: `aura runs families`, `aura runs
family <id> [rank <metric>]`; `aura sweep`/`walkforward`/`mc` persist via
`append_family` with an optional `--name`."
**Current-state "Cross-instrument generalization" paragraph (superseded by
the #319 sugar retirement, 2026-07-25):** "`FamilyKind::CrossInstrument`:
`aura generalize` runs one candidate across an instrument list and persists
the M per-instrument runs via `append_family`, each member self-identifying
through `RunManifest.instrument`. The generalization score (worst-case R
floor + sign-agreement + per-instrument breakdown) is a **recomputable
aggregate** over those members, not a persisted family-level record."
**Current-state "The campaign executor" opening sentence (superseded by the
#319 sugar retirement, 2026-07-25):** "`aura campaign run <file|content-id>`
executes a campaign (a file is register-then-run sugar; the content id is
canonical): a zero-fault referential gate, then the process pipeline."
+28 -19
View File
@@ -39,9 +39,10 @@ by aura. The experiments-&-results plane is the run registry in `aura-registry`
- `runs.jsonl` — the append-only flat store, one `RunReport` per line
(`RunManifest` + `RunMetrics`), with a typed read-path (`load`) and best-first
ranking (`rank_by`/`optimize`). No live producer writes it today — sweep /
walk-forward / mc persist to the family store and `aura run` does not persist —
so the flat lib API is retained but selectively live: `rank_by` backs
ranking (`rank_by`/`optimize`). No live producer writes it today — the campaign
executor (`exec`'s campaign leg, `aura-campaign`, #319) persists to the family
store instead, and `exec`'s blueprint leg (single run) does not persist — so
the flat lib API is retained but selectively live: `rank_by` backs
`aura runs family … rank`, `optimize` backs walk-forward's in-sample step
(`aura-campaign` and the registry's own selection helper both call it), while
`append`/`load` remain public API with no in-tree caller (a latent surface for
@@ -49,14 +50,16 @@ by aura. The experiments-&-results plane is the run registry in `aura-registry`
- `families.jsonl` — the family store. A sweep / Monte-Carlo / walk-forward /
cross-instrument run persists as a *set of related records*, each a
`FamilyRunRecord` (a `RunReport` stamped with `family` + `run` + `kind` +
`ordinal`; `FamilyKind ∈ {Sweep, MonteCarlo, WalkForward, CrossInstrument}`).
`group_families` re-derives a family from the stored links (re-listable /
rankable as a unit — C21). The user-facing `family_id = "{family}-{run}"` handle
is **derived** from the stored `family` name plus a per-name `run` index
(numeric max+1 — not a content hash, so re-running the same family mints a fresh
id). CLI: `aura runs families`, `aura runs family <id> [rank <metric>]`;
`aura sweep`/`walkforward`/`mc` persist via `append_family` with an optional
`--name`.
`ordinal`; `FamilyKind ∈ {Sweep, MonteCarlo, WalkForward, CrossInstrument}`,
the last now dead — below). `group_families` re-derives a family from the
stored links (re-listable / rankable as a unit — C21). The user-facing
`family_id = "{family}-{run}"` handle is **derived** from the stored `family`
name plus a per-name `run` index (numeric max+1 — not a content hash, so
re-running the same family mints a fresh id). CLI: `aura runs families`,
`aura runs family <id> [rank <metric>]`; the campaign executor persists per-cell
sweep/walk-forward families via `append_family` under a deterministic,
campaign-derived name — no user-supplied `--name` (the retired quintet's
naming flag, #319).
- `campaign_runs.jsonl` — one thin `CampaignRunRecord` per campaign run (below),
over untouched family records.
- `blueprints/<hash>.json`, `processes/`, `campaigns/` — the content-addressed
@@ -109,12 +112,13 @@ the byte-exact `topology_hash` keeps every debug role untouched (introspection-o
the blueprint does not use leaves the id byte-stable. `--content-id` and
`--identity-id` are combinable.
**Cross-instrument generalization.** `FamilyKind::CrossInstrument`: `aura generalize`
runs one candidate across an instrument list and persists the M per-instrument runs
via `append_family`, each member self-identifying through `RunManifest.instrument`.
The generalization score (worst-case R floor + sign-agreement + per-instrument
breakdown) is a **recomputable aggregate** over those members, not a persisted
family-level record.
**Cross-instrument generalization (retired standalone family, #319).** The
`FamilyKind::CrossInstrument` variant is now dead: the standalone `aura
generalize` verb that used to run one candidate across an instrument list and
persist the M per-instrument runs via `append_family` is retired. The
surviving cross-instrument grading is `std::generalize`'s campaign-scope
computation (below, "Annotators are terminal") — a **recomputable aggregate**
over the cells' nominees, not a persisted family-level record.
**Research-artifact document stores.** `processes/` and `campaigns/` hold two
document types (C25 roles 5 / 6b): the **process document** (a named
@@ -143,9 +147,14 @@ entry); name coincidence against one of the two namespaces IS the mechanism, and
the referenced blueprint is the single source of truth for which namespace a name
falls in. Pinned by
`referential_tier_accepts_a_kind_correct_axis_over_a_bound_param`
(`aura-registry/src/lib.rs`).
(`aura-registry/src/lib.rs`). The single-run analogue — `exec`'s own
`--override NODE.PARAM=VALUE` reopening a bound param for one execution — now
truthfully has the identical hash consequence (#343, revised): both legs
stamp the referenced/loaded document's own id, never the reopened topology's,
exactly as a reopened campaign member's has always been ([C24](c24-blueprint-data.md)'s
"Reproduction identity" carries the full clause).
**The campaign executor.** `aura campaign run <file|content-id>` executes a
**The campaign executor.** `aura exec <file|content-id>` (#319) executes a
campaign (a file is register-then-run sugar; the content id is canonical): a
zero-fault referential gate, then the process pipeline. The executable shape is
`std::sweep [std::gate]* [std::walk_forward]? [std::monte_carlo]? [std::generalize]?`
@@ -34,3 +34,16 @@ scaffolding's last data weld — `wrap_r`'s hard-wired `price`←close role and
`M1Field::Close`-only open sites — is retired; a strategy's input roles now bind
archive columns by name (C26). `wrap_r`'s remaining R-scaffolding retirement
stays #159.]
**Current-state "Pre-C24 scaffolding retired" paragraph's family-builder
naming (superseded by the #319 sugar retirement, 2026-07-25):** "The family
builders are now generic and blueprint-driven: `blueprint_sweep_family`,
`blueprint_walkforward_family`, `blueprint_mc_family` (and
`blueprint_sweep_over`) in `crates/aura-runner/src/family.rs`." Those four
functions are themselves retired by #319 (2026-07-25): the campaign executor
(`aura_campaign::exec::execute`) builds every family now, driven by the
process document's stage pipeline; `family.rs` keeps only the synthetic-
stream helpers `aura_runner::reproduce` calls to re-derive a recorded family
(`showcase_prices`, `walkforward_prices`, `walkforward_window_source`,
`synthetic_walk_sources`, `DataSource`/`DataChoice`). The current contract's
"Pre-C24 scaffolding retired" paragraph names this real state.
+14 -7
View File
@@ -71,13 +71,20 @@ rather than a hand-enumerated menu. Persisted experiment *intent* is the campaig
document (C25/C18).
**Pre-C24 scaffolding retired.** The pre-C24 scaffolding — `HarnessKind` and the
per-strategy `*_sweep_family` functions — is **gone**. The family builders are now
generic and blueprint-driven: `blueprint_sweep_family`, `blueprint_walkforward_family`,
`blueprint_mc_family` (and `blueprint_sweep_over`) in
`crates/aura-runner/src/family.rs`. The R-evaluator scaffold `wrap_r`
(defined in `crates/aura-runner/src/member.rs`; imported and called from
`runner.rs`) survives; its last hard data weld — the
hard-wired `price`←close role and the `M1Field::Close`-only open sites — is retired
per-strategy `*_sweep_family` functions — is **gone**. The generic blueprint-driven
family builders that first replaced it (`blueprint_sweep_family`,
`blueprint_walkforward_family`, `blueprint_mc_family`, `blueprint_sweep_over`) are,
in turn, retired by the #319 sugar retirement (2026-07-25): the **campaign
executor** (`aura_campaign::exec::execute`) now builds every family, driven by
the process document's stage pipeline (`std::sweep`/`std::grid`,
`std::walk_forward`, `std::monte_carlo`). `crates/aura-runner/src/family.rs`
retains only the synthetic-stream helpers serving `aura_runner::reproduce`
(`showcase_prices`, `walkforward_prices`, `walkforward_window_source`,
`synthetic_walk_sources`, `DataSource`/`DataChoice`) — reproduction's own
re-derivation path, not a family-minting path. The R-evaluator scaffold
`wrap_r` (defined in `crates/aura-runner/src/member.rs`; imported and called
from `runner.rs`) survives; its last hard data weld — the hard-wired
`price`←close role and the `M1Field::Close`-only open sites — is retired
(a strategy's input roles now bind archive columns by name, C26), and `wrap_r`'s
remaining R-scaffolding retirement is tracked at #159.
@@ -135,3 +135,36 @@ the mean shows the net/duty-cycle level. Deferred refinements: rendering the min
envelope honestly as range bars / OHLC rather than a polyline (#112); a `--width`
budget flag and true intra-bucket min/max ordering for the non-default continuous
x-mode (#110).
**Current-state "Single run" + "Families" + "Newcomer" clauses (superseded by
the #319 sugar retirement, 2026-07-25):** "**Single run.** `aura run
<blueprint.json>` persists every tap the blueprint declares to the trace
store under the run's own name, on both shapes: a `bias`-output strategy
(`aura-runner::member::run_signal_r`, R-wrapped) and a bare measurement
blueprint with ≥1 declared tap but no `bias`
(`aura-runner::measure::run_measurement`). A tap-free run writes nothing to
`runs/` and its stdout stays byte-identical. The CLI `--trace <NAME>` flag is
**retired** on `run` (and on `mc`): it parses but is refused at dispatch
(exit 2) — naming a trace is the family / campaign path's job, not a per-run
flag.
**Families.** `aura sweep|walkforward --real <SYM> … --trace <NAME>`
persists *each member* via the campaign path: the dissolved verb is
translated into a content-addressed process + campaign document whose
`presentation.persist_taps` requests the tap vocabulary, run through the one
campaign executor; `aura-runner::runner::persist_campaign_traces` writes
each member under a depth-2 fan-out `runs/traces/<name>/<cell>/<member>/`.
Every written member is independently re-run once in non-reduce trace mode
over its own recorded window, and its re-derived metrics are asserted equal
to the recorded member's — a **C1 drift alarm** that refuses (exit 1) rather
than persist a silently-wrong trace. The **synthetic** sweep / walk-forward
path refuses `--trace` (exit 2); **Monte-Carlo is excluded from trace
persistence** (`mc --real` refuses `--name`/`--trace`, exit 2 — the
real-data R-bootstrap campaign itself runs, but records no per-member
family traces; the synthetic `--seeds` family's realization argument, C12,
does not carry over to one real series). `TraceStore::ensure_name_free`
makes name resolution a total function, refusing cross-kind reuse of one
name by both a run and a family." … (Newcomer, tail sentence) "so `aura run`
prints summary R-metrics to stdout but writes no on-disk trace — a
**chartable** trace today comes from a blueprint that declares taps (single
run) or from a `--real … --trace` family campaign."
+27 -27
View File
@@ -60,31 +60,31 @@ columnar (SoA, C7) `ColumnarTrace` — struct→JSON only — in `aura-engine`
`aura-cli` (`render_chart_html` + the vendored `chart-viewer.js`). The engine stays
headless.
**Single run.** `aura run <blueprint.json>` persists every tap the blueprint
declares to the trace store under the run's own name, on both shapes: a
`bias`-output strategy (`aura-runner::member::run_signal_r`, R-wrapped) and a bare
measurement blueprint with ≥1 declared tap but no `bias`
(`aura-runner::measure::run_measurement`). A tap-free run writes nothing to `runs/`
and its stdout stays byte-identical. The CLI `--trace <NAME>` flag is **retired** on
`run` (and on `mc`): it parses but is refused at dispatch (exit 2) — naming a trace
is the family / campaign path's job, not a per-run flag.
**Single run.** `aura exec <blueprint.json>` (#319) persists every tap the
blueprint declares to the trace store under the run's own name, on both
shapes: a `bias`-output strategy (`aura-runner::member::run_signal_r`,
R-wrapped) and a bare measurement blueprint with ≥1 declared tap but no
`bias` (`aura-runner::measure::run_measurement`). A tap-free run writes
nothing to `runs/` and its stdout stays byte-identical. `exec` carries no
free-standing `--trace <NAME>` flag — naming a trace is the family / campaign
path's job, not a per-run flag (the repeatable `--tap TAP=FOLD` selector,
#310, only chooses which of the blueprint's own declared taps to subscribe,
never names the trace).
**Families.** `aura sweep|walkforward --real <SYM> … --trace <NAME>` persists *each
member* via the campaign path: the dissolved verb is translated into a
content-addressed process + campaign document whose `presentation.persist_taps`
requests the tap vocabulary, run through the one campaign executor;
`aura-runner::runner::persist_campaign_traces` writes each member under a depth-2
fan-out `runs/traces/<name>/<cell>/<member>/`. Every written member is
independently re-run once in non-reduce trace mode over its own recorded window,
and its re-derived metrics are asserted equal to the recorded member's — a **C1
drift alarm** that refuses (exit 1) rather than persist a silently-wrong trace. The
**synthetic** sweep / walk-forward path refuses `--trace` (exit 2);
**Monte-Carlo is excluded from trace persistence** (`mc --real` refuses
`--name`/`--trace`, exit 2 — the real-data R-bootstrap campaign itself runs, but
records no per-member family traces; the synthetic `--seeds` family's realization
argument, C12, does not carry over to one real series).
`TraceStore::ensure_name_free` makes name resolution a total function, refusing
cross-kind reuse of one name by both a run and a family.
**Families.** A campaign document's `presentation.persist_taps` (the closed
tap vocabulary, C18/C27) requests traces for each cell's nominee, run through
`aura exec <campaign.json|id>` (#319 — a document's `axes` replace the
retired `sweep|walkforward --real <SYM> --trace <NAME>` grammar entirely;
there is no per-invocation `--trace`/`--name` flag left on any surface):
`aura-runner::runner::persist_campaign_traces` writes each member under a
depth-2 fan-out `runs/traces/{campaign8}-{run}/<cell>/<member>/`, the name
derived deterministically (`derive_trace_name`), never user-chosen. Every
written member is independently re-run once in non-reduce trace mode over
its own recorded window, and its re-derived metrics are asserted equal to
the recorded member's — a **C1 drift alarm** that refuses (exit 1) rather
than persist a silently-wrong trace. `TraceStore::ensure_name_free` makes
name resolution a total function, refusing cross-kind reuse of one name by
both a run and a family.
**Viewer.** `aura chart <name> [--tap <t>] [--panels]` classifies the name on disk
(`TraceStore::name_kind`: top-level `index.json` → a single Run; member subdirs → a
@@ -117,10 +117,10 @@ vocabulary (an SMA-cross → `Bias` strategy), `.gitignore`, and a project `CLAU
The engine also ships example blueprints under
`crates/aura-cli/examples/r_{sma,breakout,channel,meanrev}.json`. Honest gap against
the Guarantee's "populated trace immediately": these shipped blueprints are
*strategies* (a `bias` output, no declared taps), so `aura run` prints summary
*strategies* (a `bias` output, no declared taps), so `aura exec` prints summary
R-metrics to stdout but writes no on-disk trace — a **chartable** trace today comes
from a blueprint that declares taps (single run) or from a `--real … --trace` family
campaign.
from a blueprint that declares taps (single run) or from a campaign document
requesting `presentation.persist_taps`.
**Deferred.** Live sink streams *during* a run are not built — taps are
buffer-then-drain (collected, then written after the run), and there is no local
@@ -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
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
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
correctness invariant) — on two levels:
@@ -157,4 +157,100 @@ convert every deliberate template improvement into forced churn of the frozen
fixture — the same cross-purpose coupling that rules out regenerating the
fixture from the scaffolder.
**Current-state "Runs and families are built FROM blueprint-data" section
(superseded by the #319 sugar retirement, 2026-07-25):** "`aura run
<blueprint.json>` loads a serialized **signal** blueprint and emits a
`RunReport` **bit-identical** (C1) to its Rust-built twin; the run scaffolding
(sinks / broker / data) is supplied **at run**, not serialized. Beyond a
single run, the World constructs and orchestrates **families** of harnesses
from topology-data:
- `aura sweep <blueprint.json> --axis <name>=<csv>``FamilyKind::Sweep`. A
sweep needs an **open** blueprint (a fully-bound one has an empty
`param_space`).
- `aura mc <blueprint.json> --seeds N``FamilyKind::MonteCarlo`, each seed a
distinct synthetic price walk drawn disjoint-parallel through the engine
`monte_carlo` seam (invariant 1). MC binds no axis, so it needs a **closed**
blueprint (the sweep's distinction inverted); an open one returns a named
`Err`, rendered exit-2 at the builder boundary — no hidden exit in the pure
builder.
- `aura walkforward <blueprint.json> --axis <name>=<csv> [--select
argmax|plateau:mean|plateau:worst]` → `FamilyKind::WalkForward`:
re-optimizes the loaded blueprint's params over the `--axis` grid on each
24/12/12 IS window, selects the winner by `sqn_normalized`, runs it
out-of-sample. Reduce-mode members are R-measured (`oos_r` the meaningful
summary; stitched pip-equity empty, C10).
The family builders live in `aura-runner::family` (`blueprint_sweep_family` /
`blueprint_mc_family` / `blueprint_walkforward_family`). Every member manifest
carries the **shared** `topology_hash` (one signal topology, only params
vary; `member_key` distinguishes members), and each family stores its
blueprint(s) content-addressed so `aura reproduce` re-derives every member
bit-identically (`aura-runner::reproduce`, C18). The synthetic-walk DGP is
the MC machinery, not trader-grade statistics; a real-data block-bootstrap —
and retiring the `synthetic_walk_sources` `len:60`↔warm-up coupling — rides
#172."
**Current-state "Axis discovery" section (superseded by the #319 sugar
retirement, 2026-07-25):** "`aura sweep <blueprint.json> --list-axes` lists
a loaded blueprint's open sweepable knobs (one `<name>:<kind>` per line,
`param_space()` order) and exits; the printed names are exactly what
`--axis` binds. Every listed name is **mandatory** on `sweep` /
`walkforward` — the blueprint must be fully bound before it runs — so a
subset grid is refused with the missing knob named (`BindError::MissingKnob`,
`aura-engine`) and there is no default; pin a knob you do not want to vary
with a single-value axis. A single `blueprint_axis_probe` (`aura-runner`)
single-sources the wrapped probe for the sweep terminal, the MC closed-check,
and the listing, so **listed == swept by construction** (and stays so across
the harness retirement — the listing tracks whatever the sweep actually
resolves). One raw namespace, `<node>.<param>` (a splice path keeps its
interior path, e.g. `anchor.sess.period_minutes`), is the only user-facing
axis name (#328): `graph introspect --params` and `--list-axes` are
line-identical (open params bare, bound params with `default=`) and both
print exactly what `--axis` binds, on both sweep routes. The wrapped
`<blueprint>.<node>.<param>` form the probe still resolves against
internally is retired from the surface, with a translation refusal naming
the raw candidate on both intake seams (`--axis` and `campaign validate`).
`--trace` on `sweep` / `walkforward` writes per-member traces on the
real-data campaign path (depth-2 fan-out, chartable by the printed family
handle, #224); the synthetic path still refuses (`run` / `mc` refuse
`--trace` outright). The live trace-writer is the campaign
`presentation.persist_taps` (`persist_campaign_traces`,
`aura-runner::runner`)."
**Current-state `name`-op gated-intake clause (superseded by the #319 sugar
retirement, 2026-07-25):** "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."
**[C26, 2026-07-10 (#231): the single-price data weld inside the surviving `wrap_r` scaffolding is retired — input roles bind archive columns by name; the wrapper's remaining R-scaffolding retirement stays #159.]** (From the C24 Forbids clause as of that date; the single-price weld was retired at #231/C26, but `wrap_r` itself survives — its full R-scaffolding retirement stays deferred, #159.)
---
> The disjoint-parallel synthetic
> price walks a sweep/MC family draws (`aura_runner::family`) are the family
> machinery, not trader-grade statistics; a real-data block-bootstrap — and
> retiring the `synthetic_walk_sources` `len:60`↔warm-up coupling — rides #172.
(superseded by the #319 sugar retirement, 2026-07-25 — audit-close correction:
no family-minting path draws synthetic walks after the family-builder
retirement; only `reproduce` consumes them)
**"Registered-blueprint splice" clause, build-free-introspection sentence
(amended by the 2026-07-26 harvest, #339 item 4):** "build-free introspection
paths pass `&|_| None`." (The CLI's introspection paths now resolve `use`
refs through the store exactly like `graph build` does, via the shared
`parse_and_resolve_ops` phase — retiring the earlier build-free resolver on
the introspection side; the engine's `subgraph` closure contract itself is
unchanged.)
+92 -51
View File
@@ -75,34 +75,36 @@ channel). `LinComb` / `CostSum` / `Session` — formerly listed here — entered
round-trippable set with #271's typed construction args (see the add-op `args`
clause and the data-driven `format_version` below).
### Runs and families are built FROM blueprint-data
### Runs and families are built FROM documents, via `exec` (#319, 2026-07-25)
`aura run <blueprint.json>` loads a serialized **signal** blueprint and emits a
`RunReport` **bit-identical** (C1) to its Rust-built twin; the run scaffolding (sinks
/ broker / data) is supplied **at run**, not serialized. Beyond a single run, the
World constructs and orchestrates **families** of harnesses from topology-data:
`aura exec <blueprint.json>` loads a serialized **signal** blueprint and runs it
once, emitting a `RunReport` **bit-identical** (C1) to its Rust-built twin (a
no-bias blueprint with ≥1 declared tap runs the measurement leg instead); the
run scaffolding (sinks / broker / data) is supplied **at run**, not serialized.
Beyond a single run, the World constructs and orchestrates **families** from a
**campaign document** (role 6b, [C18](c18-registry.md)/[C25](c25-role-model.md)):
`aura exec <campaign.json|id>` drives the document's `strategies[].axes` through
the `aura-campaign` executor (`aura_campaign::exec::execute`) — one member per
(strategy, instrument, window) cell, the process document's stage pipeline
(`std::sweep [std::gate]* [std::walk_forward]? [std::monte_carlo]?
[std::generalize]?`) deciding what each cell realizes (grid family, gated
survivors, walk-forward roll, MC bootstrap, cross-instrument generalization —
C18's cycle-0107/0108 realizations). The one deliberate argv residue,
`--override NODE.PARAM=VALUE`, reopens a bound param for the execution without
touching the document (blueprint leg: `override_paths` + `reopen_all`; campaign
leg: injected as a single-value axis ahead of `validate_campaign_refs`, refusing
a collision with a document-declared axis).
- `aura sweep <blueprint.json> --axis <name>=<csv>``FamilyKind::Sweep`. A sweep
needs an **open** blueprint (a fully-bound one has an empty `param_space`).
- `aura mc <blueprint.json> --seeds N``FamilyKind::MonteCarlo`, each seed a
distinct synthetic price walk drawn disjoint-parallel through the engine
`monte_carlo` seam (invariant 1). MC binds no axis, so it needs a **closed**
blueprint (the sweep's distinction inverted); an open one returns a named `Err`,
rendered exit-2 at the builder boundary — no hidden exit in the pure builder.
- `aura walkforward <blueprint.json> --axis <name>=<csv> [--select
argmax|plateau:mean|plateau:worst]` → `FamilyKind::WalkForward`: re-optimizes the
loaded blueprint's params over the `--axis` grid on each 24/12/12 IS window,
selects the winner by `sqn_normalized`, runs it out-of-sample. Reduce-mode members
are R-measured (`oos_r` the meaningful summary; stitched pip-equity empty, C10).
The family builders live in `aura-runner::family` (`blueprint_sweep_family` /
`blueprint_mc_family` / `blueprint_walkforward_family`). Every member manifest carries
the **shared** `topology_hash` (one signal topology, only params vary; `member_key`
distinguishes members), and each family stores its blueprint(s) content-addressed so
`aura reproduce` re-derives every member bit-identically (`aura-runner::reproduce`,
C18). The synthetic-walk DGP is the MC machinery, not trader-grade statistics; a
real-data block-bootstrap — and retiring the `synthetic_walk_sources` `len:60`↔warm-up
coupling — rides #172.
Every member manifest carries the **shared** `topology_hash` (one signal
topology, only params vary; `member_key` distinguishes members), and the
executor stores its blueprint(s) content-addressed so `aura reproduce`
re-derives every member bit-identically (C18). The synthetic price walks
(`aura_runner::family::synthetic_walk_sources`) survive the #319 retirement
only as `reproduce`'s re-derivation inputs for historically minted synthetic
families — no family-minting path draws them anymore (the campaign executor
is the one family builder, C20) — and they are not trader-grade statistics; a
real-data block-bootstrap — and retiring the `len:60`↔warm-up coupling —
rides #172.
### Reproduction identity
@@ -117,29 +119,44 @@ itself blueprint-data. The **identity id** (#171) is an additive sibling:
--identity-id`. The byte-exact content id keeps the store/reproduce roles; the
identity id is introspection-only until a dedup consumer exists.
**`topology_hash` carries reference semantics on every leg (#343, revised).**
`exec`'s `--override NODE.PARAM=VALUE` (above) reopens a bound param before
bootstrap, but the executed run's `topology_hash` stamps the **loaded (or
stored) base document's own content id** — computed before the reopen —
never a transient reopened variant, exactly as the campaign leg's #246
axis-over-bound-param path has always stamped the referenced strategy's id
for a reopened member ([C18](c18-registry.md)'s bound-override coincidence).
The manifest's `params` map carries the variation (the bound value still
moves from `defaults` to `params`, #249); `params` plus the base document
deterministically reconstruct the executed run, byte-identically —
reproduction identity, not the hash, is where the variation lives. A no-op
override (re-binding a param to its existing bound value) therefore changes
nothing about identity: the bare run's and the no-op-override run's
`topology_hash` are equal. An unresolved `topology_hash` (a file-target exec
never registered via `graph register`) is still the honest name of a real,
reconstructible topology, not a dangling reference — store-resolution
(`get_blueprint`) is a lookup convenience, never an identity guarantee.
### Axis discovery
`aura sweep <blueprint.json> --list-axes` lists a loaded blueprint's open sweepable
knobs (one `<name>:<kind>` per line, `param_space()` order) and exits; the printed
names are exactly what `--axis` binds. Every listed name is **mandatory** on `sweep` /
`walkforward` — the blueprint must be fully bound before it runs — so a subset grid is
refused with the missing knob named (`BindError::MissingKnob`, `aura-engine`) and
there is no default; pin a knob you do not want to vary with a single-value axis. A
single `blueprint_axis_probe` (`aura-runner`) single-sources the wrapped probe for the
sweep terminal, the MC closed-check, and the listing, so **listed == swept by
construction** (and stays so across the harness retirement — the listing tracks
whatever the sweep actually resolves). One raw namespace, `<node>.<param>` (a splice
path keeps its interior path, e.g. `anchor.sess.period_minutes`), is the only
user-facing axis name (#328): `graph introspect --params` and `--list-axes` are
line-identical (open params bare, bound params with `default=`) and both print
exactly what `--axis` binds, on both sweep routes. The wrapped
`<blueprint>.<node>.<param>` form the probe still resolves against internally is
retired from the surface, with a translation refusal naming the raw candidate on
both intake seams (`--axis` and `campaign validate`). `--trace` on
`sweep` / `walkforward` writes per-member traces on the real-data campaign path
(depth-2 fan-out, chartable by the printed family handle, #224); the synthetic path
still refuses (`run` / `mc` refuse `--trace` outright). The live trace-writer is the
campaign `presentation.persist_taps` (`persist_campaign_traces`, `aura-runner::runner`).
`aura graph introspect --params <blueprint.json|id>` (#328) is the one
axis-discovery surface: it lists a loaded blueprint's open knobs (one
`<name>:<kind>` per line, `param_space()` order) followed by its bound knobs
(`<name>:<kind> default=<value>`) — the campaign-axis namespace
`validate_campaign_refs` checks a campaign document's `strategies[].axes`
against. Every open name a campaign document's `axes` varies is **mandatory**:
a subset is refused with the missing knob named (`BindError::MissingKnob`,
`aura-engine`) and there is no default; pin a knob you do not want to vary with
a single-value axis. One raw namespace, `<node>.<param>` (a splice path keeps
its interior path, e.g. `anchor.sess.period_minutes`), is the only user-facing
axis name (#328) — the wrapped `<blueprint>.<node>.<param>` form is retired
from the surface, with a did-you-mean refusal naming the raw candidate at the
one remaining intake seam, `campaign validate` (#319 retired the standalone
`--axis`/`--list-axes` CLI flags this used to also gate). Per-member traces
are the campaign document's `presentation.persist_taps` (closed vocabulary,
C18 cycle-0109) or, on `exec`'s blueprint leg, the repeatable `--tap
TAP=FOLD` selector (#310) over a blueprint's own declared taps (C27); the
live trace-writer is `persist_campaign_traces` (`aura-runner::runner`).
### The construction service (op-script)
@@ -181,7 +198,12 @@ it ever reaches the session, so a doc-less fetched entry cannot enter a NEW
composition — a **backstop**: the register verb already gates both input forms, so
this fires only for store content written before C29 or through the raw in-crate
path), and the resolution echo — happens CLI-side, at DTO conversion, before
replay; build-free introspection paths pass `&|_| None`. The echo
replay; the CLI's introspection paths resolve `use` refs through the store
exactly like `graph build` does (`parse_and_resolve_ops`, the shared first
phase both callers run — #339 item 4 harvest retired the earlier build-free
`&|_| None` resolver on the introspection side). The engine's closure
contract above is unchanged: only the CLI-side caller supplied for
introspection now always threads a real store-backed resolver. The echo
(`aura: note: use "<instance>": <label-or-prefix> -> <full id>`) is the existing
`aura: note:` benign-diagnostic marker (C14) — a new instance of the existing
class, not a new one, so the exit-code/marker taxonomy is unchanged.
@@ -196,8 +218,27 @@ rejected at construction.
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
`<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** — `graph register`, `introspect
--content-id <FILE>`, the bare graph-file viewer, `introspect --params
<FILE>`'s file branch, `introspect --taps <FILE>`'s file branch (#337,
same `composite_from_authored_text` route), and `exec`'s blueprint leg
(#319; its narrower, envelope-only grammar shares the same
`gate_authored_root_name` call directly rather than through the
shape-discriminating wrapper) — 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 — `exec` and the bare
graph-file viewer prepend `aura: <path>:`, while `register` 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 role producing a base column of `kind`.
- `input``{"op":"input","role":<str>}` — declare a root input role (kind inferred
@@ -228,8 +269,8 @@ replayed in order; nodes are referenced **by identifier**, ports as dotted
recorded observation point (a `Composite.taps` entry, C27), not a boundary output.
Name-addressed like every other op (no raw index); tap names are their own
namespace (a duplicate refuses). The `finish` gate threads op-declared taps into
the built `Composite` (`.with_taps`); a single `aura run` records each, a sweep
leaves them inert.
the built `Composite` (`.with_taps`); a single `aura exec` (#319) records
each, a campaign member run leaves them inert.
- `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
@@ -0,0 +1,10 @@
# C25 — The role model: nine authoring roles, cut by artifact + surface + iteration cost: history
> FROZEN HISTORICAL RECORD. Each block below was true as of its cycle/date stamp and may be superseded; this file is NOT current truth and NOT a grounding surface. Current contract: [c25-role-model.md](c25-role-model.md).
**Current-state executor-verb-set sentence (owner-minuted 2026-07-21 on #295,
delivered by #300; superseded by the #319 sugar retirement, 2026-07-25):**
"The executor verb set is settled: `run`, the four thin per-verb generators,
and the document verbs validate/introspect/register/show/run/runs — `show`
being #300's read-back addition. The verbs' per-verb identity is re-ratified
(#300 F8 — reduction, not grammar collapse)."
+10 -6
View File
@@ -74,12 +74,14 @@ present but faceless: no addressed verb families yet. Role homes in the project
layout and docs-by-role are open (#192).
**Document-first completion** is the resolved direction on the control-surface
amendment (owner-minuted 2026-07-21 on #295), delivered by #300. The executor verb
set is settled: `run`, the four thin per-verb generators, and the document verbs
validate/introspect/register/show/run/runs — `show` being #300's read-back
addition. The verbs' per-verb identity is re-ratified (#300 F8 — reduction, not
grammar collapse). A typed-protocol host or MCP face remains demand-driven and
unbuilt.
amendment (owner-minuted 2026-07-21 on #295), delivered by #300. The executor
surface is settled again, one reduction further (#319, 2026-07-25): a single
`exec <target>` verb runs both document classes — a campaign (file or content
id) and a signal blueprint (single run) — over the document verbs
validate/introspect/register/show/runs (`show` #300's read-back addition); the
one argv residue is `--override NODE.PARAM=VALUE`, a per-execution bound-param
reopen threading through both `exec` legs. A typed-protocol host or MCP face
remains demand-driven and unbuilt.
## See also
- [C16](c16-engine-project-split.md) — the game-engine analogy this extends to people
@@ -88,3 +90,5 @@ unbuilt.
- [C20](c20-strategy-harness.md), [C21](c21-world.md) — the node/harness/World tiers behind the 6a/6b split
- [C24](c24-blueprint-data.md) — topology-as-data, the strategy designer's artifact
- [C26](c26-input-binding.md) — a role's input contract carried in blueprint data
> History: [c25-role-model.history.md](c25-role-model.history.md)
@@ -9,3 +9,8 @@ the #310 `--tap` selector):** "…and the shared `bind_tap_plan`/`BoundTaps`
pair called by both declared-tap entry points, `run_signal_r`
(`aura-runner::member`) and `run_measurement` (`aura-runner::measure`); both
CLI verbs pass a record-all plan."
**Current-state "single CLI verb" clause (superseded by the #319 sugar
retirement, 2026-07-25):** "…both arms of the single CLI verb `aura run`,
whose repeatable `--tap TAP=FOLD` selector (#310) makes the `Named`
selection data-reachable…"
+15 -2
View File
@@ -68,8 +68,8 @@ and the roster-enumerating refusal — plus a scalar-typed param schema; all cor
entries are param-less today, the seam ships in every entry's build signature),
and the shared `bind_tap_plan`/`BoundTaps` pair called by both declared-tap entry
points, `run_signal_r` (`aura-runner::member`) and `run_measurement`
(`aura-runner::measure`) — both arms of the single CLI verb `aura run`, whose
repeatable `--tap TAP=FOLD` selector (#310) makes the `Named` selection
(`aura-runner::measure`) — both arms of the single CLI verb `aura exec` (#319),
whose repeatable `--tap TAP=FOLD` selector (#310) makes the `Named` selection
data-reachable: no flag keeps the record-all default, any flag replaces the
plan entirely (unlisted taps stay unbound/inert). The boundary is thereby
fixed in place: *selecting* a subscription is run-mode authority, exercised
@@ -96,6 +96,19 @@ The chain-pruning benefit — a sweep paying zero for the study wires behind an
unbound tap — is **deferred to the future DCE cycle** ([C23](c23-graph-compilation.md));
the mechanism ships now, verified sound.
**#337 (2026-07-26 harvest) ships the positive discovery view**:
`Composite::declared_taps()` (`crates/aura-engine/src/blueprint.rs`) walks the
blueprint depth-first collecting `(tap name, source wire, column kind)`, bare
at every depth, bounds-total over an invalid wire; `graph introspect --taps
<FILE|ID>` renders one row per declared tap (a tap-less blueprint is a
stderr-noted listing, exit 0). This closes the **recovery-only** discovery
gap: before #337, the only way to learn a blueprint's declared tap names was
provoking `bind_tap_plan`'s `UnknownTap` refusal roster (#333) — a real name
surfaced only as the side effect of naming a wrong one first. The refusal
roster itself is unchanged and remains the **recovery** half (what to do once
a tap name is already wrong); `--taps` is the **discovery** half (learning the
right names up front).
## See also
- [C26](c26-input-binding.md) — the input-side twin (`input_roles`); `check_root_roles_bound`, the mandatory-input counterpart
- [C23](c23-graph-compilation.md) — compilation/lowering and the deferred DCE cycle the tap design anticipates
+21 -19
View File
@@ -33,15 +33,15 @@ A strategy's primary, backtestable DAG output: one signed, bounded `f64 ∈ [-1,
### blueprint
**Avoid:** —
The param-generic, input-role-generic graph-as-data produced by running a Rust builder; it carries free numeric params and free input roles before bootstrap. Bootstrapped into a frozen instance by binding params + data + seed. Registered and inspected headless (`aura graph register`, `aura graph introspect --params` — the raw `param_space` namespace campaign axes bind against; cycle 0107/#196); a *bound* param is an overridable **default** — a sweep axis naming it re-opens it per family (#246), while `run` uses it as-is.
The param-generic, input-role-generic graph-as-data produced by running a Rust builder; it carries free numeric params and free input roles before bootstrap. Bootstrapped into a frozen instance by binding params + data + seed. Registered and inspected headless (`aura graph register`, `aura graph introspect --params` — the raw `param_space` namespace campaign axes bind against; cycle 0107/#196); a *bound* param is an overridable **default** — a sweep axis naming it re-opens it per family (#246), while a plain `exec` uses it as-is (#319).
### blueprint label
**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
**Avoid:** —
The distinct, recursive construction phase that binds `(blueprint + param-set + data bindings + seed)` into a frozen instance — buffers sized, topology fixed. The explicit name for the "wiring / graph build" that C7/C12 reference — the construction/compilation sense. Disambiguation: the *statistical* moving-block bootstrap of a trade-R series (`r_bootstrap`, `RBootstrap`) is a different thing that keeps its statistics name — it appears as the deflation null, the `aura mc` R path, and since 0108 the `std::monte_carlo` stage's `stage bootstrap` annotation; context (construction vs annotation) disambiguates.
The distinct, recursive construction phase that binds `(blueprint + param-set + data bindings + seed)` into a frozen instance — buffers sized, topology fixed. The explicit name for the "wiring / graph build" that C7/C12 reference — the construction/compilation sense. Disambiguation: the *statistical* moving-block bootstrap of a trade-R series (`r_bootstrap`, `RBootstrap`) is a different thing that keeps its statistics name — it appears as the deflation null and, since 0108, the `std::monte_carlo` stage's `stage bootstrap` annotation (the campaign executor's one R-bootstrap path, #319); context (construction vs annotation) disambiguates.
### bot
**Avoid:** —
@@ -53,11 +53,11 @@ A downstream consumer node, never part of the strategy: the signal-quality side
### campaign document
**Avoid:** experiment doc, campaign file
The role-6b research artifact (#188/#189): persisted experiment intent as closed-vocabulary canonical JSON — instruments × windows × strategy refs (by `content id` or `identity id`) × per-strategy param axes (each axis declares its `ScalarKind` once over bare values) × a process reference (content-id-only) × data-level presentation (taps to persist — the closed `tap` vocabulary — and tables to emit). Authored, validated, and executed headless (`aura campaign validate|introspect|register|show|run|runs``show` prints a registered document's canonical bytes back, #300), content-addressed beside blueprints in the registry store; carries P1 control constructs (bounded axes, gates, ladders) as intent, executed by `aura campaign run` (v2 pipeline shape `std::sweep [std::gate]* [std::walk_forward]? [std::monte_carlo]? [std::generalize]?` — the two annotators terminal, cycles 0107/0108, #198/#200) into a `campaign run` realization. `std::sweep`'s own selection group (`metric`+`select`) is optional, all-or-nothing, and permitted only as the pipeline's terminal stage when omitted (a selection-free sweep, #210).
The role-6b research artifact (#188/#189): persisted experiment intent as closed-vocabulary canonical JSON — instruments × windows × strategy refs (by `content id` or `identity id`) × per-strategy param axes (each axis declares its `ScalarKind` once over bare values) × a process reference (content-id-only) × data-level presentation (taps to persist — the closed `tap` vocabulary — and tables to emit). Authored headless (`aura campaign validate|introspect|register|runs|show``show` prints a registered document's canonical bytes back, #300), content-addressed beside blueprints in the registry store; carries P1 control constructs (bounded axes, gates, ladders) as intent, executed by `aura exec` (#319; v2 pipeline shape `std::sweep [std::gate]* [std::walk_forward]? [std::monte_carlo]? [std::generalize]?` — the two annotators terminal, cycles 0107/0108, #198/#200) into a `campaign run` realization. `std::sweep`'s own selection group (`metric`+`select`) is optional, all-or-nothing, and permitted only as the pipeline's terminal stage when omitted (a selection-free sweep, #210).
### campaign run
**Avoid:** campaign execution record, realized campaign
One execution of a `campaign document` (`aura campaign run <file|content id>`; a file is register-then-run sugar — the content id is the canonical address). Realized once per (strategy, instrument, window) cell in doc order and recorded as a thin `CampaignRunRecord` line in the registry's `campaign_runs.jsonl` — linking the per-stage family ids, gate survivor ordinals, `stage bootstrap` annotations, and (campaign-scope, per strategy × window) `generalizations` entries (`generalization` with `worst_case`/`sign_agreement`/`per_instrument`, plus `winners` params and `missing` instruments on shortfall) over untouched family records, run-counted per campaign id. A cell whose gate leaves no survivors records its realized prefix and the run exits 0: a null result is a valid research result. Deterministic from doc + stores + data (C1): deflation and bootstrap nulls seed from the doc's `seed`. When the document requests `persist_taps`, the record's sparse `trace_name` (`"{campaign8}-{run}"`) points at the TraceStore family holding each nominee cell's persisted taps (0109/#201). NB the stdout emit wraps each record as `{"campaign_run": …}`; the stored `campaign_runs.jsonl` line is the bare record.
One execution of a `campaign document` (`aura exec <file|content id>`, #319; a file is register-then-run sugar — the content id is the canonical address). Realized once per (strategy, instrument, window) cell in doc order and recorded as a thin `CampaignRunRecord` line in the registry's `campaign_runs.jsonl` — linking the per-stage family ids, gate survivor ordinals, `stage bootstrap` annotations, and (campaign-scope, per strategy × window) `generalizations` entries (`generalization` with `worst_case`/`sign_agreement`/`per_instrument`, plus `winners` params and `missing` instruments on shortfall) over untouched family records, run-counted per campaign id. A cell whose gate leaves no survivors records its realized prefix and the run exits 0: a null result is a valid research result. Deterministic from doc + stores + data (C1): deflation and bootstrap nulls seed from the doc's `seed`. When the document requests `persist_taps`, the record's sparse `trace_name` (`"{campaign8}-{run}"`) points at the TraceStore family holding each nominee cell's persisted taps (0109/#201). NB the stdout emit wraps each record as `{"campaign_run": …}`; the stored `campaign_runs.jsonl` line is the bare record.
### cdylib
**Avoid:** —
@@ -89,7 +89,7 @@ A composable downstream **C9 graph of cost nodes**, in **R**, that **approximate
### cross-instrument generalization
**Avoid:** cross-symbol pooling, pooled generalization
The validation read that grades how consistently one *brought* candidate holds across a set of instruments, scored on its weakest one — the across-instrument axis of the anti-false-discovery discipline. Realised by `aura generalize` and, since 0108 (#200), by the `std::generalize` process stage at campaign scope (per (strategy, window) over the cells' nominees across instruments, recorded in the `campaign run`'s `generalizations`); an aggregator (a recomputable family score), never a selector that picks a winner.
The validation read that grades how consistently one *brought* candidate holds across a set of instruments, scored on its weakest one — the across-instrument axis of the anti-false-discovery discipline. Realised by the `std::generalize` process stage at campaign scope (since 0108, #200; per (strategy, window) over the cells' nominees across instruments, recorded in `CampaignRunRecord.generalizations`, executed via `aura exec`, #319) — the standalone `aura generalize` verb that used to persist its own per-instrument family is retired (#319: `FamilyKind::CrossInstrument` is now dead); an aggregator (a recomputable family score), never a selector that picks a winner.
### cycle
**Avoid:** —
@@ -203,7 +203,7 @@ The recorded chance that a deflated sweep winner is noise rather than edge — a
### plateau selection
**Avoid:** plateau-over-peak (as a noun)
A selection objective that argmaxes the neighbourhood-smoothed metric surface (mean or worst-case) rather than the bare in-sample peak, preferring a robust parameter plateau to a lucky spike. Opt-in via `--select plateau:mean|plateau:worst`; the default selection stays a bare argmax.
A selection objective that argmaxes the neighbourhood-smoothed metric surface (mean or worst-case) rather than the bare in-sample peak, preferring a robust parameter plateau to a lucky spike. Opt-in via a process document's `std::sweep`/`std::walk_forward` stage `"select": "plateau:mean"|"plateau:worst"` field; the default selection stays a bare argmax.
### playground
**Avoid:** —
@@ -245,13 +245,15 @@ A node that converts a finer stream to a coarser bar stream, emitting a complete
**Avoid:** risk section
One entry of a campaign document's structural risk axis (`risk`): a
serializable protective-stop regime (variants `vol{length,k}` — per-cycle —
and `vol_tf{period_minutes,length,k}` — per completed time bucket) the matrix
runs every cell under, so cells differ by execution discipline, never by
signal. Absent or empty = one implicit default regime; the regime's stop
defines the risk unit R — in `vol{length,k}` (stop = k·√EMA(Δ², length) over
m1 cycles) `length` only smooths the vol estimator while `k` scales the stop
distance, so the stop's timescale stays one cycle (`vol_tf` sets the stop's
timescale via `period_minutes`).
`vol_tf{period_minutes,length,k}` — per completed time bucket — and
`fixed{distance}` — a constant price-unit distance, binding the shipped
`FixedStop` composite) the matrix runs every cell under, so cells differ by
execution discipline, never by signal. Absent or empty = one implicit default
regime; the regime's stop defines the risk unit R — in `vol{length,k}` (stop =
k·√EMA(Δ², length) over m1 cycles) `length` only smooths the vol estimator
while `k` scales the stop distance, so the stop's timescale stays one cycle
(`vol_tf` sets the stop's timescale via `period_minutes`; `fixed` has no
timescale — the distance never adapts).
### run
**Avoid:** —
@@ -331,15 +333,15 @@ The harness's structural parameterization — which strategy, instrument(s), bro
### sweep
**Avoid:** param-sweep, parameter sweep
An orchestration axis varying tuning params (grid or random) within a fixed structure. The inner, param-tuning loop, distinct from the structural experiment matrix. On a loaded blueprint every open knob (`--list-axes`) is **required** — a subset is refused with the missing knob named; pin an unwanted knob with a single-value axis (`--axis name=<one-value>`), there is no default. One raw namespace, `<node>.<param>` (a splice path keeps its interior path), is the only axis name (#328): `graph introspect --params` and `aura sweep --list-axes` are line-identical (open params bare, bound params with `default=`), and `--axis` accepts exactly that raw form on both sweep routes — the older `<blueprint>.<node>.<param>` wrapped form (e.g. `graph.fast.length`) is retired from the surface; naming it on `--axis` refuses (`axis names are raw node.param paths — use "fast.length" …`), and quoting it in a campaign document gets a did-you-mean toward the raw candidate. The `aura sweep` CLI verb is now thin sugar over the `campaign document` path — its blueprint form (`<bp.json> --real`) translates to a generated, content-addressed campaign run through the one executor (#210); the built-in `--strategy` sweep surface was retired by #159 (its hard-wired harnesses removed) — sweep now runs from a blueprint + `--axis`, and a retired `--strategy` token falls to the generic usage error. A ganged pair contributes ONE axis.
An orchestration axis varying tuning params (grid or random) within a fixed structure. The inner, param-tuning loop, distinct from the structural experiment matrix. On a loaded blueprint every open knob (`graph introspect --params`) is **required** on a campaign document's `strategies[].axes` — a subset is refused with the missing knob named; pin an unwanted knob with a single-value axis (one value in its `values` list), there is no default. One raw namespace, `<node>.<param>` (a splice path keeps its interior path), is the only axis name (#328): `graph introspect --params` and a campaign axis key are line-identical (open params bare, bound params with `default=`) — the older `<blueprint>.<node>.<param>` wrapped form (e.g. `graph.fast.length`) is retired from the surface; naming it in a campaign document gets a did-you-mean toward the raw candidate, the one remaining intake seam (#319 retired the standalone `--axis`/`--list-axes` CLI flags along with the blueprint-form `aura sweep` verb, whose own `--strategy` built-in surface #159 had already retired). Realised as a campaign document's axes (#210), executed through the one `aura exec` executor (#319). A ganged pair contributes ONE axis.
### tap
**Avoid:** probe, monitor, scope (for the observation slot — "probe" is taken by the sweep-terminal `blueprint_axis_probe` sense; the #77 `Recorder``Probe` rename was retired, 2026-07-21)
A named recorded stream produced by a recording `sink` — the addressable label (e.g. `equity`, `net_r_equity`) under which one sink's per-cycle output is persisted as a columnar (SoA) `ColumnarTrace` and selected for charting via `--tap`. Distinct from the `sink` node that emits it (a tap is the stream, the sink is the role) and from a whole recorded run (a bundle of taps); taps fire at their own cadences and are fused only by joining on the recorded timestamp, never by positional index. In a `campaign document`, `persist_taps` names taps from the CLOSED vocabulary `equity | exposure | r_equity | net_r_equity` (`tap_vocabulary`, 0109/#201) — a new observable is a new vocabulary entry or an authored blueprint sink, never an open node-path namespace. Persisted taps are charted by the printed handle: a campaign run's via the record's `trace_name`, a `sweep`/`walkforward --trace` family's via the family handle the run prints (`aura chart <handle>`; its members are keyed `<cell>/<member>` in the chart) — or, equivalently, by the `--trace <NAME>` the user chose, when `NAME` uniquely resolves against the recorded campaign documents (#238; a name reused across runs refuses rather than guessing).
A named recorded stream produced by a recording `sink` — the addressable label (e.g. `equity`, `net_r_equity`) under which one sink's per-cycle output is persisted as a columnar (SoA) `ColumnarTrace` and selected for charting via `--tap`. Distinct from the `sink` node that emits it (a tap is the stream, the sink is the role) and from a whole recorded run (a bundle of taps); taps fire at their own cadences and are fused only by joining on the recorded timestamp, never by positional index. In a `campaign document`, `persist_taps` names taps from the CLOSED vocabulary `equity | exposure | r_equity | net_r_equity` (`tap_vocabulary`, 0109/#201) — a new observable is a new vocabulary entry or an authored blueprint sink, never an open node-path namespace. Persisted taps are charted by the printed handle: a campaign run's deterministic `trace_name` (`"{campaign8}-{run}"`, never a user-chosen name since #319) charts its whole family (`aura chart <handle>`; members keyed `<cell>/<member>` in the chart) — or, equivalently, a bare campaign id/name that uniquely resolves against the recorded campaign documents (#238; a name reused across runs refuses rather than guessing).
Since C27 (#282) the word also names a second, author-facing sense: a **declared tap** — a `Composite.taps` entry `Tap { name, from: {node, field} }` a hand-authored blueprint declares on an interior output wire, the output-side twin of an `input_role`. It is a pure declaration (no channel endpoint); the harness binds it run-mode-aware (a single `aura run` constructs a recorder at each and persists the series through the trace store; a sweep leaves it unbound and inert). This is an OPEN, per-blueprint author-declared name — distinct from the CLOSED campaign `persist_taps` vocabulary above, which selects among fixed observables of the standard R-harness. Both senses land as `ColumnarTrace`s in the same trace store; the closed vocabulary is what a `campaign document` selects, the declared tap is what a blueprint author names.
Since C27 (#282) the word also names a second, author-facing sense: a **declared tap** — a `Composite.taps` entry `Tap { name, from: {node, field} }` a hand-authored blueprint declares on an interior output wire, the output-side twin of an `input_role`. It is a pure declaration (no channel endpoint); the harness binds it run-mode-aware (a single `aura exec` constructs a recorder at each and persists the series through the trace store; a campaign member run leaves it unbound and inert). This is an OPEN, per-blueprint author-declared name — distinct from the CLOSED campaign `persist_taps` vocabulary above, which selects among fixed observables of the standard R-harness. Both senses land as `ColumnarTrace`s in the same trace store; the closed vocabulary is what a `campaign document` selects, the declared tap is what a blueprint author names.
Since #283 what CONSUMES a declared tap is itself declared per run by a **tap plan**: a subscription is either `Named { label, params }` — resolved against a layered **fold registry** whose core vocabulary is `record | count | sum | mean | min | max | first | last`, each entry carrying a doc line (the help surface and the roster-enumerating refusal) and a scalar-typed param schema (all core entries param-less; growth is a new Rust entry per C25, and higher layers register entries without touching the core) — or `Live(closure)`, the single non-data variant (an in-process consumer with consumer-owned loss policy). `record` streams the full series to the trace store at constant memory (no buffer-then-drain); folds keep an O(1) accumulator and land one summary row at finalize. Both declared-tap entry points are arms of the single verb `aura run` (`aura measure` is the post-hoc IC analysis over already-persisted traces and constructs no tap plan); `aura run` subscribes every declared tap to `record` by default, and its repeatable `--tap TAP=FOLD` selector (#310) replaces that default with an explicit plan — only listed taps are bound, unlisted taps stay unbound/inert. A fold's one summary row is emitted at finalize and stamped with the instant of the last contributing (warm) value — `first` alone pins the first contributing instant, and `min`/`max` deliberately do not carry the extremum's instant (ratified #335).
Since #283 what CONSUMES a declared tap is itself declared per run by a **tap plan**: a subscription is either `Named { label, params }` — resolved against a layered **fold registry** whose core vocabulary is `record | count | sum | mean | min | max | first | last`, each entry carrying a doc line (the help surface and the roster-enumerating refusal) and a scalar-typed param schema (all core entries param-less; growth is a new Rust entry per C25, and higher layers register entries without touching the core) — or `Live(closure)`, the single non-data variant (an in-process consumer with consumer-owned loss policy). `record` streams the full series to the trace store at constant memory (no buffer-then-drain); folds keep an O(1) accumulator and land one summary row at finalize. Both declared-tap entry points are arms of the single verb `aura exec` (#319; `aura measure` is the post-hoc IC analysis over already-persisted traces and constructs no tap plan); `aura exec` subscribes every declared tap to `record` by default, and its repeatable `--tap TAP=FOLD` selector (#310) replaces that default with an explicit plan — only listed taps are bound, unlisted taps stay unbound/inert. A fold's one summary row is emitted at finalize and stamped with the instant of the last contributing (warm) value — `first` alone pins the first contributing instant, and `min`/`max` deliberately do not carry the extremum's instant (ratified #335).
### topology hash
**Avoid:** —
@@ -355,7 +357,7 @@ The **optional** documented pre-trade-gate seam in the execution chain (`stop-ru
### walk-forward
**Avoid:** —
An orchestration axis: rolling in-sample optimize + out-of-sample test across moving windows, stitched into one out-of-sample verdict plus parameter stability. As with `sweep`, every open knob named by `--list-axes` is **required** on a loaded blueprint — a subset is refused with the missing knob named; pin one with a single-value axis, there is no default. The `aura walkforward <blueprint.json> --real <sym> --axis <name>=<csv> …` CLI verb is thin sugar over the `campaign document` path — translated to a generated campaign (`std::sweep → std::walk_forward`) run through the one executor (#210; blueprint-generic over arbitrary blueprints and axes since #220).
An orchestration axis: rolling in-sample optimize + out-of-sample test across moving windows, stitched into one out-of-sample verdict plus parameter stability. As with `sweep`, every open knob a campaign document's axes name is **required** — a subset is refused with the missing knob named; pin one with a single-value axis, there is no default. Realised as a campaign document's `std::sweep → std::walk_forward` process pipeline (#210; blueprint-generic over arbitrary blueprints and axes since #220), executed through the one `aura exec` executor (#319) — the standalone `aura walkforward` CLI verb is retired.
### World
**Avoid:** —
+5 -4
View File
@@ -144,9 +144,10 @@ command sequences.
`ThirdCandleLong` node's `schema` + `eval` in it, against `aura-core`.
3. **Backtest:** an op-script wiring `ger40_lab_nodes::ThirdCandleLong` (§1,
`docs/authoring-guide.md`) is built into `blueprints/third-candle-long.json`
(`aura graph build`); `aura run blueprints/third-candle-long.json --real
GER40 --from 1704067200000 --to 1735689600000` (the window bounds are Unix
milliseconds — here 2024) → the strategy produces a broker-independent, unsized
(`aura graph build`); `aura exec blueprints/third-candle-long.json` (#319)
smoke-runs it over the synthetic stream — a real-data window over `GER40`
between two Unix-millisecond bounds is a one-cell campaign document (step
4) → the strategy produces a broker-independent, unsized
**bias stream** (one signed, bounded `f64 ∈ [-1,+1]` per cycle — sign = direction,
magnitude = optional conviction). A downstream **risk-based executor** (stop-rule →
position-management, in **R**, the protective stop defining 1R) turns the bias into
@@ -165,7 +166,7 @@ command sequences.
Monte-Carlo over seeds, or a structural matrix like "these 10 strategies ×
these 3 instruments × {fixed-stop, vol-stop} risk-executors" — instruments
× windows × strategy × param axes × process, headless-authorable and
registered under `blueprints/`. `aura campaign run <content-id>`
registered under `blueprints/`. `aura exec <content-id>` (#319)
bootstraps the matrix, fans the disjoint sims over all cores (C1), and
writes the comparable runs to `runs/`.
5. **Compose:** "combine it with `momentum-filter` as a weighted sum" → Claude
@@ -0,0 +1,179 @@
# Fieldtest transcript — cycle 319 (sugar retirement, one `exec` verb)
Binary under test: `target/release/aura`, built from this worktree
(`cargo build --release --workspace`, HEAD = `1476990`, tree dirty only by
this corpus). Every command below was run from inside the scaffolded project
`c319lab/` unless the prompt says otherwise. Public-interface sources used:
`README.md`, `docs/authoring-guide.md`, `docs/project-layout.md`,
`docs/design/INDEX.md` + `contracts/c14-headless-two-faces.md`, `aura --help`,
the scaffolded `c319lab/CLAUDE.md`. No implementation source was read.
## Persona
A downstream consumer who last used aura when the research verbs were
`run / sweep / walkforward / mc / generalize`, returning to a fresh project.
## Files
| file | what it is |
|---|---|
| `c319lab/` | project scaffolded with `aura new c319lab` (its `runs/` store is git-ignored by the scaffold's own `.gitignore`) |
| `c319_1_spread_tap.ops.json` | op-script: SMA crossover with a declared `spread` tap (canonical authoring form) |
| `c319_1b_open.ops.json` | same, both SMA lengths left open (free knobs) |
| `c319_2b_zero_bind.ops.json` | same, `fast.length` bound to `0` — the out-of-domain value authored, not overridden |
| `c319_2_targets_*.json` | malformed / wrong-class `exec` targets |
| `c319lab/blueprints/c319_3_process_sweep.json` | process document: `std::sweep` argmax on `sqn` |
| `c319lab/blueprints/c319_3_campaign_sweep.json` | campaign document: GER40, 1 month, 3 axes / 4 points |
| `c319lab/blueprints/c319_4_campaign_override.json` | same with only 2 axes, so `bias.scale` stays a bound default (override target) |
| `c319lab/blueprints/c319_4b_campaign_zerolen.json` | `fast.length` axis = `[0]` (out-of-domain via the campaign leg) |
| `c319lab/blueprints/c319_5_process_wf.json` | process: `std::sweep``std::walk_forward` |
| `c319lab/blueprints/c319_5_campaign_wf_flat.json` | campaign with `fast.length == slow.length` → flat spread → zero trades |
| `c319_*.txt` / `c319_*.err` | verbatim captured runs |
## 1 — Coming back to a retired verb (axis 3)
```
$ aura new c319lab
created project "c319lab" (data-only; attach native nodes later with `aura nodes new`)
$ aura run blueprints/signal.json
error: unrecognized subcommand 'run'
tip: a similar subcommand exists: 'runs'
EXIT=2
```
(full set in `c319_3_retired_verbs.err`: `sweep`, `walkforward`, `mc`,
`generalize` all give the bare clap "unrecognized subcommand", no tip; `run`
gets the `runs` tip.)
Recovery took one `aura --help`: its about-text names `exec` as the one
executor over both document classes and points at `graph introspect --params`
for axes. The scaffolded `c319lab/CLAUDE.md` teaches the same three lines.
Neither the retired-verb refusal nor anything else names `exec` at the point
of failure.
## 2 — Blueprint leg: smoke run, taps, overrides (axes 1, 2, 4)
`c319_1_tap_loop.txt` — build from an op-script, list the fold roster, run
with `--tap spread=record`, run again under a different `--override`.
`c319_2_override_family.txt` — the whole override refusal family on both legs.
Highlights (verbatim):
```
$ aura exec blueprints/signal.json --override fast.length=8
→ manifest.params=[['fast.length', {'I64': 8}]], defaults keep the rest EXIT=0
$ aura exec blueprints/signal.json --override fast.length=8 --override fast.length=9
aura: exec: --override names path "fast.length" twice EXIT=2
$ aura exec blueprints/signal.json --override nosuch.param=3
aura: axis nosuch.param: names no param of this blueprint (open or bound) — see `aura graph introspect --params <bp>` EXIT=1
$ aura exec blueprints/signal.json --override fast.length
aura: exec: --override expects NODE.PARAM=VALUE, got `fast.length` EXIT=2
$ aura exec blueprints/signal.json --override fast.length=abc
aura: exec: --override expects NODE.PARAM=VALUE, got `fast.length=abc` EXIT=2
$ aura exec blueprints/signal.json --override fast.length=1.5
aura: this blueprint does not compile to a runnable harness: ParamKindMismatch { slot: 0, expected: I64, got: F64 } EXIT=1
$ aura exec blueprints/signal.json --override fast.length=0
thread 'main' panicked at crates/aura-std/src/sma.rs:46:9: SMA length must be >= 1 EXIT=101
$ aura exec blueprints/signal.json --override fast.length=-3
thread 'main' panicked at .../alloc/src/raw_vec/mod.rs:28:5: capacity overflow EXIT=101
$ aura exec blueprints/signal.json --override c319lab_signal.fast.length=1
thread 'main' panicked at crates/aura-cli/src/main.rs:1160:38: override set derived from these exact overrides EXIT=101
```
The same out-of-domain value authored into the blueprint (`c319_2b_zero_bind.ops.json`,
`bind {"length":{"I64":0}}`, no `--override` at all) panics identically — so
the containment gap is the single-run leg's, not `--override`'s parser.
Open blueprint, and whether `--override` can close one:
```
$ aura exec blueprints/c319_1b_open.bp.json
aura: run requires a closed blueprint (no free parameters); 2 free knob(s) — bind them in the
blueprint or vary them as campaign axes; a single bound knob can be overridden with --override
EXIT=2
$ aura exec blueprints/c319_1b_open.bp.json --override fast.length=3 --override slow.length=8
(same message) EXIT=2
```
Tap loop: `--tap spread=record` persisted `runs/traces/c319_spread/spread.json`
(11 warm rows) + `index.json` carrying the run manifest; `aura chart c319_spread`
emitted HTML. `--tap spread=median` → `aura: unknown fold 'median' — available:
count, first, last, max, mean, min, record, sum` (exit 1); `--tap spread` →
`aura: --tap expects TAP=FOLD, got "spread"` (exit 2); a tap name the blueprint
does not declare → `aura: the tap plan names 'spread', but the blueprint declares
no such tap — declared taps: ` (exit 1, trailing empty list).
A second `exec` of the same blueprint under a different `--override` wrote into
the same `runs/traces/c319_spread/` and replaced the first run's series; no
note, no second directory.
## 3 — Campaign leg: axis discovery → running sweep family (axes 2, 3)
`c319_3_sweep_family.txt`. Register blueprint + process, author the campaign
document from `docs/authoring-guide.md` §3, validate (three tiers all green),
`aura exec`: 4 members over GER40 2024-09-01…2024-10-01 (16612581 trades each)
plus a `selection_report`, `aura: campaign run N recorded: 1 cells`, exit 0.
Campaign-leg overrides (`c319_2_override_family.txt`, second half): a bound
param not on an axis is accepted (exit 0); a declared axis collides
(`aura: exec: --override \`fast.length\` collides with a declared axis of the
campaign; an override overrides a bound value, never an axis`, exit 2); an
unknown path and a kind mismatch speak the campaign's referential prose (exit 1);
a duplicate path and a non-scalar value speak the shared lexer (exit 2). The
retired wrapped form is refused here with the documented translation pointer:
```
$ aura exec blueprints/c319_4_campaign_override.json --override c319lab_signal.bias.scale=0.25
aura: campaign references do not resolve:
strategy 7987bec1…: axis "c319lab_signal.bias.scale" is not in the param space;
axis names are raw node.param paths — did you mean "bias.scale"? EXIT=1
```
— the same token that panics on the blueprint leg (see §2).
## 4 — Target classification (axis 1)
`c319_2_target_classes.err`, exit codes verified individually:
| target | stderr | exit |
|---|---|---|
| file, not JSON | `aura: exec: <path>: target file is not valid JSON: key must be a string at line 1 column 3` | 2 |
| op-script array (valid JSON) | `aura: <path>: blueprint document is not valid JSON: invalid type: map, expected u32 at line 2 column 2` | 2 |
| `{}` | `aura: <path>: blueprint document is not valid JSON: missing field \`format_version\`` | 2 |
| `"kind":"campain"` typo | `aura: campaign document: the "kind" key must be "campaign"` | 1 |
| a **process** document | `aura: campaign document: the "kind" key must be "campaign"` | 1 |
| missing file / directory / bare name | `aura: '<t>' is neither a readable .json file nor a 64-hex content id` | 1 |
| id prefix `3ca5ca90` | same as above | 1 |
| campaign *name* | same as above | 1 |
| unregistered 64-hex | `aura: no campaign <id> in the project store` | 1 |
| a registered **blueprint's** id | `aura: no campaign <id> in the project store` | 1 |
| registered campaign id | runs | 0 |
## 5 — Walk-forward zero-trade note on the campaign path (axis 4)
`c319_4_walkforward_zero_trade.err`. `fast.length == slow.length == 4` makes the
spread identically zero, so the bias never leaves 0:
```
$ aura exec blueprints/c319_5_campaign_wf_flat.json
aura: note: all 2 walk-forward windows recorded zero trades
aura: campaign run N recorded: 1 cells
EXIT=0
```
Class marker, exit code and "a null result is a valid research result" all as
C14 describes.
## 6 — Per-cell fault containment, for contrast
`c319_4b_campaign_zerolen.json` puts the same out-of-domain `0` on a campaign
axis:
```
aura: warning: cell (7987bec1…, GER40, [1725148800000, 1727740800000]) failed at stage 0:
a member panicked: SMA length must be >= 1 — recorded, campaign continues
aura: campaign run N recorded: 1 cells, 1 failed (GER40: panic)
EXIT=3
```
@@ -0,0 +1,15 @@
[
{"op": "name", "name": "c319_spread"},
{"op": "doc", "text": "fast/slow SMA spread, tapped before the bias clamp"},
{"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,36 @@
$ aura graph build < ../c319_1_spread_tap.ops.json > blueprints/c319_1_spread_tap.bp.json
EXIT=0
$ aura graph introspect --folds
count — number of warm rows (any kind; i64 row); one row at the last warm ts
first — first warm value, at its own timestamp (any kind; kind-preserving row)
last — last warm value, at its own timestamp (any kind; kind-preserving row)
max — maximum of the series (f64 taps; f64 row); one row at the last warm ts
mean — arithmetic mean of the series (f64 taps; f64 row); one row at the last warm ts
min — minimum of the series (f64 taps; f64 row); one row at the last warm ts
record — persist the full series, lossless, at constant memory (any kind)
sum — sum of the series (f64 taps; f64 row); one row at the last warm ts
$ aura exec blueprints/c319_1_spread_tap.bp.json --override fast.length=2 --tap spread=record
EXIT=0
runs/traces/c319_spread/spread.json:
{"tap":"spread","kinds":["F64"],"ts":[8,9,10,11,12,13,14,15,16,17,18],"columns":[[0.002650000000000041,-0.0002249999999999197,-0.0027250000000000885,-0.004237499999999894,-0.004124999999999934,-0.0023
$ aura exec blueprints/c319_1_spread_tap.bp.json --override fast.length=6 --tap spread=record # second override, same blueprint
EXIT=0
runs/traces/ now holds:
c319_spread
runs/traces/c319_spread/spread.json (run A's series is gone):
{"tap":"spread","kinds":["F64"],"ts":[8,9,10,11,12,13,14,15,16,17,18],"columns":[[0.001383333333333292,0.0011750000000001481,0.0006749999999999812,-0.00010416666666679397,-0.001091666666666491,-0.0015
$ aura exec blueprints/c319_1_spread_tap.bp.json --tap spread=median
EXIT=1
aura: unknown fold 'median' — available: count, first, last, max, mean, min, record, sum
$ aura exec blueprints/c319_1_spread_tap.bp.json --tap spread
EXIT=2
aura: --tap expects TAP=FOLD, got "spread"
$ aura exec blueprints/signal.json --tap spread=record # blueprint declares no taps
EXIT=1
aura: the tap plan names 'spread', but the blueprint declares no such tap — declared taps:
@@ -0,0 +1,15 @@
[
{"op": "name", "name": "c319_open"},
{"op": "doc", "text": "fast/slow SMA spread, tapped before the bias clamp"},
{"op": "source", "role": "price", "kind": "F64"},
{"op": "add", "type": "SMA", "name": "fast"},
{"op": "add", "type": "SMA", "name": "slow"},
{"op": "feed", "role": "price", "into": ["fast.series", "slow.series"]},
{"op": "add", "type": "Sub", "name": "sub"},
{"op": "connect", "from": "fast.value", "to": "sub.lhs"},
{"op": "connect", "from": "slow.value", "to": "sub.rhs"},
{"op": "add", "type": "Bias", "name": "bias", "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,69 @@
== blueprint leg (blueprints/signal.json; fast.length:I64=2 slow.length:I64=4 bias.scale:F64=0.5) ==
$ aura exec blueprints/signal.json --override fast.length=8
(stdout run record; manifest.params=[['fast.length', {'I64': 8}]])
EXIT=0
$ aura exec blueprints/signal.json --override fast.length=8 --override bias.scale=0.25
(stdout run record; manifest.params=[['fast.length', {'I64': 8}], ['bias.scale', {'F64': 0.25}]])
EXIT=0
$ aura exec blueprints/signal.json --override fast.length=8 --override fast.length=9
aura: exec: --override names path "fast.length" twice
EXIT=2
$ aura exec blueprints/signal.json --override nosuch.param=3
aura: axis nosuch.param: names no param of this blueprint (open or bound) — see `aura graph introspect --params <bp>`
EXIT=1
$ aura exec blueprints/signal.json --override bias.scal=0.25
aura: axis bias.scal: names no param of this blueprint (open or bound) — see `aura graph introspect --params <bp>`
EXIT=1
$ aura exec blueprints/signal.json --override fast.length
aura: exec: --override expects NODE.PARAM=VALUE, got `fast.length`
EXIT=2
$ aura exec blueprints/signal.json --override fast.length=1.5
aura: this blueprint does not compile to a runnable harness: ParamKindMismatch { slot: 0, expected: I64, got: F64 }
EXIT=1
$ aura exec blueprints/signal.json --override bias.scale=1
aura: this blueprint does not compile to a runnable harness: ParamKindMismatch { slot: 0, expected: F64, got: I64 }
EXIT=1
$ aura exec blueprints/signal.json --override fast.length=abc
aura: exec: --override expects NODE.PARAM=VALUE, got `fast.length=abc`
EXIT=2
== campaign leg (blueprints/c319_4_campaign_override.json; declared axes fast.length, slow.length) ==
$ aura exec blueprints/c319_4_campaign_override.json --override bias.scale=0.25
aura: campaign run 1 recorded: 1 cells
EXIT=0
$ aura exec blueprints/c319_4_campaign_override.json --override fast.length=3
aura: exec: --override `fast.length` collides with a declared axis of the campaign; an override overrides a bound value, never an axis
EXIT=2
$ aura exec blueprints/c319_4_campaign_override.json --override nosuch.param=3
aura: campaign references do not resolve:
strategy 7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911: axis "nosuch.param" is not in the param space
EXIT=1
$ aura exec blueprints/c319_4_campaign_override.json --override bias.scal=0.25
aura: campaign references do not resolve:
strategy 7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911: axis "bias.scal" is not in the param space
EXIT=1
$ aura exec blueprints/c319_4_campaign_override.json --override bias.scale=0.25 --override bias.scale=0.75
aura: exec: --override names path "bias.scale" twice
EXIT=2
$ aura exec blueprints/c319_4_campaign_override.json --override bias.scale=1
aura: campaign references do not resolve:
strategy 7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911: axis "bias.scale" declares a kind that is not the param's kind
EXIT=1
$ aura exec blueprints/c319_4_campaign_override.json --override bias.scale=abc
aura: exec: --override expects NODE.PARAM=VALUE, got `bias.scale=abc`
EXIT=2
@@ -0,0 +1,18 @@
$ aura exec blueprints/signal.json --override fast.length=0
thread 'main' (2726290) panicked at crates/aura-std/src/sma.rs:46:9:
SMA length must be >= 1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
EXIT=101
$ aura exec blueprints/signal.json --override fast.length=-3
thread 'main' (2726295) panicked at /rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/alloc/src/raw_vec/mod.rs:28:5:
capacity overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
EXIT=101
$ aura exec blueprints/c319_4b_campaign_zerolen.json # same value via a campaign axis
aura: warning: cell (7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911, GER40, [1725148800000, 1727740800000]) failed at stage 0: a member panicked: SMA length must be >= 1 — recorded, campaign continues
aura: campaign run 1 recorded: 1 cells, 1 failed (GER40: panic)
EXIT=3
@@ -0,0 +1,44 @@
$ aura exec ../c319_2_targets_notjson.json
aura: exec: ../c319_2_targets_notjson.json: target file is not valid JSON: key must be a string at line 1 column 3
EXIT=2
$ aura exec ../c319_2_targets_opscript.json
aura: ../c319_2_targets_opscript.json: blueprint document is not valid JSON: invalid type: map, expected u32 at line 2 column 2
EXIT=2
$ aura exec ../c319_2_targets_emptyobj.json
aura: ../c319_2_targets_emptyobj.json: blueprint document is not valid JSON: missing field `format_version` at line 1 column 2
EXIT=2
$ aura exec ../c319_2_targets_kindtypo.json
aura: campaign document: the "kind" key must be "campaign"
EXIT=1
$ aura exec blueprints/c319_5_process_wf.json
aura: campaign document: the "kind" key must be "campaign"
EXIT=1
$ aura exec blueprints/nosuch.json
aura: 'blueprints/nosuch.json' is neither a readable .json file nor a 64-hex content id
EXIT=1
$ aura exec blueprints
aura: 'blueprints' is neither a readable .json file nor a 64-hex content id
EXIT=1
$ aura exec 3ca5ca90
aura: '3ca5ca90' is neither a readable .json file nor a 64-hex content id
EXIT=1
$ aura exec c319-ger40-smacross-sweep
aura: 'c319-ger40-smacross-sweep' is neither a readable .json file nor a 64-hex content id
EXIT=1
$ aura exec 7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911
aura: no campaign 7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911 in the project store
EXIT=1
$ aura exec blueprints/c319_1b_open.bp.json
aura: run requires a closed blueprint (no free parameters); 2 free knob(s) — bind them in the blueprint or vary them as campaign axes; a single bound knob can be overridden with --override
EXIT=2
@@ -0,0 +1 @@
{"format_version": 1, "kind": "campain", "name": "typo-kind"}
@@ -0,0 +1 @@
{ this is not json at all,
@@ -0,0 +1,12 @@
[
{"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": "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,11 @@
$ aura exec blueprints/signal.json --override c319lab_signal.bias.scale=0.25
thread 'main' (2726331) panicked at crates/aura-cli/src/main.rs:1160:38:
override set derived from these exact overrides
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
EXIT=101
$ aura exec blueprints/c319_4_campaign_override.json --override c319lab_signal.bias.scale=0.25 # same token, campaign leg
aura: campaign references do not resolve:
strategy 7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911: axis "c319lab_signal.bias.scale" is not in the param space; axis names are raw node.param paths — did you mean "bias.scale"?
EXIT=1
@@ -0,0 +1,15 @@
[
{"op": "name", "name": "c319_zero"},
{"op": "doc", "text": "fast/slow SMA spread, tapped before the bias clamp"},
{"op": "source", "role": "price", "kind": "F64"},
{"op": "add", "type": "SMA", "name": "fast", "bind": {"length": {"I64": 0}}},
{"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,35 @@
$ aura run blueprints/signal.json
error: unrecognized subcommand 'run'
tip: a similar subcommand exists: 'runs'
EXIT=2
$ aura sweep blueprints/signal.json
error: unrecognized subcommand 'sweep'
Usage: aura [OPTIONS] <COMMAND>
EXIT=2
$ aura walkforward blueprints/signal.json
error: unrecognized subcommand 'walkforward'
Usage: aura [OPTIONS] <COMMAND>
EXIT=2
$ aura mc blueprints/signal.json
error: unrecognized subcommand 'mc'
Usage: aura [OPTIONS] <COMMAND>
EXIT=2
$ aura generalize blueprints/signal.json
error: unrecognized subcommand 'generalize'
Usage: aura [OPTIONS] <COMMAND>
EXIT=2
@@ -0,0 +1,24 @@
$ aura graph introspect --params blueprints/signal.json
fast.length:I64 default=2
slow.length:I64 default=4
bias.scale:F64 default=0.5
$ aura graph register blueprints/signal.json
registered blueprint 7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911 (/home/brummel/dev/aura/.claude/worktrees/issue-319-sugar-retirement/fieldtests/cycle-319-sugar-retirement/c319lab/runs/blueprints/7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911.json)
$ aura process register blueprints/c319_3_process_sweep.json
registered process 954e735fc824ac74cceaaa6cbef8f8706a2fdaef3979ffb53cab854f0e3ca43b (/home/brummel/dev/aura/.claude/worktrees/issue-319-sugar-retirement/fieldtests/cycle-319-sugar-retirement/c319lab/runs/processes/954e735fc824ac74cceaaa6cbef8f8706a2fdaef3979ffb53cab854f0e3ca43b.json)
$ aura campaign validate blueprints/c319_3_campaign_sweep.json
campaign document valid (intrinsic): 1 strategy(ies), 3 axes (4 points), 1 instrument(s), 1 window(s), 1 regime(s) (default) — 1 cell(s)
campaign document valid (referential): all references resolve, axes are in the param space
campaign document valid (executable): pipeline shape and static guards pass
$ aura exec blueprints/c319_3_campaign_sweep.json # stderr + member summary
aura: campaign run 2 recorded: 1 cells
member {'fast.length': 2, 'slow.length': 16} n_trades=2581 exp_r=0.0019 sqn=0.0515
member {'fast.length': 2, 'slow.length': 32} n_trades=1869 exp_r=-0.0433 sqn=-0.8164
member {'fast.length': 4, 'slow.length': 16} n_trades=2388 exp_r=-0.0219 sqn=-0.5449
member {'fast.length': 4, 'slow.length': 32} n_trades=1661 exp_r=-0.0132 sqn=-0.2080
selection: std::sweep winner_ordinal=0 [['bias.scale', {'F64': 0.5}], ['fast.length', {'I64': 2}], ['slow.length', {'I64': 16}]]
EXIT=0
@@ -0,0 +1,4 @@
$ aura exec blueprints/c319_5_campaign_wf_flat.json
aura: note: all 2 walk-forward windows recorded zero trades
aura: campaign run 1 recorded: 1 cells
EXIT=0
@@ -0,0 +1 @@
/runs
@@ -0,0 +1,4 @@
# Static project context only (C17); paths only.
[paths]
runs = "runs"
# data = "/path/to/archive" # the recorded-data root; defaults to the built-in path
@@ -0,0 +1,24 @@
# c319lab — an aura research project (data-only)
This directory is an aura project: blueprints + research documents over the
std vocabulary, anchored by `Aura.toml`. There is no crate and no build step.
- Run: `aura exec blueprints/signal.json` (single smoke run, synthetic stream
— the starter is closed; all params bound; bound values are defaults —
`--override NODE.PARAM=VALUE` may override one for this run (#246))
- Campaign: `aura exec <campaign.json>` executes a registered campaign
document (file or content id) — the multi-cell/axis surface
- Axes: `aura graph introspect --params blueprints/signal.json` lists the
open + bound-overridable axes, RAW `<node>.<param>` names (#328)
- Native nodes: when the project needs its first project-specific node,
`aura nodes new <name>` scaffolds a node crate beside this project and
attaches it via `[nodes]` in `Aura.toml` (build it with `cargo build`).
- Topology is data (`blueprints/*.json`); results land in `runs/`.
- Execution model: a strategy emits a bias in [-1,+1] per cycle, held as the
continuously-tracked target position; a protective stop defines the risk
unit R, and quality metrics are R-based. Entry signals become held state
via the signal-side latch/edge-pulse idiom (see
`aura graph introspect --vocabulary`).
- Data plane: process/campaign documents are authored directly with
`aura process` / `aura campaign`, growing one from a bare `{}` via
`aura campaign introspect --unwired`.
@@ -0,0 +1 @@
{"format_version":1,"blueprint":{"name":"c319_spread","doc":"fast/slow SMA spread, tapped before the bias clamp","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 @@
{"format_version":1,"blueprint":{"name":"c319_open","doc":"fast/slow SMA spread, tapped before the bias clamp","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow"}},{"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 @@
{"format_version":1,"blueprint":{"name":"c319_zero","doc":"fast/slow SMA spread, tapped before the bias clamp","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":0}}]}},{"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,22 @@
{
"format_version": 1,
"kind": "campaign",
"name": "c319-ger40-smacross-sweep",
"seed": 42,
"data": {
"instruments": ["GER40"],
"windows": [ { "from_ms": 1725148800000, "to_ms": 1727740800000 } ]
},
"strategies": [
{
"ref": { "content_id": "7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911" },
"axes": {
"fast.length": { "kind": "I64", "values": [2, 4] },
"slow.length": { "kind": "I64", "values": [16, 32] },
"bias.scale": { "kind": "F64", "values": [0.5] }
}
}
],
"process": { "ref": { "content_id": "954e735fc824ac74cceaaa6cbef8f8706a2fdaef3979ffb53cab854f0e3ca43b" } },
"presentation": { "persist_taps": [], "emit": ["family_table", "selection_report"] }
}
@@ -0,0 +1,9 @@
{
"format_version": 1,
"kind": "process",
"name": "c319-sweep-argmax",
"description": "Plain axis sweep, best member by SQN.",
"pipeline": [
{ "block": "std::sweep", "metric": "sqn", "select": "argmax" }
]
}
@@ -0,0 +1,21 @@
{
"format_version": 1,
"kind": "campaign",
"name": "c319-ger40-smacross-sweep-2axes",
"seed": 42,
"data": {
"instruments": ["GER40"],
"windows": [ { "from_ms": 1725148800000, "to_ms": 1727740800000 } ]
},
"strategies": [
{
"ref": { "content_id": "7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911" },
"axes": {
"fast.length": { "kind": "I64", "values": [2, 4] },
"slow.length": { "kind": "I64", "values": [16, 32] }
}
}
],
"process": { "ref": { "content_id": "954e735fc824ac74cceaaa6cbef8f8706a2fdaef3979ffb53cab854f0e3ca43b" } },
"presentation": { "persist_taps": [], "emit": ["family_table", "selection_report"] }
}
@@ -0,0 +1,21 @@
{
"format_version": 1,
"kind": "campaign",
"name": "c319-zero-length-axis",
"seed": 42,
"data": {
"instruments": ["GER40"],
"windows": [ { "from_ms": 1725148800000, "to_ms": 1727740800000 } ]
},
"strategies": [
{
"ref": { "content_id": "7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911" },
"axes": {
"fast.length": { "kind": "I64", "values": [0] },
"slow.length": { "kind": "I64", "values": [16, 32] }
}
}
],
"process": { "ref": { "content_id": "954e735fc824ac74cceaaa6cbef8f8706a2fdaef3979ffb53cab854f0e3ca43b" } },
"presentation": { "persist_taps": [], "emit": ["family_table", "selection_report"] }
}
@@ -0,0 +1,21 @@
{
"format_version": 1,
"kind": "campaign",
"name": "c319-ger40-flat-walkforward",
"seed": 42,
"data": {
"instruments": ["GER40"],
"windows": [ { "from_ms": 1725148800000, "to_ms": 1727740800000 } ]
},
"strategies": [
{
"ref": { "content_id": "7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911" },
"axes": {
"fast.length": { "kind": "I64", "values": [4] },
"slow.length": { "kind": "I64", "values": [4] }
}
}
],
"process": { "ref": { "content_id": "a7220282128b56f53b75cf54c586a2ade11fd6a81d1638aeb3fb844fd0f51e3f" } },
"presentation": { "persist_taps": [], "emit": ["family_table", "selection_report"] }
}
@@ -0,0 +1,10 @@
{
"format_version": 1,
"kind": "process",
"name": "c319-sweep-then-walkforward",
"description": "Sweep for a nominee, then walk it forward out of sample.",
"pipeline": [
{ "block": "std::sweep", "metric": "sqn", "select": "argmax" },
{ "block": "std::walk_forward", "in_sample_ms": 1209600000, "out_of_sample_ms": 604800000, "step_ms": 604800000, "mode": "rolling", "metric": "sqn", "select": "argmax" }
]
}
@@ -0,0 +1 @@
{"format_version":1,"blueprint":{"name":"c319lab_signal","doc":"fast/slow SMA difference clamped into a directional bias","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
@@ -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}}]}}

Some files were not shown because too many files have changed in this diff Show More