Compare commits

...

15 Commits

Author SHA1 Message Date
claude 05e9a00afc fieldtest: harvest-sweep — 3 examples, 1 bug / 1 friction / 6 working
Source-blind against the release binary at 120d116, one example per
axis: hs_1_discovery (help -> --folds roster -> --tap spread=mean
without opening source or JSON; fold row at the last warm ts as
documented), hs_2_typo_key (three closed-set refusals — typo'd op key,
undeclared tap, unknown fold — each naming token + valid set, exit 1),
hs_3_three_taps (skipped-tap note fires exactly per skipped tap under
an explicit plan, silent under record-all and bind-all).

Findings routed: the op-key refusal cites the wrong line (points at
the next element; recovery still carried by the key name) -> #336;
declared-tap names have no positive discovery surface (the deferred
#333 view half, evidence now in hand) -> #337. All seven sweep
closures verified working from the outside.

refs #336, refs #337
2026-07-24 16:46:38 +02:00
claude 120d116982 audit: harvest-sweep cycle close — C29 forward-pointers lifted to shipped state
Architect drift review (range 7cc3ce0..HEAD): holds confirmed — C27
unbound-inert intact (the #334 note is C14's promised benign class,
structurally dead under record-all); #326's exit-1 matches C14's
class-1 wording and the pinned #175 attribution family; C29 discipline
moved with the FoldSchema retirement (registry roster doc-gated +
prose pinned against the executable rules); commit bodies substantiate
against the diff.

Drift resolved: [medium] C29's closing forward-pointers still described
#315 rendering as pending, #267 as upcoming, and a fold introspection
surface as blocked on #310 — all shipped/retired; updated to the shipped
state including the #332 registry-roster form. [low] the runner-side
eprintln register family grew by the note and the roster-carrying
refusal — tracked forward on #297 (scope comment there), consistent
with the C14 tension already routed in the #310 audit.

Regression check (aura-bench, report-only): all five fingerprints OK.
engine_throughput 14002097 bars/s (-0.0%), ingest 12982397 (-0.1%),
campaign_sweep wall 1.402s (+0.5%), campaign_heavy wall 5.588s (+0.3%,
peak_rss +7.7%), cli_fixed_cost help 1.7ms (+11.4% NOTICE) — the NOTICE
and rss delta measured at loadavg 8.9 with a parallel build running;
sub-2ms jitter, not ratified as a real move (no baseline change;
harness is report-only).

Erratum to 8dbca82's body: the blast-radius corpus is 24 committed
*.ops.json files, not 21 (all verified building; the count was
understated).

refs #297
2026-07-24 16:37:50 +02:00
claude 938397295d refactor(aura-std, aura-runner): retire the orphaned FoldSchema table
Review finding on the #332 fix: rendering --folds from the fold-registry
roster removed fold_vocabulary()/FoldSchema's last production consumer,
leaving a second, unsurfaced source of fold docs whose wording had
already diverged from the registry. The table and its two tests are
gone; the discipline they carried moves onto the surviving surface: the
registry tests now doc-gate every roster entry (C29 entry seam) and pin
the bind/output prose inside each doc line against the entry's
executable binds_at/output rules, so the roster cannot drift from what
the wiring enforces.

refs #332
2026-07-24 16:32:59 +02:00
claude 98342246f6 docs(ledger, glossary): ratify the fold summary-row timestamp semantics
Fieldtest spec_gap (#335): the timestamp a fold's one summary row
carries was undocumented. Ratified as-is (derived decision, minuted on
the issue): the row is emitted at finalize and stamped with the instant
of the last contributing (warm) value (TapFold::last_ts) — first alone
pins the first contributing instant, and min/max deliberately do not
carry the extremum's instant (a whole-window row privileges no interior
instant, and the extremum ts would cost extra state for no consumer).

The issue's initial finalize-ts reading (and the first draft of this
ratification) was refuted in review against tap_fold.rs: a tap that
goes cold before run end stamps the last WARM eval's instant, not the
run-end instant — observationally identical only while the tap stays
warm to the end, which is what the fieldtest fixtures did. Correction
minuted on the issue.

Recorded in C27's Current state and the glossary tap-plan paragraph;
the fold-registry roster doc lines state it on the surface itself
(sibling commit).

closes #335
2026-07-24 16:29:50 +02:00
claude fa7453dd9f feat(aura-runner): skipped-tap note; undeclared-tap refusal enumerates declared taps
Two fold-selector fieldtest findings on the --tap seam, RED-first.

Skipped-tap note (#334): an explicit --tap plan replaces the record-all
default entirely (C27: unbound is inert) but gave no runtime signal that
unlisted declared taps went unbound — a forgotten tap surfaced only as a
missing trace file. bind_tap_plan's unbound arm now emits the C14 benign
note (aura: note: declared tap "<name>" unbound this run) per skipped
tap, exit unaffected. No new carrier: the arm is only reachable when
plan.default_named is None (an explicit plan) — under record-all it is
structurally dead, so the default emits nothing. Emitted runner-side
beside the existing eprintln registers; the runner->CLI print migration
is #297.

Undeclared-tap refusal (#333): --tap on a tap the blueprint does not
declare was refused without naming the valid taps, forcing the author
back into the blueprint JSON. TapPlanError::UnknownTap now carries the
declared-tap roster and Display appends it, mirroring UnknownLabel's
fold-roster idiom (C29: a refusal names the closed set it checks
against). Decision minuted on #333 (refusal roster over a new introspect
view).

Rider (#335): the fold roster doc lines now state the summary-row
timestamp — the accumulator folds (count/sum/mean/min/max) land one row
at the last warm (contributing) value's ts, and last gains the same
at-its-own-timestamp wording first already carried. The ratification
record lands in the sibling ledger/glossary commit.

closes #334
closes #333
2026-07-24 16:29:50 +02:00
claude 8dbca82756 fix(aura-cli): --folds renders the registry roster; op-lists refuse unknown keys
Two fold-selector/op-script surface-honesty bugs from the 2026-07-24
fieldtests, both RED-first.

--folds (#332): graph introspect --folds rendered the aura-std FoldKind
table — capitalized ids (Mean) and no record entry — while aura run
--tap resolves labels against the fold-registry roster (lowercase,
record included). The discovery surface the --tap help points at thus
misdescribed the accepted input twice. It now renders
FoldRegistry::core().roster() (label + doc per entry); the pinned
FoldKind-rendering test was rewritten to pin the roster form (8 rows,
no capitalized id leaks, record present).

Unknown op-list keys (#326): a typo'd key in an op-list element
("params" for "bind") was silently dropped and the wrong graph built
with zero signal, contradicting the closed-vocabulary posture (C25).
OpDoc now carries serde deny_unknown_fields; the refusal names the
offending key. Exit class is 1, not the issue's suggested 2: op-lists
arrive on stdin, and the pinned #175 attribution principle classes
stdin-content faults as runtime — the new refusal fires from that same
deserialize. Blast radius verified: all 21 committed *.ops.json under
fieldtests/ still build cleanly.

closes #332
closes #326
2026-07-24 16:12:12 +02:00
claude 73ad87b08a fix(aura-research): process.ref hint states the tagged { content_id } shape
campaign introspect (--block std::process_ref and --unwired) described
the slot as "content id of a process document" — a plausible bare-string
reading the validator rejects; the required {"content_id":"..."} tagging
appeared in no introspection output (binary-only M1 fieldtest finding).

The SlotKind::ContentRef label and the open-slot hint now lead with the
tagged { content_id } shape. Deliberately narrower than the issue's
suggestion of mirroring std::strategy's { content_id } | { identity_id }
form: validate_campaign refuses an identity_id process ref
(DocFault::ProcessRefMustBeContentId) and a pinned test forbids
advertising it, so the hint states the one accepted key only (C29
honesty; correction minuted on the issue). RED-first: the extended
process_ref_describe_advertises_content_id_only assertion failed against
the old label before the fix.

closes #329
2026-07-24 16:02:45 +02:00
claude 7943b123ae fix(aura-strategy): charge-basis-specific cost-node one-liners
The three cost nodes (ConstantCost, CarryCost, VolSlippageCost) shared a
byte-identical generic doc line, so `aura graph introspect --vocabulary`
could not tell them apart — C29's per-entry meaning was formally met but
not entry-specific (binary-only M1 fieldtest finding).

cost_node_builder now takes the factor-specific one-line doc as a
parameter (the shared string is gone from the single schema home); each
builder states its own charge basis. RED-first:
cost_node_descriptions_are_pairwise_distinct failed against the shared
string before the fix.

closes #330
2026-07-24 16:02:45 +02:00
claude 7cc3ce0d9e fieldtest: fold-selector — 2 examples, 0 bugs / 5 friction / 1 spec-gap
Per-cycle fieldtest of the #310 --tap selector, source-blind against the
release binary (e482f0e). Spine holds: fold selection end-to-end
byte-exact incl. the C1 pin and kind-aware folds/refusals on a Bool tap;
parse refusals precise at exit 2. Findings routed to the tracker:
introspect --folds renders the aura-std FoldKind table instead of the
registry roster (capitalized ids, record missing); no public surface
enumerates a blueprint's declared taps; an explicit --tap plan drops
unlisted taps without the C14 'aura: note:' skipped-tap notice; fold
summary-row timestamp semantics undocumented and mixed. Exit-class
tension already tracked on #297 (audit).

refs #310
2026-07-24 15:22:24 +02:00
claude e482f0ec35 audit: #310 cycle close — run usage names --tap; C14 class tension routed to #297
Architect (cycle 9124275..1baee77) holds: the C27 boundary is preserved
and ledger-anchored (selection = run-mode authority, fold growth stays
Rust/role 2; validation solely in the shared bind_tap_plan seam before
store I/O; unlisted taps inert); C1 byte-identity pinned for the
explicit all-record plan vs the record-all default; ledger discipline
kept (superseded C27 sentence verbatim in the new history sidecar); the
C25 projection tension was explicitly adjudicated in C27's Current
state, deferral #312/#327 minuted.

Drift resolution:
- [medium] C14 exit-class: --tap-content refusals (unknown label /
  undeclared tap) exit 1 through the assembly crate's runner-side
  registers while C14 assigns argv/blueprint-content faults to class 2.
  Pre-existing register family (#283 duplicate-tap, #286 exposes-
  neither). RESOLVED AS TRACKED-FORWARD to #297 (RunnerError
  propagation is where the CLI regains class authority; evidence
  comment with concrete sites posted there). A CLI-side shadow
  pre-validation was rejected: it duplicates the bind_tap_plan seam
  and can drift.
- [low] FIXED here: the three hand-rolled `aura run` usage strings and
  the loaded-blueprint-grammar comment now name --tap.
- [low] ADJUDICATED equivalent-as-delivered: the fold-mean expectation
  is derived from the pinned R_SMA_PRICES const rather than a second
  record run — an independent derivation of the same series; the
  spec's intent (fold row equals the series mean) is pinned.

Regression: aura-bench report-only, exit 0, all five surfaces within
tolerance, fingerprints OK (engine 13.78M bars/s Δ-1.6%, ingest 13.13M
Δ+1.0%, campaign_sweep rss 93.9MB Δ+1.9%, campaign_heavy rss 106.9MB
Δ+5.4%, cli run 3.6ms Δ-0.3%; loadavg-5.3 warning noted, report-only).
No baseline update.

Cycle spec + plan + the spent m1 fieldtest spec removed from the
working tree (git-ignored files, shell rm per lifecycle).

refs #310, refs #297
2026-07-24 15:11:27 +02:00
claude 1baee774bb feat(aura-cli): --tap TAP=FOLD fold-subscription selector on aura run
Decide #310's data-authorability fork as (b), split by run-mode
authority: selecting a Named fold subscription is data-reachable on the
one-shot run path; adding a fold stays a Rust entry (role 2, C25).
`aura run` gains a repeatable --tap TAP=FOLD selector feeding the
existing TapPlan seam: no flag keeps the record-all default; any flag
replaces the plan entirely (unlisted taps stay unbound — C27 inert,
non-error). Validation stays in bind_tap_plan (roster-enumerating label
refusal, typed UndeclaredTap, both before store I/O); parse-level
refusals (malformed pair, duplicate tap) exit 2 via the verb's usage
convention.

Rejected alternatives (minuted on #310 with the skeptic-pass
rationale): an op-script/blueprint carrier (C27 forbids recording
policy in the serialized fragment); ratifying Rust-only (softens a
designed-in promise with effort as the only rationale). The
campaign/document carrier and the persist_taps/declared-tap namespace
reconciliation are deferred to the Measurement-reachable milestone
(#312/#327, minuted on #312).

Ledger: C27 Current state records the boundary; the superseded sentence
moved verbatim to the new c27-declared-taps.history.md sidecar; the
glossary tap-plan paragraph updated, fixing the pre-existing inaccuracy
that named `aura measure` a record-all-passing verb (it is the post-hoc
IC analysis and constructs no tap plan).

Verification: 5 new unit tests (parse branches), 5 new binary-level
selector tests incl. a byte-identity pin of an explicit all-record plan
vs the record-all default (C1), 1 measurement-arm test; full workspace
suite green; clippy -D warnings clean. Independent opus diff review:
approved, no Important/Minor findings (nits: inert-arm generic refusal,
a=b=c falling to the roster refusal — accepted as-is).

closes #310
2026-07-24 15:03:39 +02:00
claude 9124275bf3 fieldtest: m1-self-description — 5 examples, 1 bug / 3 friction / 3 spec-gap
Binary-only milestone fieldtest for milestone 36 (self-description): a
fieldtester agent bootstrapped from the release binary (d26f0c8) alone —
no engine sources, no repo docs, no bootstrap card — and drove five
end-to-end scenarios: cold bootstrap -> author -> validate (green,
synthetic + real EURUSD, 3944 trades), execution semantics +
latch/edge-pulse idiom (green), vocabulary breadth (33 nodes / 7 folds /
17 metrics / 6+6 blocks, every entry with a meaning), document ramp from
a bare {} (green, 3 members), trace/measurement path (dead-ends,
tracked).

Verdict: the bootstrap card (#267, closed as superseded) is empirically
redundant for the bootstrap->author->validate spine. Two seams keep the
promise short of end-to-end, both tracked outside M1: the declared-tap
drop on the registration path (#327, the missing mechanism of #312/M6)
and the sugar<->document axis namespace (#328, prerequisite of the #319
retirement). Friction routed: #329 #330 #331; evidence comments on #312
and #324. Milestone 36 closed with this adjudication; the fieldtest spec
remains the usual git-ignored working file.

refs #267, refs #312, refs #319
2026-07-24 11:06:22 +02:00
claude d26f0c84a4 fix(cli): qualify the desugaring claims to the --real path; loud vocabulary-resolve invariant
Independent post-cycle review (opus, fresh context) on the branch diff
returned one Minor and one Nit; both adjudicated against the source and
fixed:

- Minor: the concepts paragraph and the sweep/walkforward/mc long helps
  claimed the document desugaring unconditionally, but only the --real
  branches route through verb_sugar — the synthetic branches execute
  in-process and register no documents. The claims now carry the --real
  qualifier (and the paragraph names the in-process synthetic path).
  generalize keeps the unconditional wording: its only mode is --real
  (usage requires it), so the claim is true as stated.
- Nit: the vocabulary listing's `None` fallback would have silently printed
  a bare, C29-incomplete row if a rostered type id ever failed to resolve.
  Replaced with a loud expect — an invariant breach should not degrade into
  exactly the surface this cycle exists to remove.

refs #315
2026-07-24 10:19:50 +02:00
claude 162bf849ce fix(cli): the concepts paragraph no longer claims run writes documents
The shipped wording lifted #319's loose "quintet is pure translation"
phrasing into "the research verbs (run, sweep, ...) are sugar — each writes
registered process/campaign documents". Source check (dispatch_run): only
the four verb_sugar bridges (sweep, walkforward, mc, generalize) generate
and register documents; `run` is a direct single backtest, exactly why its
own long help already avoided the desugaring claim. The paragraph now names
the four document-bridged verbs and places run as their single-backtest
sibling — consistent with `run --help`.

Found by re-auditing the shipped prose claims against the source after the
owner asked how I know no error was made (skills-less-cycle experiment,
refs #315): prose claims in help text are exactly the class the green test
suite cannot semantically verify.
2026-07-24 10:10:12 +02:00
claude 829a1984e6 feat(cli, std, research): self-describing surfaces — two-layer help, per-entry meanings, op reference
The binary is the only always-present teacher (C29); the 2026-07-22 external
field test showed the help teaching nothing but the verb inventory. This
cycle makes every closed vocabulary the binary ships listable with a one-line
meaning, and opens the help with the concepts the field agent had to discover
forensically.

#315 — help + introspection:
- `aura --help` opens with the two-layer concepts paragraph (research verbs
  as sugar over registered process/campaign documents; the directly
  authorable data plane; bias-as-target-position + protective-stop execution
  model in R; how traces come to exist).
- Each document-bridged sugar verb's long help names the process shape it
  desugars to (sweep -> std::sweep; walkforward -> [std::grid,
  std::walk_forward]; mc -> + std::monte_carlo; generalize -> [std::sweep
  (selection), std::generalize]). `run` is not verb_sugar-bridged, so its
  help points at the canonical document-first form instead of claiming a
  desugaring it does not perform.
- `graph introspect --vocabulary` lists each node type with its schema doc
  (bare names taught nothing); `--node <T>`'s head line carries the meaning.
- `graph introspect --folds` (new): the tap-fold vocabulary with bind rule,
  output kind, and meaning — it existed only as source comments. The rows
  live in aura-std (`fold_vocabulary`), unit-pinned against
  `fold_binds_at`/`fold_output_kind` so the prose cannot drift from the
  executable rules, and doc_gate-checked (C29 entry seam). The discovery
  surface is graph introspect because folds bind at graph-declared taps
  (C27); the document layer still refuses a folds slot pending #310.
- `process introspect --metrics` lines carry each metric's meaning (the doc
  column existed since #316 but was never printed).
- `campaign introspect --block std::presentation` lists the four persisted
  taps' meanings under the persist_taps slot.
- The scaffolded project CLAUDE.md teaches the execution semantics (bias as
  held target, protective stop, R) and the document data plane.

#323 — C29 authoring surfaces:
- `introspect --unwired` enumerates the optional `description` envelope slot
  for both document kinds (absent-only, like risk/cost); the campaign lib
  fixture gains a description, and the risk/cost-bound e2e binds description
  too so "no open slots" keeps meaning complete.
- `graph build --help` carries the op-list reference: all nine op kinds with
  fields and a worked element each (OP_REFERENCE lives beside OpDoc so a new
  variant is one screen from its help line).
- The authoring guide's S0 worked literal is byte-pinned to the scaffold's
  LIB_RS template (rendered with the guide's my_lab namespace); the scaffold
  e2es compile that template for real, so the pin is a transitive compile
  guard. The template gains the guide's C29 comment, the guide gains the
  template's rename note — one worked example, two surfaces, zero drift.

Deliberately out of scope (ratified #319, 2026-07-24): no doc-strings for
the quintet's 48 flag fields — the sugar surface retires; only the pointer
lines above bridge toward the document layer.

Verification: full workspace suite green, clippy -D warnings clean,
aura-bench exit 0 with all fingerprints OK (help_ms +1.6% — the longer help
text, in tolerance), cargo doc warning count unchanged (7 pre-existing).
9 new e2e pins in tests/help_self_description.rs; guide + CLAUDE.md pins in
scaffold unit tests; fold-vocabulary rules + doc_gate unit-pinned in
aura-std.

closes #315
closes #323
2026-07-24 10:02:38 +02:00
44 changed files with 1293 additions and 54 deletions
+47 -4
View File
@@ -18,12 +18,37 @@ use serde::Deserialize;
#[cfg(test)]
use aura_vocabulary::std_vocabulary;
/// The op-list reference `aura graph build --help` appends (#323): the nine
/// op kinds with their fields and one worked element each. Lives beside
/// [`OpDoc`] so a new op variant is one screen away from its help line.
pub const OP_REFERENCE: &str = r#"Op-list reference (stdin: a JSON array of op objects, applied in order):
{"op":"source","role":"price","kind":"F64"}
declare a bound root input role of a scalar kind
{"op":"input","role":"price"}
declare an open input role (wired by an enclosing graph)
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":2}}}
instantiate a node ("name" optional; "bind" maps param -> typed scalar)
{"op":"feed","role":"price","into":["fast.series","slow.series"]}
wire a role into one or more input slots
{"op":"connect","from":"fast.value","to":"sub.lhs"}
wire a node output into an input slot
{"op":"expose","from":"sub.value","as":"bias"}
name a graph output field
{"op":"tap","from":"sub.value","as":"spread"}
declare a recordable tap on a wire (expose's output-side twin)
{"op":"gang","as":"length","into":["fast.length","slow.length"]}
fuse two or more sibling params into one public knob
{"op":"doc","text":"..."}
declare the composite's one-line meaning (C29)
Node types and their ports: aura graph introspect --vocabulary | --node <T>"#;
/// The wire DTO for one construction op — the document's by-identifier shape,
/// internally tagged by `"op"`, mapped into the serde-free engine `Op`. Bind
/// values are the typed `Scalar` form (`{"I64":2}`); `kind` the capitalized
/// `ScalarKind` form (`"F64"`) — both the #155 representations.
#[derive(Debug, Deserialize)]
#[serde(tag = "op", rename_all = "lowercase")]
#[serde(tag = "op", rename_all = "lowercase", deny_unknown_fields)]
enum OpDoc {
Source { role: String, kind: ScalarKind },
Input { role: String },
@@ -201,7 +226,8 @@ pub fn build_cmd(env: &aura_runner::project::Env) {
pub fn introspect_node(type_id: &str, env: &aura_runner::project::Env) -> Result<String, String> {
let builder = env.resolve(type_id).ok_or_else(|| format!("unknown node type {type_id:?}"))?;
let schema = builder.schema();
let mut out = format!("{}\n", builder.label());
// C29 (#315): the head line carries the node's one-line meaning.
let mut out = format!("{}{}\n", builder.label(), schema.doc);
for port in &schema.inputs {
out.push_str(&format!(" in {}:{:?}\n", port.name, port.kind));
}
@@ -239,17 +265,34 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd, env: &aura_runner::project
let count = cmd.vocabulary as usize
+ cmd.node.is_some() as usize
+ cmd.unwired as usize
+ cmd.folds as usize
+ cmd.params.is_some() as usize
+ (cmd.content_id.is_some() || cmd.identity_id) as usize;
if count != 1 {
eprintln!(
"aura: Usage: aura graph introspect --vocabulary | --node <T> | --unwired | --params <FILE|ID> | --content-id [FILE] | --identity-id (the two id flags may be combined)"
"aura: Usage: aura graph introspect --vocabulary | --node <T> | --unwired | --folds | --params <FILE|ID> | --content-id [FILE] | --identity-id (the two id flags may be combined)"
);
std::process::exit(2);
}
if cmd.vocabulary {
// C29 (#315): each type id carries its schema's one-line meaning —
// bare names teach nothing (What do Latch, When, Select, Bias do?).
// A rostered id that fails to resolve is an invariant breach; better
// loud than a silent C29-incomplete bare row.
for t in env.type_ids() {
println!("{t}");
let b = env.resolve(t).expect("every rostered type id resolves to a builder");
println!("{t:<18} {}", b.schema().doc);
}
} else if cmd.folds {
// The fold vocabulary binds at graph-declared taps (C27), so the
// graph introspect namespace is its discovery surface (#315). #332:
// this renders the fold-REGISTRY roster — the same roster `aura run
// --tap TAP=FOLD` resolves labels against (aura-runner's layered
// `FoldRegistry`) — not the aura-std `FoldKind` table: labels here
// must be exactly what `--tap` accepts (lowercase), including the
// `record` entry that has no `FoldKind` counterpart.
for (label, doc) in aura_runner::FoldRegistry::core().roster() {
println!("{label:<7}{doc}");
}
} else if let Some(type_id) = cmd.node.as_deref() {
match introspect_node(type_id, env) {
+131 -8
View File
@@ -46,7 +46,7 @@ use aura_measurement::information_coefficient;
// (a sibling module reaching it via `crate::blueprint_axis_probe_reopened`,
// the crate-root re-export this `use` gives it), not from this module itself.
use aura_runner::member::{blueprint_axis_probe, blueprint_axis_probe_reopened, run_signal_r, wrapped_bound_names, RunData};
use aura_runner::TapPlan;
use aura_runner::{TapPlan, TapSubscription};
#[cfg(test)]
use aura_runner::member::{run_blueprint_member, wrap_r, SYNTHETIC_PIP_SIZE};
// The family builders (blueprint sweep / walk-forward / MC), the shared
@@ -105,6 +105,7 @@ use aura_std::Sma;
#[cfg(test)]
use std::sync::mpsc;
use std::collections::HashSet;
use std::collections::BTreeSet;
#[cfg(test)]
use std::collections::BTreeMap;
use clap::{Args, Parser, Subcommand};
@@ -1026,11 +1027,32 @@ fn version_string() -> &'static str {
/// The `aura` root parser. `version` is built from `CARGO_PKG_VERSION` (the
/// workspace `0.1.0`) plus the parenthesized `ENGINE_COMMIT`, so
/// `aura --version` prints `aura 0.1.0 (<commit>)`.
/// The two-layer concepts paragraph `aura --help` opens with (#315): the
/// zero-setup self-description a reader without repo access gets.
const CONCEPTS_HELP: &str = "\
Author, backtest, and validate trading strategies — research CLI.
Two layers, one vocabulary: the research verbs are the convenience surface —
over --real data, sweep, walkforward, mc, and generalize desugar to
registered process/campaign documents and execute them (run is their
single-backtest sibling; synthetic runs execute in-process). That document
data plane is directly authorable: `aura process` / `aura campaign`
(validate | introspect | register | run), growing a document from a bare {}
via `introspect --unwired`.
Execution model: a strategy emits a bias in [-1,+1] per cycle, held as the
continuously-tracked target position; a protective stop defines the risk unit
R, and signal quality is measured in R.
Traces: `sweep --real --trace` / `walkforward --real --trace` record per-cycle
taps under runs/, consumed by `aura chart` and `aura measure`.";
#[derive(Parser)]
#[command(
name = "aura",
version = version_string(),
about = "Author, backtest, and validate trading strategies — research CLI",
long_about = CONCEPTS_HELP,
infer_long_args = true
)]
struct Cli {
@@ -1044,22 +1066,43 @@ struct Cli {
#[derive(Subcommand)]
enum Command {
/// Run a single backtest (built-in harness or a loaded blueprint).
///
/// Part of the research sugar surface; the canonical, document-first form
/// of any research run is a registered process + campaign pair — see
/// `aura process` / `aura campaign`.
Run(RunCmd),
/// Render a recorded run's trace to an HTML chart.
Chart(ChartCmd),
/// Emit / construct / introspect a graph.
Graph(GraphCmd),
/// Sweep a parameter grid over a strategy or a loaded blueprint.
///
/// Sugar over the document layer: over --real data this desugars to a
/// registered process (std::sweep) + campaign pair — author the documents
/// directly via `aura process` / `aura campaign`.
Sweep(SweepCmd),
/// Walk-forward validation over a strategy or a loaded blueprint. Over --real, the
/// fixed 90/30-day roller fits to a --from/--to window shorter than it, preserving
/// the 3:1 IS:OOS ratio, instead of refusing the window outright.
///
/// Sugar over the document layer: over --real data this desugars to a
/// registered process ([std::grid, std::walk_forward]) + campaign pair —
/// author the documents directly via `aura process` / `aura campaign`.
Walkforward(WalkforwardCmd),
/// Grade one candidate across multiple instruments.
///
/// Sugar over the document layer: desugars to a registered process
/// ([std::sweep (selection), std::generalize]) + campaign pair — author
/// the documents directly via `aura process` / `aura campaign`.
Generalize(GeneralizeCmd),
/// Monte-Carlo over synthetic draws, an R-bootstrap, or a loaded blueprint. Over
/// --real, the fixed 90/30-day walk-forward roller fits to a --from/--to window
/// shorter than it, preserving the 3:1 IS:OOS ratio, instead of refusing outright.
///
/// Sugar over the document layer: over --real data this desugars to a
/// registered process ([std::grid, std::walk_forward, std::monte_carlo]) +
/// campaign pair — author the documents directly via `aura process` /
/// `aura campaign`.
Mc(McCmd),
/// List or inspect recorded run families.
Runs(RunsCmd),
@@ -1181,6 +1224,7 @@ struct GraphCmd {
#[derive(Subcommand)]
enum GraphSub {
/// Construct a graph from a stdin op-list.
#[command(after_help = crate::graph_construct::OP_REFERENCE)]
Build,
/// Introspect a graph.
Introspect(GraphIntrospectCmd),
@@ -1193,7 +1237,7 @@ enum GraphSub {
#[derive(Args)]
struct GraphIntrospectCmd {
/// List the closed node vocabulary (one node type per line).
/// List the closed node vocabulary (one node type + its meaning per line).
#[arg(long)]
vocabulary: bool,
/// Describe one node type's ports by name.
@@ -1202,6 +1246,9 @@ struct GraphIntrospectCmd {
/// List the graph's unwired (unbound) ports.
#[arg(long)]
unwired: bool,
/// List the closed tap-fold vocabulary (fold, bind rule, output kind, meaning).
#[arg(long)]
folds: bool,
/// Print the graph's content id (topology hash). With FILE, read the
/// document from it (a blueprint envelope or an op-list, shape-
/// discriminated, #196); without FILE, read a stdin op-list as before.
@@ -1297,6 +1344,12 @@ struct RunCmd {
/// Not accepted — CLI-side trace persistence is retired (see #224).
#[arg(long)]
trace: Option<String>,
/// Subscribe a declared tap to a fold for this run (repeatable,
/// TAP=FOLD; e.g. --tap signal=mean). Replaces the record-all
/// default: only listed taps are bound, unlisted taps stay unbound.
/// Fold roster: `aura graph introspect --folds`.
#[arg(long = "tap", value_name = "TAP=FOLD")]
tap: Vec<String>,
}
#[derive(Args)]
@@ -1421,7 +1474,7 @@ fn is_blueprint_file(arg: &Option<String>) -> Option<&str> {
/// mirroring the old `parse_blueprint_run_args` window guard (`--from`/`--to`
/// require `--real`; empty symbol rejected). Refuses in place (stderr + exit 2).
fn run_data_from(real: Option<&str>, from: Option<i64>, to: Option<i64>) -> RunData {
let usage = "Usage: aura run <blueprint.json> [--params <json-cell-array>] [--seed <n>] [--real <SYMBOL> [--from <ms>] [--to <ms>]]";
let usage = "Usage: aura run <blueprint.json> [--params <json-cell-array>] [--seed <n>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--tap <TAP=FOLD> …]";
match real {
Some(s) if !s.is_empty() => RunData::Real { symbol: s.to_string(), from, to },
Some(_) => {
@@ -1715,18 +1768,44 @@ fn campaign_window_ms(choice: DataChoice, env: &aura_runner::project::Env) -> (i
)
}
/// Build the run's tap plan from repeated `--tap TAP=FOLD` selections
/// (#310). No selections → the record-all default (today's behaviour).
/// Any selection → an explicit plan that REPLACES the default entirely:
/// only listed taps are bound; unlisted declared taps stay unbound,
/// which C27 defines as inert, not an error. Tap/label existence is
/// bind_tap_plan's to validate (roster refusal / UndeclaredTap), before
/// any store I/O.
fn tap_plan_from_args(args: &[String]) -> Result<TapPlan, String> {
if args.is_empty() {
return Ok(TapPlan::record_all());
}
let mut plan = TapPlan::empty();
let mut seen = BTreeSet::new();
for raw in args {
let (tap, label) = match raw.split_once('=') {
Some((t, l)) if !t.is_empty() && !l.is_empty() => (t, l),
_ => return Err(format!("--tap expects TAP=FOLD, got \"{raw}\"")),
};
if !seen.insert(tap.to_string()) {
return Err(format!("--tap names tap \"{tap}\" twice"));
}
plan.subscribe(tap, TapSubscription::named(label));
}
Ok(plan)
}
/// `aura run`: the loaded-blueprint branch (an existing `.json` first-positional) or
/// the built-in harness-kind dispatch.
fn dispatch_run(a: RunCmd, env: &aura_runner::project::Env) {
match is_blueprint_file(&a.blueprint) {
Some(path) => {
// The loaded-blueprint grammar takes only --params/--seed/--real/--from/--to;
// The loaded-blueprint grammar takes only --params/--seed/--real/--from/--to/--tap;
// the built-in-only flags are rejected here (exit 2), never silently dropped —
// mirroring the sweep/mc blueprint branches, which reject their non-branch flags
// exhaustively (refuse-don't-guess). clap's optional `[blueprint]` positional
// makes these structurally parseable, so the guard is re-asserted at dispatch.
if a.trace.is_some() {
eprintln!("aura: Usage: aura run <blueprint.json> [--params <json-cell-array>] [--seed <n>] [--real <SYMBOL> [--from <ms>] [--to <ms>]]");
eprintln!("aura: Usage: aura run <blueprint.json> [--params <json-cell-array>] [--seed <n>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] [--tap <TAP=FOLD> …]");
std::process::exit(2);
}
let doc = std::fs::read_to_string(path).unwrap_or_else(|e| {
@@ -1751,6 +1830,13 @@ fn dispatch_run(a: RunCmd, env: &aura_runner::project::Env) {
// probing a no-`bias` signal would panic on UnknownOutPort).
let has_bias = signal.output().iter().any(|o| o.name == "bias");
let has_tap = !signal.taps().is_empty();
let tap_plan = match tap_plan_from_args(&a.tap) {
Ok(p) => p,
Err(m) => {
eprintln!("aura: {m}");
std::process::exit(2);
}
};
if has_bias {
// Refuse an open (free-knob) blueprint at the dispatch boundary,
// mirroring `blueprint_mc_family`'s closed-guard: `run` bootstraps
@@ -1773,7 +1859,7 @@ fn dispatch_run(a: RunCmd, env: &aura_runner::project::Env) {
None => Vec::new(),
};
let data = run_data_from(a.real.as_deref(), a.from, a.to);
let report = run_signal_r(signal, &params, data, a.seed.unwrap_or(0), env, TapPlan::record_all());
let report = run_signal_r(signal, &params, data, a.seed.unwrap_or(0), env, tap_plan);
println!("{}", report.to_json());
} else if has_tap {
// Measurement path: wrap_r-free closed guard via the signal's own
@@ -1794,7 +1880,7 @@ fn dispatch_run(a: RunCmd, env: &aura_runner::project::Env) {
None => Vec::new(),
};
let data = run_data_from(a.real.as_deref(), a.from, a.to);
let report = run_measurement(signal, &params, data, a.seed.unwrap_or(0), env, TapPlan::record_all());
let report = run_measurement(signal, &params, data, a.seed.unwrap_or(0), env, tap_plan);
println!("{}", report.to_json());
} else {
eprintln!(
@@ -1807,7 +1893,8 @@ fn dispatch_run(a: RunCmd, env: &aura_runner::project::Env) {
None => {
eprintln!(
"aura: Usage: aura run <blueprint.json> [--params <json-cell-array>] \
[--seed <n>] [--real <SYMBOL> [--from <ms>] [--to <ms>]]"
[--seed <n>] [--real <SYMBOL> [--from <ms>] [--to <ms>]] \
[--tap <TAP=FOLD> …]"
);
std::process::exit(2);
}
@@ -3813,5 +3900,41 @@ mod tests {
assert_eq!(stop_length, R_SMA_STOP_LENGTH, "omitted --stop-length defaults to the regime");
assert_eq!(stop_k, R_SMA_STOP_K, "omitted --stop-k defaults to the regime");
}
#[test]
fn tap_plan_from_args_empty_is_record_all_ok() {
assert!(tap_plan_from_args(&[]).is_ok());
}
#[test]
fn tap_plan_from_args_refuses_a_pair_without_equals() {
let err = match tap_plan_from_args(&["fast_tapmean".to_string()]) {
Err(e) => e,
Ok(_) => panic!("expected an error"),
};
assert_eq!(err, "--tap expects TAP=FOLD, got \"fast_tapmean\"");
}
#[test]
fn tap_plan_from_args_refuses_empty_tap_or_label() {
assert!(tap_plan_from_args(&["=mean".to_string()]).is_err());
assert!(tap_plan_from_args(&["fast_tap=".to_string()]).is_err());
}
#[test]
fn tap_plan_from_args_refuses_the_same_tap_twice() {
let args = vec!["a=mean".to_string(), "a=last".to_string()];
let err = match tap_plan_from_args(&args) {
Err(e) => e,
Ok(_) => panic!("expected an error"),
};
assert_eq!(err, "--tap names tap \"a\" twice");
}
#[test]
fn tap_plan_from_args_accepts_distinct_selections() {
let args = vec!["a=mean".to_string(), "b=record".to_string()];
assert!(tap_plan_from_args(&args).is_ok());
}
}
+11 -2
View File
@@ -107,10 +107,12 @@ fn print_metric_roster() {
(true, false) => "rankable",
(false, false) => "annotation",
};
// C29 (#315): the roster carries each metric's one-line meaning, not
// only where it may be used.
if generalize {
println!("{name:<24} {base} | generalize");
println!("{name:<24} {base} | generalize{}", m.doc);
} else {
println!("{name:<24} {base}");
println!("{name:<24} {base}{}", m.doc);
}
}
}
@@ -372,6 +374,13 @@ fn describe_one_block(id: &str) -> Result<(), String> {
for s in schema.slots {
let req = if s.required { "required" } else { "optional" };
println!(" {:<20} {req}, {}", s.name, slot_kind_label(s.kind));
// C29 (#315): the tap slot's closed value vocabulary carries its
// per-entry meanings, indented under the slot line.
if matches!(s.kind, aura_research::SlotKind::TapKinds) {
for t in aura_research::tap_vocabulary() {
println!(" {:<14} {}", t.id, t.doc);
}
}
}
Ok(())
}
+45
View File
@@ -165,6 +165,8 @@ impl Scale {
}],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![ParamSpec { name: "factor".into(), kind: ScalarKind::F64 }],
// C29: every vocabulary entry ships a one-line meaning — the
// doc field is required (E0063 without it) and gated at load.
doc: "scalar gain: emits the input times the factor param",
},
|p| Box::new(Scale::new(p[0].f64())),
@@ -238,6 +240,14 @@ std vocabulary, anchored by `Aura.toml`. There is no crate and no build step.
`aura nodes new <name>` scaffolds a node crate beside this project and
attaches it via `[nodes]` in `Aura.toml` (build it with `cargo build`).
- Topology is data (`blueprints/*.json`); results land in `runs/`.
- Execution model: a strategy emits a bias in [-1,+1] per cycle, held as the
continuously-tracked target position; a protective stop defines the risk
unit R, and quality metrics are R-based. Entry signals become held state
via the signal-side latch/edge-pulse idiom (see
`aura graph introspect --vocabulary`).
- Data plane: the research verbs are sugar over registered process/campaign
documents — author them directly with `aura process` / `aura campaign`,
growing one from a bare `{}` via `aura campaign introspect --unwired`.
"#;
/// Render a data-only project template: only `__NAME__`/`__NAME_SNAKE__`
@@ -455,6 +465,41 @@ mod tests {
assert!(claude.contains("demo_lab_signal.fast.length"));
}
/// #315: the scaffolded project CLAUDE.md teaches the execution semantics
/// (bias as held target position, protective stop, R) and the document
/// data plane — the two things the 2026-07-22 field agent had to discover
/// forensically.
#[test]
fn project_claude_md_teaches_execution_semantics_and_the_data_plane() {
let claude = render_project(CLAUDE_MD_PROJECT, "demo-lab");
assert!(claude.contains("target position"), "names the held-target model: {claude}");
assert!(claude.contains("protective stop"), "names the stop that defines R: {claude}");
assert!(claude.contains("aura process"), "points at the document layer: {claude}");
assert!(claude.contains("introspect --unwired"), "names the bare-{{}} ramp: {claude}");
}
/// #323: the authoring guide's S0 worked literal is byte-identical to the
/// scaffold's `LIB_RS` template (rendered with the guide's `my_lab`
/// namespace). The scaffold e2e tests compile that template for real, so
/// this equality pin is a transitive compile guard — the guide literal
/// can no longer drift silently from the schema (it broke against the
/// required `NodeSchema.doc` once).
#[test]
fn guide_worked_example_matches_the_scaffold_template() {
let guide = include_str!("../../../docs/authoring-guide.md");
let anchor = "### Worked example: `Scale`";
let after = &guide[guide.find(anchor).expect("guide keeps the S0 worked example")..];
let start = after.find("```rust\n").expect("worked example is a rust fence") + 8;
let fence = &after[start..];
let fence = &fence[..fence.find("\n```").expect("fence closes") + 1];
let body = &LIB_RS[LIB_RS.find("use aura_core::{").expect("template body starts at the import")..];
let expected = body.replace("__NS__", "my_lab");
assert_eq!(
fence, expected,
"docs/authoring-guide.md S0 literal drifted from the compiled scaffold template"
);
}
/// Property (#183, fieldtest F2 gap): the scaffold's own starter node teaches
/// the bound-param build closure. Its rendered `src/lib.rs` declares a param in
/// the node schema and reads a bound value in the build closure (not the
+15
View File
@@ -354,6 +354,21 @@ fn graph_build_bad_stdin_content_is_runtime_exit_1() {
assert!(stderr.contains("Nope"), "still names the cause: {stderr}");
}
/// Property (#326): an op-list element carrying an unknown/typo'd key (e.g.
/// `params` where the schema expects `bind`) is refused at parse — not
/// silently dropped. Previously the unrecognized key vanished, the field it
/// meant to set kept its default, and the wrong graph built with zero
/// signal. Same content-fault family as `graph_build_bad_stdin_content_is_
/// runtime_exit_1` (both fire from the same top-level deserialize), so the
/// exit class matches: exit 1, stderr names the offending key.
#[test]
fn graph_build_rejects_an_unknown_op_field() {
let doc = r#"[{"op":"add","type":"Const","params":{"value":{"F64":1.0}}}]"#;
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("params"), "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
@@ -0,0 +1,175 @@
//! End-to-end pins for the self-description surfaces (#315, #323): the help
//! opens with the two-layer concept, the sugar verbs name the document shape
//! they desugar to, every introspection roster carries per-entry meanings,
//! and `graph build --help` carries the op-list reference. Driven over the
//! built binary — the zero-setup surface a reader without repo access gets.
use std::process::Command;
const BIN: &str = env!("CARGO_BIN_EXE_aura");
/// Run `aura <args>` (no stdin); return (stdout, stderr, success).
fn run(args: &[&str]) -> (String, String, bool) {
let out = Command::new(BIN).args(args).output().expect("spawn aura");
(
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
out.status.success(),
)
}
/// #315: `aura --help` opens with the concepts paragraph — the two layers,
/// the bias-as-target-position + protective-stop execution model, and how
/// traces come to exist. Each pin sits on one authored line of the explicit
/// `long_about` string, so clap's wrapping cannot split it.
#[test]
fn top_level_help_opens_with_the_two_layer_concept() {
let (out, err, ok) = run(&["--help"]);
assert!(ok, "aura --help failed: {err}");
assert!(out.contains("research verbs"), "names the sugar layer: {out}");
assert!(out.contains("directly authorable"), "names the document data plane: {out}");
assert!(out.contains("bias in [-1,+1]"), "names the bias output: {out}");
assert!(out.contains("defines the risk unit"), "names the protective stop / R: {out}");
assert!(out.contains("introspect --unwired"), "names the bare-{{}} ramp: {out}");
assert!(out.contains("aura chart"), "names the trace consumers: {out}");
}
/// #315: each document-bridged sugar verb's long help names the process
/// shape it desugars to and points at the document layer. `run` (not
/// document-bridged) points at the canonical document-first form instead.
#[test]
fn sugar_verbs_name_their_document_shape() {
for (verb, needle) in [
("sweep", "std::sweep"),
("walkforward", "std::walk_forward"),
("mc", "std::monte_carlo"),
("generalize", "std::generalize"),
] {
let (out, err, ok) = run(&[verb, "--help"]);
assert!(ok, "aura {verb} --help failed: {err}");
assert!(out.contains("Sugar"), "{verb} --help names the sugar relation: {out}");
assert!(out.contains(needle), "{verb} --help names {needle}: {out}");
assert!(out.contains("aura campaign"), "{verb} --help points at the documents: {out}");
}
let (out, err, ok) = run(&["run", "--help"]);
assert!(ok, "aura run --help failed: {err}");
assert!(out.contains("document-first"), "run --help names the canonical form: {out}");
}
/// #323: `graph build --help` carries the op-list reference — the op kinds
/// and fields are learnable from the binary, not only from serde refusals.
#[test]
fn graph_build_help_carries_the_op_reference() {
let (out, err, ok) = run(&["graph", "build", "--help"]);
assert!(ok, "aura graph build --help failed: {err}");
for op in [
r#"{"op":"source""#,
r#"{"op":"input""#,
r#"{"op":"add""#,
r#"{"op":"feed""#,
r#"{"op":"connect""#,
r#"{"op":"expose""#,
r#"{"op":"tap""#,
r#"{"op":"gang""#,
r#"{"op":"doc""#,
] {
assert!(out.contains(op), "op reference carries {op}: {out}");
}
assert!(out.contains("graph introspect --vocabulary"), "points at the node roster: {out}");
}
/// #315: the node vocabulary listing carries each type's one-line meaning —
/// no more bare names that teach nothing.
#[test]
fn graph_introspect_vocabulary_carries_meanings() {
let (out, err, ok) = run(&["graph", "introspect", "--vocabulary"]);
assert!(ok, "vocabulary listing failed: {err}");
let sma = out
.lines()
.find(|l| l.split_whitespace().next() == Some("SMA"))
.unwrap_or_else(|| panic!("no SMA line: {out}"));
assert!(sma.contains("moving average"), "SMA line carries its meaning: {sma}");
for l in out.lines().filter(|l| !l.trim().is_empty()) {
assert!(l.split_whitespace().count() >= 2, "bare name without meaning: {l}");
}
}
/// #315: `--node <T>`'s head line carries the type's meaning beside its label.
#[test]
fn graph_introspect_node_head_line_carries_the_meaning() {
let (out, err, ok) = run(&["graph", "introspect", "--node", "SMA"]);
assert!(ok, "node introspect failed: {err}");
let head = out.lines().next().unwrap_or("");
assert!(head.contains("moving average"), "head line carries the meaning: {out}");
}
/// #332: `--folds` renders the fold-REGISTRY roster (the same roster
/// `aura run --tap TAP=FOLD` resolves labels against), not the aura-std
/// `FoldKind` table — lowercase, parseable labels, including the `record`
/// entry that has no `FoldKind` counterpart. Previously this surface printed
/// capitalized `FoldKind` ids (`Mean`) and omitted `record`, so the help
/// text's own discovery surface described input `--tap` refused.
#[test]
fn graph_introspect_folds_lists_the_fold_registry_roster() {
let (out, err, ok) = run(&["graph", "introspect", "--folds"]);
assert!(ok, "folds listing failed: {err}");
let lines: Vec<&str> = out.lines().filter(|l| !l.trim().is_empty()).collect();
assert_eq!(lines.len(), 8, "one line per registry entry (record + 7 folds): {out}");
assert!(
!out.split_whitespace().any(|w| w == "Mean"),
"no capitalized FoldKind id leaks through: {out}"
);
let record = lines.iter().find(|l| l.starts_with("record ")).expect("record row");
assert!(record.contains("series"), "record row meaning: {record}");
let mean = lines.iter().find(|l| l.starts_with("mean ")).expect("mean row");
assert!(mean.contains("f64") && mean.contains("mean"), "mean row rules + meaning: {mean}");
let count = lines.iter().find(|l| l.starts_with("count ")).expect("count row");
assert!(count.contains("i64") && count.contains("any"), "count row rules: {count}");
}
/// #315: the metric roster carries each metric's one-line meaning beside its
/// applicability tags.
#[test]
fn process_introspect_metrics_carries_meanings() {
let (out, err, ok) = run(&["process", "introspect", "--metrics"]);
assert!(ok, "metric roster failed: {err}");
let er = out
.lines()
.find(|l| l.split_whitespace().next() == Some("expectancy_r"))
.unwrap_or_else(|| panic!("no expectancy_r line: {out}"));
assert!(er.contains("mean realized R"), "meaning text on the roster line: {er}");
for l in out.lines().filter(|l| !l.trim().is_empty()) {
assert!(l.contains(""), "roster line without meaning: {l}");
}
}
/// #315: the persisted-tap vocabulary's meanings are readable where the taps
/// are advertised — under the `std::presentation` block's slot listing.
#[test]
fn campaign_introspect_block_presentation_lists_tap_meanings() {
let (out, err, ok) = run(&["campaign", "introspect", "--block", "std::presentation"]);
assert!(ok, "presentation describe failed: {err}");
assert!(out.contains("cumulative pip equity"), "equity tap meaning: {out}");
assert!(out.contains("after the cost model"), "net_r_equity tap meaning: {out}");
}
/// #323: `introspect --unwired` enumerates the optional C29 `description`
/// slot for both document kinds — the authoring side of the gate is
/// advertised where every other slot is.
#[test]
fn introspect_unwired_lists_the_description_slot() {
let dir = std::env::temp_dir().join(format!("aura-selfdesc-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let bare = dir.join("bare.json");
std::fs::write(&bare, "{}").unwrap();
for family in ["process", "campaign"] {
let (out, err, ok) = run(&[family, "introspect", "--unwired", bare.to_str().unwrap()]);
assert!(ok, "{family} --unwired failed: {err}");
assert!(
out.contains("open slot: description"),
"{family} --unwired lists the description slot: {out}"
);
assert!(out.contains("C29-gated"), "{family} hint names the gate: {out}");
}
let _ = std::fs::remove_dir_all(&dir);
}
+5 -4
View File
@@ -871,13 +871,14 @@ fn campaign_introspect_unwired_lists_the_optional_risk_slot_on_bare_envelope() {
}
/// #216/#234: bound (non-empty) risk AND cost lists close both optional
/// slots — a complete document with both reports no open slots at all.
/// slots — with the optional C29 description also present (#323), a complete
/// document reports no open slots at all.
#[test]
fn campaign_introspect_unwired_omits_bound_risk_and_cost_slots() {
let dir = temp_cwd("campaign-introspect-unwired-risk-bound");
let with_both = CAMPAIGN_DOC.replacen(
"\"seed\": 1,",
"\"seed\": 1,\n \"risk\": [ { \"vol\": { \"length\": 3, \"k\": 2.0 } } ],\n \"cost\": [ { \"constant\": { \"cost_per_trade\": 2.0 } } ],",
"\"seed\": 1,\n \"description\": \"a screen with bound risk and cost\",\n \"risk\": [ { \"vol\": { \"length\": 3, \"k\": 2.0 } } ],\n \"cost\": [ { \"constant\": { \"cost_per_trade\": 2.0 } } ],",
1,
);
assert_ne!(with_both, CAMPAIGN_DOC, "replacen must actually match the fixture's seed field");
@@ -995,7 +996,7 @@ fn campaign_introspect_unwired_reports_the_spec_example_slots() {
write_doc(&dir, "draft.campaign.json", draft);
let (out, code) = run_code_in(&dir, &["campaign", "introspect", "--unwired", "draft.campaign.json"]);
assert_eq!(code, Some(0));
assert!(out.contains("open slot: process.ref (required, content id of a process document)"));
assert!(out.contains("open slot: process.ref (required, { content_id }: content id of a process document)"));
assert!(out.contains("open slot: strategies[0].axes.slow (axis declared empty — a P1 axis is a non-empty finite set)"));
}
@@ -1021,7 +1022,7 @@ fn campaign_introspect_unwired_reports_placeholder_refs() {
"strategy `ref: null` placeholder not enumerated: {out}"
);
assert!(
out.contains("open slot: process.ref (required, content id of a process document)"),
out.contains("open slot: process.ref (required, { content_id }: content id of a process document)"),
"process `ref: {{}}` placeholder not enumerated: {out}"
);
}
+25
View File
@@ -56,3 +56,28 @@ fn measurement_blueprint_runs_bare_and_emits_its_tap() {
assert_eq!(trace["tap"], "fast_tap");
assert_eq!(trace["kinds"], serde_json::json!(["F64"]));
}
/// #310: the measurement arm honours the `--tap` selector — a `last`
/// fold lands one summary row instead of the full series.
#[test]
fn measurement_run_honours_the_tap_selector() {
let cwd = temp_cwd("selector-last");
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"])
.current_dir(&cwd)
.output()
.expect("spawn aura run");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let trace_text = std::fs::read_to_string(cwd.join("runs/traces/sma_signal/fast_tap.json"))
.expect("read fold trace");
let trace: serde_json::Value = serde_json::from_str(&trace_text).expect("parse fold trace");
let rows = trace["columns"][0].as_array().expect("columns[0] array");
assert_eq!(rows.len(), 1, "a fold lands exactly one summary row");
// last warm SMA(2) value over the synthetic stream = (1.0097 + 1.0092) / 2
let got = rows[0].as_f64().expect("f64 row");
let expected = (1.0097_f64 + 1.0092) / 2.0;
assert!((got - expected).abs() < 1e-9, "last row: got {got}, expected {expected}");
}
+208
View File
@@ -124,6 +124,21 @@ fn duplicate_tap_blueprint_json() -> String {
serde_json::to_string(&v).expect("re-serialize duplicate-tap blueprint")
}
/// The shipped `examples/r_sma.json` blueprint with two distinctly named
/// declared taps: `fast_tap` on node 0 ("fast", SMA(2)) and `slow_tap` on
/// node 1 ("slow") — the smallest fixture on which an explicit `--tap`
/// plan and the record-all default can be compared file-by-file (#310).
fn two_tap_blueprint_json() -> String {
let path = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let doc = std::fs::read_to_string(path).expect("read examples/r_sma.json");
let mut v: serde_json::Value = serde_json::from_str(&doc).expect("parse r_sma.json");
v["blueprint"]["taps"] = serde_json::json!([
{"name": "fast_tap", "from": {"node": 0, "field": 0}},
{"name": "slow_tap", "from": {"node": 1, "field": 0}},
]);
serde_json::to_string(&v).expect("re-serialize two-tap blueprint")
}
/// Property: **a blueprint declaring two taps under the same name is refused
/// on the single-run path with a named error and exit code 1, before any
/// trace is persisted** — the CLI-owned `TapBindError::DuplicateBind` dedup
@@ -212,3 +227,196 @@ fn a_read_only_run_dir_surfaces_terminally_through_the_tap_trace_register() {
);
assert!(!run_dir.join("index.json").exists(), "no index — treat-as-absent crash shape");
}
/// #310: `--tap fast_tap=mean` subscribes the declared tap to the `mean`
/// fold instead of the record-all default — the trace store holds ONE
/// summary row whose value is the mean of the full SMA(2) series, and
/// `index.json` lists exactly the subscribed tap.
#[test]
fn run_tap_selector_persists_a_fold_summary_row() {
let cwd = temp_cwd("tap-selector-mean");
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=mean"])
.current_dir(&cwd)
.output()
.expect("spawn aura run");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let index_text = std::fs::read_to_string(cwd.join("runs/traces/sma_signal/index.json"))
.expect("read index.json");
let index: serde_json::Value = serde_json::from_str(&index_text).expect("parse index.json");
assert_eq!(index["taps"], serde_json::json!(["fast_tap"]));
let trace_text = std::fs::read_to_string(cwd.join("runs/traces/sma_signal/fast_tap.json"))
.expect("read fold trace");
let trace: serde_json::Value = serde_json::from_str(&trace_text).expect("parse fold trace");
assert_eq!(trace["tap"], "fast_tap");
let rows = trace["columns"][0].as_array().expect("columns[0] array");
assert_eq!(rows.len(), 1, "a fold lands exactly one summary row");
let sma: Vec<f64> = (1..R_SMA_PRICES.len())
.map(|i| (R_SMA_PRICES[i - 1] + R_SMA_PRICES[i]) / 2.0)
.collect();
let expected = sma.iter().sum::<f64>() / sma.len() as f64;
let got = rows[0].as_f64().expect("f64 row");
assert!((got - expected).abs() < 1e-9, "mean row: got {got}, expected {expected}");
}
/// #310: an all-`record` explicit plan is byte-identical to the no-flag
/// record-all default — the selector replaces the default without
/// changing what `record` itself writes (C1 determinism across runs).
#[test]
fn run_explicit_record_plan_matches_the_record_all_default() {
let cwd_a = temp_cwd("record-default");
let cwd_b = temp_cwd("record-explicit");
for cwd in [&cwd_a, &cwd_b] {
std::fs::write(cwd.join("two_tap.json"), two_tap_blueprint_json())
.expect("write two-tap blueprint");
}
let run = |cwd: &std::path::Path, extra: &[&str]| {
let mut args = vec!["run", "two_tap.json"];
args.extend_from_slice(extra);
let out = Command::new(BIN)
.args(&args)
.current_dir(cwd)
.output()
.expect("spawn aura run");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
};
run(&cwd_a, &[]);
run(&cwd_b, &["--tap", "fast_tap=record", "--tap", "slow_tap=record"]);
for file in ["fast_tap.json", "slow_tap.json"] {
let a = std::fs::read(cwd_a.join("runs/traces/sma_signal").join(file)).expect("default trace");
let b = std::fs::read(cwd_b.join("runs/traces/sma_signal").join(file)).expect("explicit trace");
assert_eq!(a, b, "{file} must be byte-identical across default and explicit record plans");
}
let taps_of = |cwd: &std::path::Path| -> serde_json::Value {
let text = std::fs::read_to_string(cwd.join("runs/traces/sma_signal/index.json"))
.expect("read index.json");
serde_json::from_str::<serde_json::Value>(&text).expect("parse index.json")["taps"].clone()
};
assert_eq!(taps_of(&cwd_a), taps_of(&cwd_b));
}
/// #310: an unknown fold label is refused through the registry's
/// roster-enumerating refusal, before any store write.
#[test]
fn run_tap_selector_refuses_an_unknown_label_with_the_roster() {
let cwd = temp_cwd("tap-selector-unknown-label");
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"])
.current_dir(&cwd)
.output()
.expect("spawn aura run");
assert!(!out.status.success(), "an unknown fold label must refuse");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("medain") && stderr.contains("mean"),
"refusal must name the label and enumerate the roster: {stderr}"
);
assert!(!cwd.join("runs").exists(), "a refused run must not write the store");
}
/// #310/#333: a selection naming a tap the blueprint does not declare is
/// refused typed (UndeclaredTap), before any store write, and the refusal
/// enumerates the blueprint's declared taps (the same way the unknown-fold
/// refusal enumerates the fold-registry roster) — on the two-tap fixture,
/// both `fast_tap` and `slow_tap` must be named so the author does not have
/// to reopen the blueprint JSON.
#[test]
fn run_tap_selector_refuses_an_undeclared_tap() {
let cwd = temp_cwd("tap-selector-undeclared");
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"])
.current_dir(&cwd)
.output()
.expect("spawn aura run");
assert!(!out.status.success(), "an undeclared tap must refuse");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("nope"), "refusal must name the offending tap: {stderr}");
assert!(
stderr.contains("fast_tap") && stderr.contains("slow_tap"),
"refusal must enumerate the blueprint's declared taps: {stderr}"
);
assert!(!cwd.join("runs").exists(), "a refused run must not write the store");
}
/// #334: an explicit `--tap` plan that leaves a declared tap unbound emits
/// the C14 benign skipped-tap note on stderr, per unbound tap, naming it —
/// exit code stays 0 (this is a note, not a refusal). `fast_tap` (subscribed)
/// gets no such note; `slow_tap` (left unlisted) does.
#[test]
fn run_tap_selector_notes_unbound_declared_taps_on_stderr() {
let cwd = temp_cwd("tap-selector-unbound-note");
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", "fast_tap=mean"])
.current_dir(&cwd)
.output()
.expect("spawn aura run");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("aura: note: declared tap \"slow_tap\" unbound this run"),
"stderr must note the unbound declared tap: {stderr}"
);
assert!(
!stderr.contains("\"fast_tap\" unbound"),
"the subscribed tap must not be noted as unbound: {stderr}"
);
}
/// #334: the record-all default (no `--tap` flag) leaves no declared tap
/// unbound — nothing is skipped, so the note must never appear.
#[test]
fn run_with_no_tap_flag_emits_no_unbound_note() {
let cwd = temp_cwd("tap-selector-default-no-note");
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")])
.current_dir(&cwd)
.output()
.expect("spawn aura run");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("unbound this run"),
"the record-all default must never note a skipped tap: {stderr}"
);
}
/// #310: a malformed or duplicate `--tap` is a usage error (exit 2)
/// before anything runs.
#[test]
fn run_tap_selector_refuses_malformed_and_duplicate_pairs() {
let cwd = temp_cwd("tap-selector-usage");
let bp_path = cwd.join("tapped_r_sma.json");
std::fs::write(&bp_path, tap_blueprint_json()).expect("write tapped blueprint");
for args in [
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")];
full.extend(args);
let out = Command::new(BIN)
.args(&full)
.current_dir(&cwd)
.output()
.expect("spawn aura run");
assert_eq!(out.status.code(), Some(2), "usage errors exit 2");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("--tap"), "usage message names the flag: {stderr}");
assert!(!cwd.join("runs").exists());
}
}
+24 -5
View File
@@ -1102,7 +1102,7 @@ pub fn slot_kind_label(kind: SlotKind) -> &'static str {
SlotKind::Strings => "list of strings",
SlotKind::Windows => "list of { from_ms, to_ms }",
SlotKind::Ref => "one of { content_id } | { identity_id }",
SlotKind::ContentRef => "content id of a process document",
SlotKind::ContentRef => "{ content_id }: content id of a process document",
SlotKind::Axes => "map: param name -> { kind, values }",
SlotKind::EmitKinds => "list of: family_table | selection_report",
SlotKind::TapKinds => "list of: equity | exposure | r_equity | net_r_equity",
@@ -1301,8 +1301,10 @@ pub fn open_slots_campaign(text: &str) -> Result<Vec<OpenSlot>, DocError> {
}
if ref_slot_is_open(v.get("process").and_then(|p| p.get("ref"))) {
// deliberately narrower than the strategy-ref hint: validate_campaign
// refuses an identity_id process ref, so the guide must not offer it
slots.push(open("process.ref", "required, content id of a process document"));
// refuses an identity_id process ref, so the guide must not offer it.
// The tagged { content_id } shape is stated explicitly (#329) so the
// hint cannot be misread as accepting a bare id string.
slots.push(open("process.ref", "required, { content_id }: content id of a process document"));
}
if v.get("seed").and_then(|s| s.as_u64()).is_none() {
slots.push(open("seed", "required, non-negative integer"));
@@ -1326,6 +1328,14 @@ fn envelope_open_slots(v: &serde_json::Value, kind: &str, slots: &mut Vec<OpenSl
if v.get("kind").and_then(|k| k.as_str()) != Some(kind) {
slots.push(open("kind", format!("required, must be \"{kind}\"")));
}
// C29 (#323): the description slot is enumerable like every other
// envelope slot — optional, so only listed while absent.
if v.get("description").is_none() {
slots.push(open(
"description",
"optional, string — a one-line meaning (C29-gated when present)",
));
}
}
#[cfg(test)]
@@ -1470,6 +1480,7 @@ mod tests {
"format_version": 1,
"kind": "campaign",
"name": "ger40-momentum-screen",
"description": "GER40 momentum screen over the fast/slow SMA grid.",
"data": {
"instruments": ["GER40"],
"windows": [ { "from_ms": 1704067200000, "to_ms": 1719792000000 } ]
@@ -2365,6 +2376,14 @@ mod tests {
label.contains("content"),
"process_ref describe must still name the content-id form: {label}"
);
// #329: the label must state the TAGGED shape ({ content_id: ... }) the
// parser actually requires, not read like a bare id string — mirroring
// the `{ content_id } | { identity_id }` bracket notation std::strategy
// already uses, narrowed to the one key process.ref accepts.
assert!(
label.contains("{ content_id }"),
"process_ref describe must state the tagged {{ content_id }} shape, not a bare id: {label}"
);
let strategy = describe_block("std::strategy").expect("strategy describable");
let strat_ref =
@@ -2421,7 +2440,7 @@ mod tests {
"presentation": { "persist_taps": [], "emit": [] } }"#;
let slots = open_slots_campaign(draft).unwrap();
assert!(slots.iter().any(|s| s.path == "process.ref"
&& s.hint == "required, content id of a process document"));
&& s.hint == "required, { content_id }: content id of a process document"));
assert!(slots.iter().any(|s| s.path == "strategies[0].axes.slow"
&& s.hint == "axis declared empty — a P1 axis is a non-empty finite set"));
@@ -2457,7 +2476,7 @@ mod tests {
// ...and the `{}` process ref reports the narrower content-id-only hint.
assert!(
slots.iter().any(|s| s.path == "process.ref"
&& s.hint == "required, content id of a process document"),
&& s.hint == "required, { content_id }: content id of a process document"),
"process `ref: {{}}` placeholder not reported as an open slot: {slots:?}"
);
}
+6 -1
View File
@@ -1728,9 +1728,14 @@ mod tests {
Ok(_) => panic!("unknown tap must be refused"),
};
assert!(
matches!(err, TapPlanError::UnknownTap { ref name } if name == "no_such_tap"),
matches!(err, TapPlanError::UnknownTap { ref name, .. } if name == "no_such_tap"),
"{err:?}"
);
let msg = err.to_string();
assert!(
msg.contains("fast_tap"),
"unknown-tap refusal enumerates the declared taps (fixture declares only fast_tap): {msg}"
);
// Unknown label — the refusal enumerates the roster.
let mut flat2 = tapped_r_sma().compile_with_params(&[]).expect("tapped r_sma compiles");
+91 -11
View File
@@ -161,17 +161,21 @@ impl FoldRegistry {
}),
});
for (label, doc, fold) in [
("count", "number of warm rows (any kind; i64 row)", FoldKind::Count),
("sum", "sum of the series (f64 taps; f64 row)", FoldKind::Sum),
("mean", "arithmetic mean of the series (f64 taps; f64 row)", FoldKind::Mean),
("min", "minimum of the series (f64 taps; f64 row)", FoldKind::Min),
("max", "maximum of the series (f64 taps; f64 row)", FoldKind::Max),
("count", "number of warm rows (any kind; i64 row); one row at the last warm ts", FoldKind::Count),
("sum", "sum of the series (f64 taps; f64 row); one row at the last warm ts", FoldKind::Sum),
(
"mean",
"arithmetic mean of the series (f64 taps; f64 row); one row at the last warm ts",
FoldKind::Mean,
),
("min", "minimum of the series (f64 taps; f64 row); one row at the last warm ts", FoldKind::Min),
("max", "maximum of the series (f64 taps; f64 row); one row at the last warm ts", FoldKind::Max),
(
"first",
"first warm value, at its own timestamp (any kind; kind-preserving row)",
FoldKind::First,
),
("last", "last warm value (any kind; kind-preserving row)", FoldKind::Last),
("last", "last warm value, at its own timestamp (any kind; kind-preserving row)", FoldKind::Last),
] {
r.register(FoldEntry {
label,
@@ -217,7 +221,7 @@ impl FoldRegistry {
/// `aura: ` + exit-1 refusal register via `Display`.
pub enum TapPlanError {
/// The plan names a tap the blueprint does not declare.
UnknownTap { name: String },
UnknownTap { name: String, declared: Vec<String> },
/// The plan names a label the registry does not carry.
UnknownLabel { label: String, roster: Vec<&'static str> },
/// The entry's bind rule rejects the tap's column kind.
@@ -237,8 +241,12 @@ pub enum TapPlanError {
impl fmt::Display for TapPlanError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TapPlanError::UnknownTap { name } => {
write!(f, "the tap plan names '{name}', but the blueprint declares no such tap")
TapPlanError::UnknownTap { name, declared } => {
write!(
f,
"the tap plan names '{name}', but the blueprint declares no such tap — declared taps: {}",
declared.join(", ")
)
}
TapPlanError::UnknownLabel { label, roster } => {
write!(f, "unknown fold '{label}' — available: {}", roster.join(", "))
@@ -402,7 +410,10 @@ pub fn bind_tap_plan(
// Unknown-tap guard: every plan name must be declared.
for name in plan.by_name.keys() {
if !declared_taps.iter().any(|t| &t.name == name) {
return Err(TapPlanError::UnknownTap { name: name.clone() });
return Err(TapPlanError::UnknownTap {
name: name.clone(),
declared: declared_taps.iter().map(|t| t.name.clone()).collect(),
});
}
}
@@ -422,7 +433,18 @@ pub fn bind_tap_plan(
Some((label, params)) => {
Resolved::Named { label: label.clone(), params: params.clone() }
}
None => continue, // unbound, inert (C27)
// Unbound, inert (C27) — but only reachable when `plan` carries
// no default (an EXPLICIT plan, e.g. from `--tap`): record-all
// (`default_named` = `Some(("record", …))`) always resolves the
// Some arm above, so this arm never fires under record-all and
// the note is exactly the C14 benign skipped-tap class (#334).
// Emitted here (aura-runner), beside the pre-existing
// runner-side `eprintln!` registers in this module/`member.rs`/
// `measure.rs` — the runner→CLI print migration is #297.
None => {
eprintln!("aura: note: declared tap \"{}\" unbound this run", tap.name);
continue;
}
},
};
if let Resolved::Named { label, params } = &sub {
@@ -504,6 +526,64 @@ mod tests {
assert!(r.roster().iter().all(|(_, doc)| !doc.is_empty()), "every entry documents itself");
}
/// C29 entry seam for the registry roster: every entry ships a gate-clean
/// one-line meaning, and the bind/output prose inside it matches the
/// entry's executable rules — the drift-pin the retired aura-std
/// `fold_vocabulary` table carried, moved here with the surface (#332).
#[test]
fn roster_docs_pass_the_doc_gate_and_match_the_executable_rules() {
let r = FoldRegistry::core();
for entry in r.entries.values() {
aura_core::doc_gate(entry.label, entry.doc)
.unwrap_or_else(|f| panic!("fold {} doc fails the gate: {f:?}", entry.label));
if entry.doc.contains("f64 taps") {
assert!(
(entry.binds_at)(ScalarKind::F64) && !(entry.binds_at)(ScalarKind::I64),
"{} claims f64-only but binds wider",
entry.label
);
} else {
assert!(
entry.doc.contains("any kind"),
"{}: bind prose must be 'f64 taps' or 'any kind': {}",
entry.label,
entry.doc
);
assert!(
(entry.binds_at)(ScalarKind::I64) && (entry.binds_at)(ScalarKind::Bool),
"{} claims any-kind but refuses a kind",
entry.label
);
}
if entry.doc.contains("i64 row") {
assert!(
matches!((entry.output)(ScalarKind::F64), FoldOutput::Row(ScalarKind::I64)),
"{} claims an i64 row but outputs otherwise",
entry.label
);
} else if entry.doc.contains("f64 row") {
assert!(
matches!((entry.output)(ScalarKind::F64), FoldOutput::Row(ScalarKind::F64)),
"{} claims an f64 row but outputs otherwise",
entry.label
);
} else if entry.doc.contains("kind-preserving row") {
assert!(
matches!((entry.output)(ScalarKind::F64), FoldOutput::Row(ScalarKind::F64))
&& matches!((entry.output)(ScalarKind::I64), FoldOutput::Row(ScalarKind::I64)),
"{} claims kind-preserving but outputs otherwise",
entry.label
);
} else {
assert!(
matches!((entry.output)(ScalarKind::F64), FoldOutput::Series),
"{}: doc names no row kind, so it must be the series entry",
entry.label
);
}
}
}
#[test]
fn unknown_label_refusal_enumerates_the_roster() {
let r = FoldRegistry::core();
+1
View File
@@ -29,6 +29,7 @@ impl CarryCost {
"CarryCost",
Vec::new(),
vec![ParamSpec { name: "carry_per_cycle".into(), kind: ScalarKind::F64 }],
"cost-model node: cost accrued per held cycle (param carry_per_cycle)",
|p| Box::new(CarryCost::new(p[0].f64())),
)
}
@@ -28,6 +28,7 @@ impl ConstantCost {
"ConstantCost",
Vec::new(),
vec![ParamSpec { name: "cost_per_trade".into(), kind: ScalarKind::F64 }],
"cost-model node: fixed cost per trade (param cost_per_trade), charged at close",
|p| Box::new(ConstantCost::new(p[0].f64())),
)
}
+22 -12
View File
@@ -214,26 +214,20 @@ impl<F: CostNode> Node for CostRunner<F> {
}
/// Assemble a cost-node `PrimitiveBuilder`: the 4 geometry inputs ++ the factor's
/// extra inputs, the standard 3-field cost output, the given params, and a build
/// closure. The single home for the cost-node schema shape.
/// extra inputs, the standard 3-field cost output, the given params, a
/// factor-specific one-line `doc` (C29 — each cost node names its own charge
/// basis rather than sharing one generic sentence, #330), and a build closure.
/// The single home for the cost-node schema shape.
pub fn cost_node_builder(
name: &'static str,
extra_inputs: Vec<PortSpec>,
params: Vec<ParamSpec>,
doc: &'static str,
build: impl Fn(&[Cell]) -> Box<dyn Node> + 'static,
) -> PrimitiveBuilder {
let mut inputs = geometry_input_ports();
inputs.extend(extra_inputs);
PrimitiveBuilder::new(
name,
NodeSchema {
inputs,
output: cost_output_fields(),
params,
doc: "cost-model node: charges its cost in R from position geometry, at close or accrued per held cycle",
},
build,
)
PrimitiveBuilder::new(name, NodeSchema { inputs, output: cost_output_fields(), params, doc }, build)
}
#[cfg(test)]
@@ -501,6 +495,7 @@ mod tests {
"Probe",
extra.clone(),
params.clone(),
"cost-model node: probe factor for the builder-assembly test",
|_p| -> Box<dyn Node> { Box::new(CostRunner::new(StubCost(0.0))) },
);
let schema = builder.schema();
@@ -537,6 +532,21 @@ mod tests {
assert_eq!(a, "volatility");
}
#[test]
fn cost_node_descriptions_are_pairwise_distinct() {
// C29: `aura graph introspect --vocabulary` must be able to tell the
// three cost nodes apart by their charge basis (flat-per-trade vs
// per-held-cycle-accrual vs volatility-scaled), not read one
// identical generic sentence three times (#330).
use crate::{CarryCost, ConstantCost, VolSlippageCost};
let constant_doc = ConstantCost::builder().schema().doc;
let carry_doc = CarryCost::builder().schema().doc;
let vol_slippage_doc = VolSlippageCost::builder().schema().doc;
assert_ne!(constant_doc, carry_doc, "ConstantCost vs CarryCost doc must differ");
assert_ne!(constant_doc, vol_slippage_doc, "ConstantCost vs VolSlippageCost doc must differ");
assert_ne!(carry_doc, vol_slippage_doc, "CarryCost vs VolSlippageCost doc must differ");
}
#[test]
fn producer_and_aggregator_share_the_triple() {
// The structural lockstep: producer output and aggregator output/inputs all
@@ -43,6 +43,7 @@ impl VolSlippageCost {
"VolSlippageCost",
volatility_input_ports(),
vec![ParamSpec { name: "slip_vol_mult".into(), kind: ScalarKind::F64 }],
"cost-model node: slippage proportional to volatility (slip_vol_mult × volatility input), charged at close",
|p| Box::new(VolSlippageCost::new(p[0].f64())),
)
}
+2 -1
View File
@@ -122,7 +122,8 @@ impl Scale {
pub fn new(factor: f64) -> Self {
Self { factor, out: [Cell::from_f64(0.0)] }
}
// Replace `doc` below when renaming this sample node — it must keep
// describing the node's own behaviour, not this scaffolding step.
pub fn builder() -> PrimitiveBuilder {
PrimitiveBuilder::new(
"my_lab::Scale",
@@ -0,0 +1,11 @@
# C27 — Declared taps: named measurement points bind sinks run-mode-aware: 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: [c27-declared-taps.md](c27-declared-taps.md).
**Current-state tap-plan sentence (2026-07-21, #283; superseded 2026-07-24 by
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."
+18 -2
View File
@@ -68,12 +68,28 @@ 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 CLI verbs pass a record-all plan. The `record`
(`aura-runner::measure`) both arms of the single CLI verb `aura run`, 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
by the run-mode owner — on the one-shot path the CLI invocation itself, a
projection exercising this contract's authority, not a second home for
intent under [C25](c25-role-model.md) — while *adding* a fold stays a Rust
entry (role 2). The campaign/document carrier, and the reconciliation of the
presentation-tap namespace (`persist_taps`) with declared taps it requires,
is deferred to the Measurement-reachable milestone (#312/#327, minuted on
#312). The `record`
consumer (`aura-runner::tap_recorder::TapRecorder`) holds the trace store's
streaming writer in-graph — `initialize` opens (deferred acquisition), `eval`
appends `(Timestamp, Cell)` (zero per-cycle heap, #77), `finalize` reports
exactly one terminal outcome; fold consumers (`aura-std::TapFold`) land one
summary row; live closures run inline (`aura-std::TapLive`). The sweep/reduce
summary row, emitted at finalize and stamped with the instant of the last
contributing (warm) value — `first` alone pins the first contributing
instant; `min`/`max` deliberately do not carry the extremum's timestamp (a
whole-window row privileges no interior instant) — ratified as-is, #335;
live closures run inline
(`aura-std::TapLive`). The sweep/reduce
path never calls `bind_tap`.
The chain-pruning benefit — a sweep paying zero for the study wires behind an
@@ -67,6 +67,8 @@ for whom the binary is the only always-present teacher. Field evidence
execution semantics and document schemas by CAS forensics — the removed
failure class is exactly that forensic recovery.
Rendering of the carried texts on the help/introspection surfaces is #315;
the generated agent bootstrap card is #267; a fold introspection surface
is blocked on #310.
Rendering of the carried texts on the help/introspection surfaces shipped
with #315; the agent bootstrap card was retired as superseded (#267 — the
C29 surfaces themselves carry it); the fold introspection surface shipped
with #310 and renders the fold-registry roster — labels exactly as the
`--tap` selector accepts them, doc lines from the registry entries (#332).
+1 -1
View File
@@ -327,7 +327,7 @@ A named recorded stream produced by a recording `sink` — the addressable label
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 #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 CLI verbs (`aura run`, `aura measure`) pass a record-all plan, so their semantics are unchanged.
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).
### topology hash
**Avoid:** —
@@ -0,0 +1,15 @@
[
{"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": 6}}},
{"op": "add", "type": "SMA", "name": "px", "bind": {"length": {"I64": 1}}},
{"op": "feed", "role": "price", "into": ["fast.series", "slow.series", "px.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": "signal"},
{"op": "tap", "from": "px.value", "as": "price"},
{"op": "expose", "from": "bias.bias", "as": "bias"}
]
@@ -0,0 +1,18 @@
[
{"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": 6}}},
{"op": "feed", "role": "price", "into": ["fast.series", "slow.series"]},
{"op": "add", "type": "Gt", "name": "cross"},
{"op": "connect", "from": "fast.value", "to": "cross.a"},
{"op": "connect", "from": "slow.value", "to": "cross.b"},
{"op": "add", "type": "Sign", "name": "sgn"},
{"op": "add", "type": "Sub", "name": "sub"},
{"op": "connect", "from": "fast.value", "to": "sub.lhs"},
{"op": "connect", "from": "slow.value", "to": "sub.rhs"},
{"op": "connect", "from": "sub.value", "to": "sgn.value"},
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 1.0}}},
{"op": "connect", "from": "sgn.value", "to": "bias.signal"},
{"op": "tap", "from": "cross.value", "as": "above"},
{"op": "expose", "from": "bias.bias", "as": "bias"}
]
@@ -0,0 +1,16 @@
[
{"op": "doc", "text": "SMA crossover bias with two measurement taps for fold discovery"},
{"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": 6}}},
{"op": "add", "type": "SMA", "name": "px", "bind": {"length": {"I64": 1}}},
{"op": "feed", "role": "price", "into": ["fast.series", "slow.series", "px.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": "tap", "from": "px.value", "as": "price"},
{"op": "expose", "from": "bias.bias", "as": "bias"}
]
@@ -0,0 +1,12 @@
[
{"op": "source", "role": "price", "kind": "F64"},
{"op": "add", "type": "SMA", "name": "fast", "params": {"length": {"I64": 3}}},
{"op": "add", "type": "SMA", "name": "slow", "bind": {"length": {"I64": 6}}},
{"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"},
{"op": "connect", "from": "sub.value", "to": "bias.signal"},
{"op": "expose", "from": "bias.bias", "as": "bias"}
]
@@ -0,0 +1,16 @@
[
{"op": "doc", "text": "SMA crossover exposing three measurement taps to probe the skipped-tap note"},
{"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": 6}}},
{"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": 1.0}}},
{"op": "connect", "from": "sub.value", "to": "bias.signal"},
{"op": "tap", "from": "fast.value", "as": "fast_ma"},
{"op": "tap", "from": "slow.value", "as": "slow_ma"},
{"op": "tap", "from": "sub.value", "as": "spread"},
{"op": "expose", "from": "bias.bias", "as": "bias"}
]
+18
View File
@@ -0,0 +1,18 @@
# Milestone 36 fieldtest — "Self-description: every surface explains itself"
Empirical, binary-only check of the milestone promise: that a deployment-posture
agent with **only** the `aura` binary (no engine sources, no repo docs, no
hand-written bootstrap card) can bootstrap from the surfaces alone.
Every artefact here was produced touching nothing but the release binary
(`d26f0c8`) and what it emits (help text, `aura new` scaffold, introspection
output, error messages). One child dir per scenario; each README states the
property it protects. Full findings + verdict: `docs/specs/fieldtest-m1-self-description.md`.
| dir | axis | outcome |
|-----|------|---------|
| s1_bootstrap_author_validate | cold bootstrap → author → validate | green |
| s2_execution_pulse_idiom | execution semantics + pulse idiom | green (1 friction) |
| s3_vocabulary_breadth | vocabulary self-description breadth| pass (1 friction) |
| s4_document_ramp | document-first ramp from `{}` | green (2 gaps) |
| s5_trace_measurement | trace / measurement path | dead-end (1 bug, 1 gap) |
@@ -0,0 +1,33 @@
# s1 — cold bootstrap → author → validate
**Protected property:** starting from `aura --help` alone (no repo, no docs), a
downstream agent can scaffold a project, author a new strategy over the
discovered op-grammar vocabulary, and drive it to a green backtest — using only
what the binary emits.
## Trail (binary-only)
1. `aura --help` — states what aura is, the two-layer model, and the execution
model (bias in [-1,+1], held as target position, stop = R).
2. `aura new demo-strat` — scaffolds `Aura.toml`, `blueprints/signal.json`, and a
machine-written `CLAUDE.md` bootstrap card.
3. `aura graph build --help` — the op-list authoring grammar (one line of meaning
per op).
4. `aura graph introspect --vocabulary` / `--node <T>` — node types and port
schemas.
5. Author `ema_cross.oplist.json` (this dir); build it:
`aura graph build < ema_cross.oplist.json > blueprints/ema_cross.json`
6. Validate green:
- synthetic: `aura run blueprints/ema_cross.json` → exit 0 (18-cycle synthetic
stream → 0 trades; all-zero metrics, no note that the stream is tiny).
- real: `aura run blueprints/ema_cross.json --real EURUSD --from 1577836800000
--to 1583020800000` → 3944 trades, valid metrics JSON, exit 0.
`ema_cross.blueprint.json` is the built artefact `graph build` emitted.
## Notes
- The harness auto-wraps the bias with `sim-optimal+risk-executor` and reports R
metrics even though the blueprint emits only a bias — confirming the documented
execution model without any extra authoring.
- Minor friction: `graph build` has no op to set the blueprint name; it defaults
to `graph`, and that name leaks into the sweep axis namespace
(`graph.fast.length`).
@@ -0,0 +1 @@
{"format_version":1,"blueprint":{"name":"graph","doc":"EMA(5)/EMA(20) spread clamped to a directional bias","nodes":[{"primitive":{"type":"EMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":5}}]}},{"primitive":{"type":"EMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":20}}]}},{"primitive":{"type":"Sub","name":"spread"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.25}}]}}],"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,13 @@
[
{"op":"source","role":"price","kind":"F64"},
{"op":"add","type":"EMA","name":"fast","bind":{"length":{"I64":5}}},
{"op":"add","type":"EMA","name":"slow","bind":{"length":{"I64":20}}},
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
{"op":"add","type":"Sub","name":"spread"},
{"op":"connect","from":"fast.value","to":"spread.lhs"},
{"op":"connect","from":"slow.value","to":"spread.rhs"},
{"op":"add","type":"Bias","name":"bias","bind":{"scale":{"F64":0.25}}},
{"op":"connect","from":"spread.value","to":"bias.signal"},
{"op":"expose","from":"bias.bias","as":"bias"},
{"op":"doc","text":"EMA(5)/EMA(20) spread clamped to a directional bias"}
]
@@ -0,0 +1,32 @@
# s2 — execution semantics + one-shot/pulse idiom
**Protected property:** from the surfaces alone a consumer can learn (a) what the
strategy's output stream means, (b) how it is executed against a protective stop,
and (c) the signal-side idiom the surfaces recommend when a held one-shot / pulse
behaviour is wanted — and can author that idiom to a green run.
## What the surfaces say
- `aura --help`: "a strategy emits a bias in [-1,+1] per cycle, held as the
continuously-tracked target position; a protective stop defines the risk unit
R, and signal quality is measured in R."
- Node schemas make the execution layer concrete: `Bias` (clamp to [-1,+1]),
`FixedStop` (`price → stop_distance`, defines R), `PositionManagement`
(`bias + price + stop_distance + size → managed position in R`). The built-in
`run`/`sweep` harness supplies this risk executor automatically.
- The scaffolded `CLAUDE.md`: "Entry signals become held state via the signal-side
latch/edge-pulse idiom (see `aura graph introspect --vocabulary`)."
## Authored idioms (both build + validate green)
- `latch_hold.oplist.json` — momentary crossover events → held state via `Latch`
(`set = fast>slow`, `reset = slow>fast`), held level → `Bias`. Runs green
(2543 trades over EURUSD 2020-01..03).
- `edge_pulse.oplist.json` — the complementary one-shot pulse, built as the
first-difference of the latched level (`Delay(lag=1)` + `Sub`) fed to `Bias`
(+1 on entry edge, 1 on exit edge, 0 held). Builds + validates green.
## Friction recorded
The `CLAUDE.md` names the "latch/edge-pulse idiom" and points to
`graph introspect --vocabulary`, but that surface only lists node one-liners —
it lists `Latch` but no `edge`/`pulse` primitive and no composition recipe. The
pulse *is* constructible (Latch→Delay→Sub→Bias), but the consumer must infer the
first-difference recipe; the pointer resolves to a list, not a recipe.
@@ -0,0 +1 @@
{"format_version":1,"blueprint":{"name":"graph","doc":"edge-pulse: first-difference of the latched level, +1 on entry edge -1 on exit edge","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":5}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":20}}]}},{"primitive":{"type":"Gt","name":"cross_up"}},{"primitive":{"type":"Gt","name":"cross_dn"}},{"primitive":{"type":"Latch","name":"hold"}},{"primitive":{"type":"Delay","name":"prev","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"Sub","name":"edge"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":1.0}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":1,"to":3,"slot":0,"from_field":0},{"from":0,"to":3,"slot":1,"from_field":0},{"from":2,"to":4,"slot":0,"from_field":0},{"from":3,"to":4,"slot":1,"from_field":0},{"from":4,"to":5,"slot":0,"from_field":0},{"from":4,"to":6,"slot":0,"from_field":0},{"from":5,"to":6,"slot":1,"from_field":0},{"from":6,"to":7,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":7,"field":0,"name":"bias"}]}}
@@ -0,0 +1,24 @@
[
{"op":"source","role":"price","kind":"F64"},
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":5}}},
{"op":"add","type":"SMA","name":"slow","bind":{"length":{"I64":20}}},
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
{"op":"add","type":"Gt","name":"cross_up"},
{"op":"connect","from":"fast.value","to":"cross_up.a"},
{"op":"connect","from":"slow.value","to":"cross_up.b"},
{"op":"add","type":"Gt","name":"cross_dn"},
{"op":"connect","from":"slow.value","to":"cross_dn.a"},
{"op":"connect","from":"fast.value","to":"cross_dn.b"},
{"op":"add","type":"Latch","name":"hold"},
{"op":"connect","from":"cross_up.value","to":"hold.set"},
{"op":"connect","from":"cross_dn.value","to":"hold.reset"},
{"op":"add","type":"Delay","name":"prev","bind":{"lag":{"I64":1}}},
{"op":"connect","from":"hold.value","to":"prev.series"},
{"op":"add","type":"Sub","name":"edge"},
{"op":"connect","from":"hold.value","to":"edge.lhs"},
{"op":"connect","from":"prev.value","to":"edge.rhs"},
{"op":"add","type":"Bias","name":"bias","bind":{"scale":{"F64":1.0}}},
{"op":"connect","from":"edge.value","to":"bias.signal"},
{"op":"expose","from":"bias.bias","as":"bias"},
{"op":"doc","text":"edge-pulse: first-difference of the latched level, +1 on entry edge -1 on exit edge"}
]
@@ -0,0 +1 @@
{"format_version":1,"blueprint":{"name":"graph","doc":"latched long: set on fast>slow, reset on slow>fast, held as bias","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":5}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":20}}]}},{"primitive":{"type":"Gt","name":"cross_up"}},{"primitive":{"type":"Gt","name":"cross_dn"}},{"primitive":{"type":"Latch","name":"hold"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":1.0}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":1,"to":3,"slot":0,"from_field":0},{"from":0,"to":3,"slot":1,"from_field":0},{"from":2,"to":4,"slot":0,"from_field":0},{"from":3,"to":4,"slot":1,"from_field":0},{"from":4,"to":5,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":5,"field":0,"name":"bias"}]}}
@@ -0,0 +1,19 @@
[
{"op":"source","role":"price","kind":"F64"},
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":5}}},
{"op":"add","type":"SMA","name":"slow","bind":{"length":{"I64":20}}},
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
{"op":"add","type":"Gt","name":"cross_up"},
{"op":"connect","from":"fast.value","to":"cross_up.a"},
{"op":"connect","from":"slow.value","to":"cross_up.b"},
{"op":"add","type":"Gt","name":"cross_dn"},
{"op":"connect","from":"slow.value","to":"cross_dn.a"},
{"op":"connect","from":"fast.value","to":"cross_dn.b"},
{"op":"add","type":"Latch","name":"hold"},
{"op":"connect","from":"cross_up.value","to":"hold.set"},
{"op":"connect","from":"cross_dn.value","to":"hold.reset"},
{"op":"add","type":"Bias","name":"bias","bind":{"scale":{"F64":1.0}}},
{"op":"connect","from":"hold.value","to":"bias.signal"},
{"op":"expose","from":"bias.bias","as":"bias"},
{"op":"doc","text":"latched long: set on fast>slow, reset on slow>fast, held as bias"}
]
@@ -0,0 +1,22 @@
# s3 — vocabulary self-description breadth
**Protected property:** every closed-vocabulary entry class the binary exposes is
reachable and carries a non-empty one-line meaning, and no entry's meaning is a
byte-identical (non-discriminating) duplicate of a sibling in the same class.
`breadth_probe.sh` drives only `aura` introspection output; `breadth_probe.out`
is its captured result. Run with `AURA=<path-to-release-aura> bash breadth_probe.sh`.
## Result (see breadth_probe.out)
- 33/33 node types resolve via `graph introspect --node <T>` with a
`"<Name> — <meaning>"` schema. PASS.
- 7/7 folds, 6/6 process blocks, 6/6 campaign blocks, 17/17 metrics reachable and
carry a meaning (metrics also carry applicability tags). PASS.
- **One blemish:** three distinct node types — `CarryCost`, `ConstantCost`,
`VolSlippageCost` — share the byte-identical one-liner
*"cost-model node: charges its cost in R from position geometry, at close or
accrued per held cycle"*. From `--vocabulary` (the primary discovery surface)
a consumer cannot tell them apart; the distinguishing detail (the param
`carry_per_cycle` vs `cost_per_trade` vs `slip_vol_mult`, and the extra
`volatility` input) lives only in `--node` and in the campaign `cost` slot
prose. The one-liner exists (C29 satisfied) but is not entry-specific.
@@ -0,0 +1,45 @@
== node vocabulary: every node resolves via --node with a meaning ==
nodes probed: 33
== duplicate node one-liners (non-discriminating meanings) ==
x3 identical: cost-model node: charges its cost in R from position geometry, at close or accrued per held cycle
== folds carry a meaning ==
ok: Count
ok: Sum
ok: Mean
ok: Min
ok: Max
ok: First
ok: Last
== process blocks resolve via --block ==
ok: std::sweep
ok: std::gate
ok: std::walk_forward
ok: std::monte_carlo
ok: std::generalize
ok: std::grid
== campaign blocks resolve via --block ==
ok: std::data
ok: std::risk
ok: std::cost
ok: std::strategy
ok: std::process_ref
ok: std::presentation
== metrics carry an applicability tag + meaning ==
ok: expectancy_r
ok: win_rate
ok: avg_win_r
ok: avg_loss_r
ok: profit_factor
ok: max_r_drawdown
ok: sqn
ok: sqn_normalized
ok: net_expectancy_r
ok: n_trades
ok: n_open_at_end
ok: total_pips
ok: max_drawdown
ok: bias_sign_flips
ok: deflated_score
ok: overfit_probability
ok: neighbourhood_score
RESULT: fail=0
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Scenario 3 — vocabulary self-description breadth probe.
# Protected property: every closed-vocabulary entry class the binary exposes is
# REACHABLE and carries a non-empty one-line meaning; no entry's meaning is a
# byte-for-byte duplicate of another entry in the SAME class (a duplicate is a
# non-discriminating meaning).
# Binary-only: uses nothing but `aura` introspection output.
set -u
AURA="${AURA:?set AURA to the release binary path}"
fail=0
echo "== node vocabulary: every node resolves via --node with a meaning =="
nodes=$("$AURA" graph introspect --vocabulary | awk '{print $1}')
for n in $nodes; do
line=$("$AURA" graph introspect --node "$n" 2>&1 | head -1)
case "$line" in
"$n"*) : ;; # "<Name> — <meaning>"
*) echo " MISSING/BAD meaning for node $n: [$line]"; fail=1 ;;
esac
done
echo " nodes probed: $(echo "$nodes" | wc -w)"
echo "== duplicate node one-liners (non-discriminating meanings) =="
"$AURA" graph introspect --vocabulary \
| sed -E 's/^[A-Za-z0-9_]+ +//' \
| sort | uniq -c | sort -rn \
| awk '$1 > 1 { print " x"$1" identical: "substr($0, index($0,$2)) }'
echo "== folds carry a meaning =="
"$AURA" graph introspect --folds | awk 'NF==0{next} {print " ok: "$1}'
echo "== process blocks resolve via --block =="
for b in $("$AURA" process introspect --vocabulary | awk '{print $1}'); do
"$AURA" process introspect --block "$b" >/dev/null 2>&1 && echo " ok: $b" || { echo " BAD: $b"; fail=1; }
done
echo "== campaign blocks resolve via --block =="
for b in $("$AURA" campaign introspect --vocabulary | awk '{print $1}'); do
"$AURA" campaign introspect --block "$b" >/dev/null 2>&1 && echo " ok: $b" || { echo " BAD: $b"; fail=1; }
done
echo "== metrics carry an applicability tag + meaning =="
"$AURA" process introspect --metrics | awk '{print " ok: "$1}'
echo "RESULT: fail=$fail"
@@ -0,0 +1,38 @@
# s4 — document-first ramp (grow from `{}` to a runnable campaign)
**Protected property:** using only `--unwired` introspection and validation
diagnostics, a consumer can grow a process document and a campaign document from a
bare `{}` to a registered, runnable state, and run the campaign green.
## Trail (binary-only)
1. `echo '{}' > x.json; aura process introspect --unwired x.json` — lists the
whole envelope (`format_version`, `kind`, `name`, `pipeline`).
2. Grow `process.json`: `--unwired` on a doc with `pipeline:[]` says "a process
needs at least one stage"; a stage `{}` is told `pipeline[0].block (required,
block id)` → correct shape is `{"block":"std::sweep"}`.
`aura process validate` → valid; `aura process register` → content id.
3. Grow `campaign.json` similarly (`aura campaign introspect --unwired`,
`--block std::*`). `aura campaign validate` passes all three tiers
(intrinsic / referential / executable); `register` → content id;
`campaign run <id>` → 3 members, green.
`process.json` and `campaign.json` in this dir are the final registered forms;
`ema_open.oplist.json` is the strategy authored with an **open** `fast.length`
param (see friction 2).
## Frictions recorded
1. **`process.ref` shape under-documented.** `campaign introspect --block
std::process_ref` and `--unwired` both describe it as *"content id of a process
document"* (reads as a bare string), but the validator rejects the bare string:
`unknown variant '<id>', expected 'content_id' or 'identity_id'` — the tagged
`{"content_id": "<id>"}` form is required and appears nowhere in the
introspection. Recoverable only via the validate error.
2. **Two-layer axis-namespace seam.** `aura sweep <bp> --list-axes` presents
`graph.fast.length` as sweepable (overriding the *bound* default, #246), and
the sugar `aura sweep --axis graph.fast.length=…` accepts it. But the canonical
**document** form rejects that same name: `axis "graph.fast.length" is not in
the param space` — a campaign axis must be an **open** param, named **without**
the `graph.` prefix (`fast.length`, per `graph introspect --params`, which
prints *nothing* for a fully-bound blueprint). The name a consumer learns from
the sugar (`graph.fast.length`) is not the name the document layer wants
(`fast.length`), and no surface reconciles the two.
@@ -0,0 +1,10 @@
{
"format_version":1,
"kind":"campaign",
"name":"ema_axis_sweep",
"data":{"instruments":["EURUSD"],"windows":[{"from_ms":1577836800000,"to_ms":1583020800000}]},
"strategies":[{"ref":{"content_id":"2c94edd4e3a3253fa446f2700f5ada1cb4cb9ceafcd99494d8d8e5244e3148a6"},"axes":{"fast.length":{"kind":"I64","values":[3,5,8]}}}],
"process":{"ref":{"content_id":"b3c0f7548731f4078ac6f0ba4444781ff51b067b4dd25567a12c49423b5d63ef"}},
"seed":0,
"presentation":{"persist_taps":["r_equity"],"emit":["family_table"]}
}
@@ -0,0 +1,13 @@
[
{"op":"source","role":"price","kind":"F64"},
{"op":"add","type":"EMA","name":"fast"},
{"op":"add","type":"EMA","name":"slow","bind":{"length":{"I64":20}}},
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
{"op":"add","type":"Sub","name":"spread"},
{"op":"connect","from":"fast.value","to":"spread.lhs"},
{"op":"connect","from":"slow.value","to":"spread.rhs"},
{"op":"add","type":"Bias","name":"bias","bind":{"scale":{"F64":0.25}}},
{"op":"connect","from":"spread.value","to":"bias.signal"},
{"op":"expose","from":"bias.bias","as":"bias"},
{"op":"doc","text":"EMA(open fast)/EMA(20) spread bias, fast.length swept by campaign"}
]
@@ -0,0 +1 @@
{"format_version":1,"kind":"process","name":"minimal_sweep","pipeline":[{"block":"std::sweep"}]}
@@ -0,0 +1,39 @@
# s5 — trace / measurement path
**Protected property:** from the surfaces a consumer can discover which verbs
persist per-cycle traces and reach a signal-quality measurement — the milestone's
own headline being "signal quality is measured in R".
## What the surfaces say
`aura --help`: "Traces: `sweep --real --trace` / `walkforward --real --trace`
record per-cycle taps under runs/, consumed by `aura chart` and `aura measure`."
`aura measure ic --help`: "Information Coefficient of a **signal** tap against
forward returns of a **price** tap" — requires `--signal <SIGNAL>` and
`--price <PRICE>`.
## What actually happens
- Discovery works: `runs families` lists the traced families; `measure ic`'s
`<RUN>` is the member path **relative to** `runs/traces/`
(`<family>/<instr-window>/<param-combo>`); the missing-tap error is clear and
lists available taps. `measure ic` runs green on the persisted taps.
- **Dead-end for the intended inputs.** The only taps any surface-reachable verb
persists are the closed presentation set `equity` / `exposure` / `r_equity`
(`net_r_equity`) — execution-side R/pip curves, **not** the strategy's bias
signal nor the instrument price. So `measure ic` can only be fed, e.g.,
`--signal exposure --price equity`, which is not a signal-vs-price IC at all
(result ≈ noise, 0.0019).
- **Declared taps are silently dropped.** `graph build --help` documents the op
`tap` as *"declare a **recordable** tap on a wire"*. `tapped.oplist.json` (this
dir) declares `tap bias.bias as signal` and `tap fast.value as price`, yet after
`sweep --real --trace` the persisted set is still `["equity","exposure",
"r_equity"]`; both `measure ic --signal signal` and `chart --tap signal` report
the tap does not exist. There is no surface-documented path to persist a genuine
signal or price tap, so `measure ic`'s stated contract is unreachable end-to-end.
## Evidence (verbatim)
```
$ aura measure ic --signal signal --price price <tapped-run>
aura: run '…' has no tap 'signal' (taps: ["equity", "exposure", "r_equity"])
$ aura chart 010f8afa-0 --tap signal
aura: no family member has a tap named 'signal' (exit 0)
```
@@ -0,0 +1,15 @@
[
{"op":"source","role":"price","kind":"F64"},
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":5}}},
{"op":"add","type":"SMA","name":"slow","bind":{"length":{"I64":20}}},
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
{"op":"add","type":"Sub","name":"spread"},
{"op":"connect","from":"fast.value","to":"spread.lhs"},
{"op":"connect","from":"slow.value","to":"spread.rhs"},
{"op":"add","type":"Bias","name":"bias","bind":{"scale":{"F64":0.25}}},
{"op":"connect","from":"spread.value","to":"bias.signal"},
{"op":"tap","from":"bias.bias","as":"signal"},
{"op":"tap","from":"fast.value","as":"price"},
{"op":"expose","from":"bias.bias","as":"bias"},
{"op":"doc","text":"tapped: bias exposed and tapped as signal, fast tapped as price"}
]