Commit Graph

260 Commits

Author SHA1 Message Date
Brummel aeb0366aed feat(cli): aura campaign run — the executor verb over the MemberRunner driver (0107 tasks 8-9)
campaign_run.rs: target resolution (file is register-then-run sugar;
bare 64-hex is the canonical id — #198 decision 1), project gate before
any store write, zero-fault referential gate, process fetch + v1
preflight, then aura_campaign::execute with the CLI MemberRunner over
the shipped loaded-blueprint convention: per-member blueprint reload,
reduce-mode wrap, unique suffix-join of raw campaign axis names onto
the wrapped param_space, windowed real data via the shipped ms->ns
source seam (absent archive/zero-bar windows -> NoData). Emission:
family_table / selection_report lines gated on the doc's emit list
(names debug_asserted against emit_vocabulary), the campaign_run
record line always, persist_taps deferred LOUDLY on stderr before
execution (F7 lesson), zero-survivor cells noted on stderr with exit 0.
exec_fault_prose/member_fault_prose keep the Debug-free house seam.

Seam tests: 8 campaign_run_* e2e tests incl. outside-project, bogus
target, unknown id, v1-boundary (mc process registers fine, run
refuses), persist_taps ordering, and the gated real-data
sweep->gate->walkforward e2e — which ran its full assert path on this
host (local GER40 2024-09 archive) rather than the data-less skip.
Also: exec.rs campaign-id guard tightened to lowercase hex, matching
is_content_id and the store's self-keyed form (review nit).

Gates: workspace 975/0, clippy -D warnings clean.

refs #198
2026-07-03 21:33:29 +02:00
Brummel b15ad07208 feat(campaign): execute — cell loop, sweep/gate stages, walk-forward stage (0107 tasks 6-7)
The execution core: cells iterate strategy x instrument x window in doc
order (sequential — C1 keeps parallelism across sims); the sweep stage
enumerates the axes odometer (BTreeMap order, last axis fastest) through
the engine sweep over a ListSpace with lowest-index-deterministic fault
capture; selection dispatches optimize / optimize_plateau /
optimize_deflated (deflation nulls seeded from the campaign doc's seed);
gates filter per-member via member_metric (metrics.r == None fails an R
predicate, conservative); an empty survivor set truncates the cell's
realization and the run continues (exit-0 semantics at the caller). The
walk-forward stage rolls the declared cell window in ms (WfMode ->
RollMode), IS-sweeps ONLY the survivor points per window, stamps
manifest.selection on OOS winner reports, and appends a WalkForward
family; realization = CampaignRunRecord over untouched family records.

The loop's task-7 BLOCKED was review-exhaustion on the task-6 stub
wording the plan itself prescribed (task 7 deletes that stub); a fresh
post-loop quality review on the final diff returned one Minor — the
campaign_id prefix slice could panic on a malformed id — fixed as a
typed PipelineShape refusal + test (execute_refuses_malformed_campaign_id).
Known and accepted: a cross-cell member fault leaves earlier cells'
families as unreferenced records in the append-only store; nested
window/member parallelism can oversubscribe cores on large campaigns.

Gates: aura-campaign 29/0, workspace 961+/0, clippy -D warnings clean.

refs #198
2026-07-03 20:53:18 +02:00
Brummel 3a4f1c4597 feat(campaign): aura-campaign crate — types, member_metric, preflight (0107 tasks 4-5)
New leaf library crate: the campaign-execution semantics home (#198
home decision — reusable beyond the CLI; NOT C21's project-side
World). CellSpec + the MemberRunner seam (the only thing a consumer
implements), MemberFault/ExecFault (Display-free, prose at the binary
seam), member_metric over the 14 per-member scalars (third roster
site, refs #190 — drift fails safe as a preflight refusal), and
preflight: v1 pipeline shape std::sweep [std::gate]* [std::walk_forward],
rankable-metric and gate-metric rosters, plateau-in-wf and
deflate+plateau refusals, i64-fit guard on wf lengths — all before any
member runs.

Post-loop: the held quality finding (untested i64-fit arm) verified
real and closed by hand — preflight_refuses_wf_length_exceeding_i64.

Gates: aura-campaign 14/0, clippy -D warnings clean.

refs #198
2026-07-03 19:59:21 +02:00
Brummel b43def7d75 feat(engine,registry): ListSpace + campaign-run realization store (0107 tasks 2-3)
aura-engine: ListSpace — explicit point set implementing Space beside
GridSpace/RandomSpace (a gate's survivor subset has no cartesian
structure); GridSpace-identical arity/kind validation reusing the
existing SweepError variants (KindMismatch.value_index carries the
point ordinal for a list); empty point list is valid and sweeps to an
empty family; re-exported at the crate root.

aura-registry: CampaignRunRecord/CellRealization/StageRealization/
StageSelection — the thin campaign-level linking record over untouched
family records (#198 decision 4) — with append_campaign_run (assigns
run via next_campaign_run, a parallel counter beside next_run) and
load_campaign_runs (missing file => empty) on the campaign_runs.jsonl
sibling store; blueprint_path and find_blueprint_by_identity promoted
to pub (the campaign executor resolves the same refs the referential
tier validates).

Gates: engine 290/0, registry 54/0, clippy -D warnings clean.

refs #198
2026-07-03 19:34:08 +02:00
Brummel 67aff34923 feat(research): std::walk_forward vocabulary corrected to machinery-true fields (0107 task 1)
folds/in_sample_bars/out_of_sample_bars mapped to nothing
WindowRoller::new accepts; the block now carries in_sample_ms/
out_of_sample_ms/step_ms/mode (WfMode: rolling|anchored) — the
roller's three lengths in the campaign window's own epoch-ms unit
plus both shipped RollModes (#198 decision 2). New intrinsic fault
ZeroWalkForwardLength (path-addressed) refuses zero lengths at the
doc tier, earlier than the roller's NonPositiveLength. Golden
canonical pin, PROCESS_FIXTURE, introspection tables, and the
aura-cli prose/fixture twins moved together; fieldtest corpus
untouched (historical record — stored walk_forward docs get new
content ids by design).

Gates: aura-research 14/14, aura-cli green, workspace build clean.

refs #198
2026-07-03 19:17:54 +02:00
Brummel e8f919d956 fix(research): process_ref describe advertises content-id only
Fieldtest finding F2 (cycle-0106 corpus): aura campaign introspect --block
std::process_ref rendered the generic dual-form ref label ('one of
{ content_id } | { identity_id }') while validate_campaign refuses an
identity_id process ref — the authoring guide offered a value the
validator rejects. The --unwired hint was narrowed in the 0106 audit; this
narrows the parallel describe path: std::process_ref's slot moves to a
distinct content-only SlotKind::ContentRef whose label names only the
content-id form. Strategy refs keep the dual-form label (both ids are
valid there) — pinned by the same RED test.

RED-first: tests::process_ref_describe_advertises_content_id_only failed
on the shared-label tree, passes now; workspace 917/0, clippy clean, doc
0 warnings.

refs #189
2026-07-03 16:58:25 +02:00
Brummel ebe85683c9 audit: cycle 0106 tidy — ledger/glossary lift, unwired-hint fix, spec/plan retired
Architect drift review (holds: C14 headless authoring, C16/C17 no-DSL with
aura-research a true leaf, content-id move behaviour-preserving per the
untouched id goldens). Resolution per item:

- fix (docs): C18 gains the cycle-0106 realization note (processes/ +
  campaigns/ stores, document canonical form, the content-id primitive's
  library home, the referential tier, no-executor status); the #109
  analysis-meta-level open thread records the artifact half shipped v1 and
  what stays open (executor question, verb dissolution 'once it carries',
  the remaining #188 amendment package). Glossary: new campaign document +
  process document entries; content id generalized to all three artifact
  classes; experiment marked superseded by the campaign document (#188
  re-cut of the builder-API thread).
- fix (code): the --unwired guide advertised an identity_id process ref
  that validate_campaign refuses — the process.ref open-slot hint now says
  content-id-only (strategy refs keep both); lib + seam pins updated.
- filed forward: #190 (metric-vocabulary single-sourcing, the 0105 roster
  drift class), #191 (identity-ref store scan is O(store), performance
  only).
- carry-on: regression gates green by hand — cargo test --workspace 916/0,
  clippy -D warnings clean, cargo doc --no-deps 0 warnings; no baseline to
  ratify (project has no metric baseline scripts).

Cycle 0106 spec and plan retired per convention (git rm); durable record
is the ledger notes above plus #188/#189.

refs #189
2026-07-03 16:27:02 +02:00
Brummel a9e047eca7 feat(cli): 0106 tasks 11-12 — aura campaign verb family + workspace gates
The campaign half of research_docs.rs: validate (intrinsic always;
referential tier when a project env is discovered, with the explicit
skip line outside one), introspect (--vocabulary/--block/--unwired/
--content-id, same exactly-one-of usage guard), register via the
registry's campaign_path accessor, and RefFault prose (Display-free,
unit-pinned). Five binary seam tests (skip line, empty-axis prose +
exit 1, section vocabulary, spec-example open slots, content-addressed
registration) reuse the file's temp_cwd helper — the plan's
tempfile::tempdir() snippet deviated from the crate's zero-extra-dep
test convention and was adapted, disclosed in the loop's report.

The referential CLI branch inside a real loadable project stays
fieldtest territory by the plan's own scope note (project::load needs a
built dylib); its logic is pinned by the aura-registry tests.

Gates: cargo test --workspace 916/0 (885 pre-cycle + 31 new), clippy -D
warnings clean, cargo doc --no-deps 0 warnings. The worked spec example
runs end-to-end headless: author both documents as text, validate with
per-tier report lines, introspect vocabularies and open slots, obtain
stable content ids, register into the store — no Rust compile, no run
executed.

closes #189
2026-07-03 16:18:22 +02:00
Brummel db09e5de52 feat(cli): 0106 task 10 — aura process verb family (validate/introspect/register)
New research_docs.rs module: role-addressed process verbs with house-style
fault prose (Display-free enums phrased at the seam), the introspect
exactly-one-of usage guard (exit 2), and content-addressed registration.
Seven binary seam tests pin intrinsic-ok/refusal prose, vocabulary/block/
unwired/content-id introspection, exit codes, and the stored file's
location under the runs root.

Quality-gate finding folded in (and back into the plan): register no
longer re-derives the store path from runs_root — aura-registry now
exposes process_path/campaign_path, the same single mapping its put/get
pair routes through, so the printed path cannot drift from the store
layout.

Verification: research_docs seam tests 7/0, registry 49/0, clippy clean.

refs #189
2026-07-03 15:36:47 +02:00
Brummel ef3bec5844 feat(research): 0106 tasks 1-9 — document layer, content-id move, stores + referential tier
The aura-research leaf crate: ProcessDoc/CampaignDoc with strict parsing
(hand-rolled StageBlock deserializer — deny_unknown_fields does not compose
with internally-tagged enums; Axis declares its ScalarKind once with bare
values, per the #189 user veto), canonical form + content_id_of (the
SHA-256 primitive moved from aura-cli, which now delegates — the id pin set
incl. the independent sweep-store recompute stayed green untouched),
intrinsic validation for both document types (P1 constraints structural),
and the introspection contract (schema tables single-source parse
strictness AND vocabulary/describe/open-slots — the Blockly litmus made
checkable). aura-registry: content-addressed processes/ + campaigns/
stores on the blueprint-store pattern (Ok(None) treat-as-empty) and the
referential tier (validate_campaign_refs: process/strategy refs incl.
identity-scan, axis-name + declared-kind checks against param_space).

Plan deviations, verified by hand and folded back into the plan file:
- Task 9's fixture could not use aura-composites as planned: every shipped
  composite with an open param routes through LinComb, which the zero-arg
  std_vocabulary roster deliberately excludes, so none round-trips through
  blueprint_from_json with the by-type-name resolver. The test hand-builds
  a minimal Bias composite instead (generic over param_space, as planned);
  the unused pro-forma aura-composites dev-dep from the repair pass is
  dropped. This is why the loop reported task 9 BLOCKED
  (review-loop-exhausted on the literal code block); the tree itself is
  complete and green.
- A plan-verbatim test assertion false-matched ("deflate" as substring of
  "deflated-positive"); tightened to the JSON key.
- OpenSlot doc comment backticked (rustdoc read strategies[0] as a link;
  doc gate back to 0 warnings).

Verification: cargo test --workspace 898/0; clippy -D warnings clean;
cargo doc --no-deps 0 warnings. Tasks 10-12 (CLI verb families, final
gates) follow.

refs #189
2026-07-03 15:09:39 +02:00
Brummel bd32c4fcb3 audit: cycle 0105 tidy (drift-clean after two doc-mirror alignments)
Architect drift review (range 7b429f9..57f401f): drift_found (low/medium
only), resolved inline as close-fixes:
- crates/aura-std/src/vocabulary.rs — the roster-site doc now names BOTH
  count pins a node addition trips (the in-crate shape test and aura-cli's
  cross-boundary --vocabulary count e2e); the singular phrasing understated
  the cross-crate lockstep the extra e2e introduced.
- docs/design/INDEX.md — the C24 enforcement-shift passage gains the 0105
  delivery note: resolver-vs-list drift closed by construction via the
  roster macro; the un-rostered-node residual stays fail-safe.

Noted, no action: spec/plan predicted 884 (no new test) but delivery added
one e2e (885) — consciously superseded, disclosed in the iter commit body;
the artifacts are ephemeral and removed below.

What holds (architect): byte-preserving refactor confirmed against the
diff (22 keys/order exact, signatures and module doc untouched, every
consumer unchanged); invariant 9 / C24 preserved (compile-time expansion,
no registry); the new e2e count pin is genuinely roster-tied.

Regression gates (all green): cargo build clean; cargo test --workspace
885 passed / 0 failed (884 baseline + 1 new e2e); clippy --all-targets
-D warnings clean; cargo doc --no-deps 0 warnings.

Ephemeral cycle artifacts removed per project convention (git rm):
docs/specs/0105-std-vocabulary-roster-macro.md,
docs/plans/0105-std-vocabulary-roster-macro.md.

refs #180
2026-07-02 22:00:36 +02:00
Brummel 57f401f2ab refactor(std): std_vocabulary_roster! macro — one source for the vocabulary roster
The three hand-kept copies of the 22-key zero-arg roster in
aura-std/vocabulary.rs (std_vocabulary match arms, std_vocabulary_types
list, the test's inline array) collapse into one private declarative
macro invoked once with the "TypeId" => Type pairs — the resolver and
the enumerable list now agree by construction (the #160 failure mode:
a node resolvable but silently absent from graph introspect
--vocabulary, or vice versa). Byte-preserving: same fn signatures, same
22 ids, same order; lib.rs re-export and every consumer untouched; the
vocabulary stays a closed compiled-in set (invariant 9 / C24 — a
compile-time expansion, no registry). Adding a zero-arg node is now one
roster line plus the conscious count-pin bumps.

Tests: the two unit tests reshaped (round-trip iterates the generated
list with PrimitiveBuilder::label() as the independent oracle; the shape
test keeps the count pin as deliberate friction). The E2E phase added
one cross-boundary pin beyond the plan — kept:
graph_introspect_vocabulary_lists_exactly_the_closed_roster_count checks
the same count through Env::type_ids() and the CLI print loop across the
real process boundary, which no roster-internal test can see.

Accepted residual (documented at the roster site, recorded on #160):
a new zero-arg node never rostered at all stays unguarded — no
enumeration of zero-arg builders exists — and fails safe in both
directions (clean UnknownNodeType on load; merely absent from
--vocabulary).

Verification: cargo build clean; cargo test --workspace 885 passed /
0 failed (884 baseline + 1 new e2e); clippy --all-targets -D warnings
clean; cargo doc --no-deps 0 warnings.

closes #160, refs #180
2026-07-02 21:56:07 +02:00
Brummel 45fb06dba3 feat(engine+cli): topology-identity hash — --identity-id beside --content-id
Engine (aura-engine/blueprint_serde): blueprint_to_json factored into
build_doc/serialize_doc (byte-preserving — canonical golden unchanged) and
blueprint_identity_json added: the canonical document with every
non-load-bearing debug symbol (C23) blanked (composite name, instance
names, bound-param names, role names, output re-export names) while
everything load-bearing survives (type ids, node order, edges, role
targets/order, output pairs/order, bound pos/kind/value — openness stays
identity-bearing). Re-exported from lib.rs. Six property tests: renamed
twins (equal identity, unequal canonical), open-vs-bound, bound-value,
edge-swap, nested-composite interior names, role/output renames — the
last two are additive beyond the plan and cover the recursion and
role/output arms the planned four did not.

CLI (aura-cli): graph introspect gains --identity-id, a sibling of
--content-id through the same shared content_id SHA-256 primitive;
composite_from_str factored out of build_from_str (fault strings
byte-identical). The exactly-one introspect dispatch is DELIBERATELY
relaxed: the two id flags form one group and may combine — one build,
both ids, one per line, content id first. In-crate cross-path twin test
(Rust sma_signal vs op-script twin: distinct content ids, one identity
id) beside the existing cross-surface pins; four e2e tests incl. the
previously uncovered count!=1 usage exit-2 path.

topology_hash, the blueprint store, reproduce, and every --content-id
byte stay untouched (spec acceptance 3; all existing pins green).

Verification: cargo build clean; cargo test --workspace 884 passed /
0 failed (873 baseline + 11 new); clippy -D warnings clean; doc build
0 warnings. The implement-loop's Task-4 spec-compliance block was a
plan-byte count mismatch only (plan under-counted pre-existing
blueprint_serde tests 6-vs-7 and did not anticipate the two
sibling-accepted extra tests); gates re-run by hand, all green.

closes #171, refs #180
2026-07-02 21:24:18 +02:00
Brummel 560f594902 feat(cli): aura new — CLI wiring, load-bypass guard, authoring-loop e2e (0103 tasks 2-4)
Command::New + NewCmd + dispatch_new (usage faults exit 2, runtime
refusals exit 1, the C14 partition); the main() guard exempts aura new
from the eager project load so scaffolding works inside unbuilt trees.

E2e (tests/project_new.rs, 6 tests): scaffold -> cargo build ->
aura run twice byte-identical with manifest.project.namespace stamped;
introspect lists the project type beside std; refusals for existing
destination (1), invalid names (2), missing engine path with
--engine-path hint (1); aura new inside an unbuilt project tree
succeeds (the bypass proof); namespace override reaches the built
vocabulary (an additive e2e beyond the plan, kept).

Verification: cargo build --workspace clean; cargo test --workspace
873 passed / 0 failed; clippy -D warnings clean.

refs #180
2026-07-02 19:18:44 +02:00
Brummel 01500cba5e feat(cli): aura new scaffold module — validation, templates, emit (0103 task 1)
Token-replace templates (Cargo.toml/Aura.toml/.gitignore/lib.rs/
signal.json/CLAUDE.md) parameterized by name/namespace/engine root;
argv validation (usage exit-2 class), refusals before first write
(existing destination, missing engine root with --engine-path hint),
best-effort git init. Unit tests: validation table, snake derivation,
engine-root default, token-free rendering, refusal pair — 5/5 green.
Verified against the fixed plan (derive(Debug) — see the plan-fix
commit); CLI wiring follows as task 2.

refs #180
2026-07-02 18:44:58 +02:00
Brummel 4928e289f7 feat(project): the project-as-crate load boundary (cycle 0102)
A research project is now a loadable external cdylib crate. Inside a
directory whose ancestry holds an Aura.toml, aura discovers the project
root cargo-style, locates the compiled dylib via cargo metadata (debug
default, --release opt-in), loads it load-and-hold, and refuses
mismatches before trusting anything: the AURA_PROJECT descriptor
(aura-core::project, #[repr(C)]) carries a C-ABI stamp prefix (rustc +
aura-core version, baked per consuming build by the new aura-core
build.rs) validated before any Rust-ABI field is read. The vocabulary
charter gates the merged resolution: project type ids are ::-namespaced
(std stays bare), duplicates refuse, and the enumerable type-id list
must agree with the resolver, so introspection can never silently omit
a project type.

All blueprint verbs resolve through the merged project + std vocabulary
via a per-invocation Env threaded through the dispatch chains;
registry, trace-store, and data paths anchor at the project runs root
(Aura.toml [paths], paths-only by design — instrument geometry stays
the recorded sidecar, C15). RunManifest gains the Tier-1 project
provenance field (namespace + dylib sha256 + best-effort commit),
stamped beside topology_hash on the blueprint-run paths; pre-0102
registry lines load unchanged. Default node names strip the namespace,
so :: never reaches the param-path address space.

Proven by the demo-project fixture (built by the e2e via cargo,
path-dep on this workspace): run twice bit-identical, provenance
recorded, introspection lists demo::* beside std, registry anchors at
the discovered root from a subdirectory; the badcharter fixture proves
the charter refusal through the real libloading path; a never-built
project refuses with a cargo-build hint. Outside a project every path
collapses to the previous literals — goldens and manifest pins
byte-identical.

Verification: cargo build --workspace clean; cargo test --workspace 862
passed / 0 failed (incl. 7 project_load e2e); clippy -D warnings clean
(one precedent-matching allow(too_many_arguments) on run_oos_blueprint,
whose arity the Env threading raised to 8); doc build unchanged.
Docs/ledger aligned: Aura.toml field lists are paths-only in
project-layout.md, glossary, C16/C17; new C13 realization note records
the per-invocation-reload reading and the load-and-hold one-shot scope
boundary.

New deps, per-case review (aura-cli leaf binary only, never the frozen
artifact): libloading, toml.

refs #180
2026-07-02 18:13:37 +02:00
Brummel 9402afe958 polish(cli): normalize usage-message casing to the clap house style
Every hand-rolled usage line now reads 'Usage: aura <verb> ...' — capital
prefix plus the program name, the shape clap itself and the built-in run
branch already spoke. 10 literal edits: 6 lowercase 'usage:' flips (graph
introspect, run blueprint x2, runs family, mc blueprint, mc built-in), 4 bare
closures gain the prefix (sweep/walkforward, blueprint + built-in branches);
the mc built-in line names the program in both |-separated alternatives.
Refusal diagnostics, the conforming run_args_from literal, clap output, and
the fieldtests corpus stay byte-identical; exit codes keep the usage=2 /
runtime=1 partition.

Lockstep pin: the single casing-sensitive assertion (mc --real) flips to
capital 'Usage'. Three additive regression tests pin the house style itself
(bare-closure prefix, both mc alternatives, mc blueprint branch) — the
pre-existing pins checked only verb tokens / a single Usage occurrence;
deviation from the spec's no-new-test-bytes wording recorded on the issue.

Verification: cargo test --workspace rc=0 (cli_run 133 passed), clippy -D
warnings clean, grep gates exact (zero lowercase 'usage:' literals; 11
'Usage: aura' source lines), smoke 'aura sweep --strategy bogus' prints the
house-style line and exits 2.

closes #179
2026-07-02 12:56:23 +02:00
Brummel 4de6d5cbad rename: retire the stage1-* family for the r-family (r-sma / r-breakout / r-meanrev)
The stage1 ordinal has been dead vocabulary since the C10 reframe dropped
the two-stage research model: a 1 structurally implies a 2 that no longer
exists. The family is renamed by its live discriminator - the R yardstick -
with members named by their signal, uniform with their signal-named
siblings (sma, macd, momentum):

- selectors: stage1-r -> r-sma, stage1-breakout -> r-breakout,
  stage1-meanrev -> r-meanrev (old tokens are usage errors, exit 2 - no
  silent alias)
- identifiers: Strategy::RSma/RBreakout/RMeanRev, HarnessKind::RSma,
  r_sma_*/r_breakout_*/r_meanrev_*, wrap_r, run_signal_r, RGrid, R_SMA_*
- persisted identity: the sma_signal composite (param prefix
  sma_signal.fast.length / .slow.length; fixtures regenerated; content-ids
  shift - no test pins a literal hash, the registry parses no record names)
- e2e test files git-mv'd to r_sma_e2e / r_breakout_e2e / r_meanrev_e2e
- dead Stage-1/Stage-2 prose reworded to post-C10 vocabulary across
  rustdoc, Cargo.tomls, ledger live lines, and the glossary (historical
  entries stay; fieldtests corpus untouched)
- CLAUDE.md invariant 7 rewritten to record the ratified C10 reframe
  faithfully (the token-swap alone would have laundered the retired
  gated-currency/realistic-broker design into unmarked live prose); the
  unbacked account-mode clause dropped

Verification: cargo build/test --workspace green (51 targets), clippy
-D warnings clean, doc build clean, acceptance grep gate leaves exactly
the one resampling-stage false positive (harness.rs), smoke: --harness
r-sma runs, --harness stage1-r exits 2 with the new usage line.

Decision log: forks and rationale recorded on the issue (reconciliation
+ implementation-phase comments).

closes #174
2026-07-02 12:03:09 +02:00
Brummel 88e9c9a539 fix(cli): symmetric sweep/mc family-verb polish (closed error, mc seed label, mandatory-knob docs)
Three polish items surfaced by the World/C21 milestone fieldtest:

- A sweep of a fully-bound (closed) blueprint is refused up front with a clear
  "this blueprint is fully bound; nothing to sweep" message — the symmetric
  inverse of blueprint_mc_family's closed-blueprint guard — instead of leaking a
  misleading UnknownKnob for a knob that is not unknown but bound out.
  blueprint_sweep_family's error contract widens to String; the sweep terminal's
  BindError (a genuine unknown/kind-mismatch axis) still surfaces verbatim.
- mc reproduce lines now carry each member's seed=<N> label. Monte-Carlo members
  hold no tuning params, so the member label was blank; the seed is each draw's
  realization identity. Sweep / walk-forward labels (which echo params) unchanged.
- docs (design ledger + glossary): state that every open knob --list-axes prints
  is mandatory on sweep / walkforward — a subset is refused with the missing knob
  named (BindError::MissingKnob), there is no default; pin a knob you do not want
  to vary with a single-value axis.

RED-first for the two behaviour items. One existing unknown-axis E2E is re-pointed
from the closed fixture (which now hits the new "nothing to sweep" pre-check) to
the open fixture; it still exercises UnknownKnob("nope") specifically — resolve
raises UnknownKnob in phase 1 before MissingKnob in phase 2 — so the property is
preserved, not weakened.

closes #178
2026-07-02 09:41:54 +02:00
Brummel 8b2defa653 fix(cli): reject a silent-vacuous blueprint Monte-Carlo instead of returning it
`aura mc <bp.json>` builds a per-seed synthetic price walk with a fixed 60-bar
SyntheticSpec. A deeper-lookback loaded blueprint warms poorly over that walk, so
every seed can collapse to a bit-identical draw and the family was returned as an
Ok result — a single point masquerading as a distribution, a wrong result with no
error. Guard it: with >= 2 seeds, if every draw's metrics equal the first draw's,
refuse with a named error (C10 refuse-don't-guess), rather than auto-sizing the
walk (which would have to guess a warm-up depth the harness does not surface).

Compares `report.metrics`, not the whole RunReport — the manifest seed differs per
draw by construction, so a whole-report compare could never detect the collapse.

RED-first; the existing `blueprint_mc_family_seeds_differ` pins that a legitimate
(differing-draw) MC is not falsely rejected. Interim guard only — the real-data
moving-block-bootstrap headline stays gated on the DataServer seam.

refs #172
2026-07-02 09:41:54 +02:00
Brummel e20dd9ae49 Merge main into the clap-adoption cycle — re-integrate the two parallel CLI fixes
main advanced with two `fix(cli)` commits (dec0780, 6a775f7) landed by a parallel
session while the clap adoption (cycles 0098/0099) was built from 780d823. Both
touch behaviour the clap migration rewrote or sits beside; this merge preserves
them rather than letting the rewrite silently drop them:

- dec0780 (open-blueprint guard, #176): `aura run <open blueprint.json>` must
  refuse with a clean exit 2, never a `compile_with_params` arity panic (exit
  101). The fix lived in the old hand-rolled `run` .json-arm, which the clap
  migration replaced — re-integrated into `dispatch_run`'s .json branch (the same
  `blueprint_axis_probe().param_space()` check + "closed blueprint" diagnostic).
- 6a775f7 (walkforward axis-once, #177): axis validation hoisted to a single
  dispatch-boundary pre-flight, in the execution fn `blueprint_walkforward_family`
  — which the migration did not touch, so it auto-merged unchanged.

Both fixes are verified by their own RED tests
(`aura_run_rejects_an_open_blueprint_without_panicking`,
`aura_walkforward_emits_an_unknown_axis_rejection_once`), and both are consistent
with this cycle's iteration-2 exit-code classification: an open blueprint and an
unknown axis are argument faults, so usage errors → exit 2.

The conflict was confined to `fn main` in crates/aura-cli/src/main.rs (the clap
`Cli::parse()` dispatch vs the old argv `match`); resolved to the clap side plus
the re-integrated guard. cli_run.rs auto-merged (their two new tests + this
cycle's renegotiated pins).

Verified green (orchestrator, this session): cargo build --workspace, cargo clippy
--workspace --all-targets -- -D warnings, and cargo test --workspace all clean.
2026-07-02 00:53:54 +02:00
Brummel fa0d30c0fd tweak(aura-cli): understate the --help about-line (drop the game-engine framing) 2026-07-02 00:40:06 +02:00
Brummel fa42bf3878 feat(0099): CLI exit-code split (iteration 2) — usage=2, runtime=1
Apply the clean exit-code partition (#175 deviation #8) on top of the clap
migration: runtime failures now exit 1, usage errors stay exit 2, with no
same-class inconsistency.

Partition (attribution principle): exit 2 = a command-line fault (a bad flag
value, an inapplicable combination, or the content of an argv-named file);
exit 1 = the command was well-formed but the environment / recorded state it
needs is missing, or piped stdin data is bad. The 6 boundary cases the spec's
"parse-time vs run-time" rule under-specified are resolved on #175
(malformed/open argv blueprint -> usage 2; stdin op-script content -> runtime 1;
applicability refusals -> usage 2; unknown --axis / --metric -> usage 2).

Flipped 2->1 (runtime): no local data, no recorded geometry, no recorded
run/family, "run has no tap named", trace-name collision, family/trace
persist-write failures (normalizing the lone generalize inconsistency -- all
persist -> 1 together), missing/corrupt content-addressed store state (the
reproduce path), chart-read failures, and graph stdin op-script content +
stdin-read I/O. reproduce-diverged stays exit 1. Usage sites (clap parse,
argv-applicability guards, the dual-grammar blueprint-file read/parse) stay
exit 2.

Tests: ~8 domain-refusal pins flipped Some(2)->Some(1) (message substrings
unchanged), the four runtime tests renamed _exit_2 -> _exit_1, and a partition
property test added. Orchestrator fix after the loop: 3 sibling no-data
skip-guards (run_real_with_trace + the two generalize cross-instrument tests)
that the plan's Task-1 list missed -- left at Some(2), they would fall through
to assert Some(0) and FAIL on a data-less machine; flipped to Some(1) for
consistency with the flipped code.

Deferred (cosmetic, filed forward): full "Usage:" / "usage:" / bare
error-message casing normalization -- no functional impact, high pin-churn.

Verified green (orchestrator, this session): cargo build --workspace, cargo
clippy --workspace --all-targets -- -D warnings, and cargo test --workspace all
clean (0 failed across every test binary).

This completes the #175 clap-adoption cycle (iteration 1 = clap migration at
366170a; iteration 2 = this exit-code split).

refs #175
2026-07-01 20:05:42 +02:00
Brummel 366170aeab feat(0098): CLI clap migration (iteration 1) — scoped help, --version, GNU flags
Replace the hand-rolled aura-cli argv parser with a clap derive parser.
Delivers, all from one declarative source:
- scoped `aura <sub> --help` (stdout, exit 0) — each subcommand's own Options
  section; retires the #131 uniform-global-blob help
- `--version` / `-V` → `aura 0.1.0` (stdout, exit 0)
- GNU `--flag=value`, the `--` end-of-options terminator, and long-option
  abbreviation (`--harn` → `--harness`)

Architecture: one root Cli/Command enum, one `*Cmd` struct per subcommand. The
four dual-grammar subcommands (run/sweep/walkforward/mc) map the loaded-blueprint
branch as an optional `[blueprint]` positional + a post-parse `is_file()`
dispatch on a single-sourced predicate; each built-in handler re-asserts a
stray-positional guard (refuse-don't-guess) so clap's optional positional cannot
silently swallow a typo (pinned by two new E2E tests). The execution layer
(run_*/emit_*/runs_*/reproduce_*) is untouched — only arg-plumbing changes, via
thin `*_from` adapters reusing the existing value helpers.

Exit codes behaviour-preserved: clap parse errors exit 2 (matching the old
usage-error=2); domain refusals stay exit 2; reproduce-diverged stays exit 1.
The exit-code split (2=usage / 1=runtime) is iteration 2. clap is admitted under
the C16 per-case review (research-side CLI, invariant 8 untouched — a dev-loop
compile tax, not a frozen-artifact tax).

Forced plan corrections (implementer; verified against the diff):
- Step 6's deletion of the `parse_*_args` fns orphaned ~34 grammar unit tests
  that called them; those were deleted and that grammar coverage moved to the
  renegotiated cli_run.rs E2E pins (`parse_select` kept + retested).
- run_malformed_cost_value_...: a bad cost value is now a clap value-parse error
  (no "Usage:" line); renegotiated to pin exit 2 + empty stdout + the flag name
  "--cost-per-trade"; the units note stays pinned via `run --help`.
- `allow_hyphen_values` on the three cost flags so `--cost-per-trade -0.5`
  reaches the non-negativity guard (else clap rejects -0.5 as an unknown option).
- Deleted the now-dead `RealWindowGrammar` (clippy -D warnings).

Orchestrator additions after the loop:
- Enabled long-option abbreviation (`infer_long_args` on the root — one
  attribute, it propagates to subcommands) + a test, closing spec acceptance
  criterion 4 which the plan's RED tests did not cover. Decided to deliver
  rather than amend the spec: the two ratified decisions conflict on this
  low-tier item — Fork B (GNU compliance, getopt_long abbreviates) vs F5
  (LLM/automation-first, human-only niceties deprioritized) — and the cost
  tiebreaker (one attribute, not per-struct work) resolves to deliver.

Carry-on debt (not this iteration): the error-message "Usage:"/"usage:"/bare
casing is now mixed (clap's capitalized "Usage:" beside preserved lowercase aura
messages); a deliberate quality-hold since the casings are pinned by
renegotiated/preserved tests. Cosmetic; a candidate for the iteration-2
exit-code-split cleanup.

Verified green (orchestrator, this session): cargo build --workspace, cargo
clippy --workspace --all-targets -- -D warnings, and cargo test --workspace all
clean (0 failed across every test binary, including the renegotiated cli_run
pins and the new --version / scoped-help / GNU-flag / dual-grammar tests).

refs #175
2026-07-01 18:56:46 +02:00
Brummel 6a775f700f fix(cli): walkforward validates its --axis grid once, not once per window
`aura walkforward <bp> --axis <name>=<v>` emitted a rejected axis's
diagnostic more than once (a racy 1-3x) when the grid did not resolve.
`blueprint_walkforward_family` resolved the axes *inside* the per-window
closure, and `walk_forward` fans that closure out across the windows in
parallel — so several windows each `eprintln!` the same `BindError` and
`exit(2)` before any one exit tears the process down.

Axis resolution is window-independent, so it is hoisted to a single
dispatch-boundary pre-flight (mirroring `aura sweep`, which validates its
axes once before any member runs): the first in-sample window is resolved
up front, surfacing the `BindError` exactly once; the per-window closure
then resolves already-validated axes and cannot re-raise (`expect`).

RED-first: cli_run.rs `aura_walkforward_emits_an_unknown_axis_rejection_once`
drives the verb 20x and asserts the rejection is single every run (the emit
count was racy, so a single invocation would be a flaky assertion). Surfaced
by the World/C21 milestone fieldtest.

closes #177
refs #170
2026-07-01 17:41:09 +02:00
Brummel dec07809df fix(cli): aura run refuses an open blueprint with a clean exit 2, not a panic
`aura run <blueprint.json>` bootstraps the loaded signal over the EMPTY
param point (a closed-blueprint assumption) and `.expect()`s
`compile_with_params`, which panics (exit 101, ParamArity) when the
blueprint carries >=1 free knob. The sibling `aura mc` already refuses an
open blueprint cleanly at the dispatch boundary; `run` now does the same.

The `["run", ..]` `.json`-discriminator arm probes the wrapped param_space
(`blueprint_axis_probe`, the single source of that wrap) and, when it is
non-empty, emits a "run requires a closed blueprint … N free knob(s) —
bind them or use `aura sweep --axis`" diagnostic and exits 2 — mirroring
`blueprint_mc_family`'s closed-guard. No panic on a naive consumer's first
move (author an open blueprint, run it).

RED-first: cli_run.rs `aura_run_rejects_an_open_blueprint_without_panicking`
pins exit 2 + the named requirement + no `panicked`. Surfaced by the
World/C21 milestone fieldtest.

closes #176
refs #170
2026-07-01 17:28:02 +02:00
Brummel ff20f74e8f feat(0097): IS-refit walk-forward over a loaded blueprint — aura walkforward <bp.json> --axis
Ship the World/C21 milestone's last piece (Arm A, #173): a true in-sample-refit
walk-forward over a loaded blueprint. `aura walkforward <blueprint.json> --axis
<name>=<csv> [--axis …] [--select argmax|plateau:mean|plateau:worst]` rolls
24/12/12 IS/OOS windows over the synthetic span; per IS window it re-optimizes
the blueprint's params over the user's --axis grid (the #169 prefixed names),
selects the winner by sqn_normalized, runs it out-of-sample, and aggregates the
per-window OOS reports into a FamilyKind::WalkForward family — content-addressed
(put_blueprint) and aura-reproduce bit-identical.

Structure mirrors the hard-wired stage1-r walk-forward arm one substitution
deep: the generic engine walk_forward driver + select_winner + the synthetic
schedule are reused; only the per-window IS-sweep (blueprint_sweep_over) and
OOS-run (run_oos_blueprint) source the loaded blueprint via cycle-0096's
blueprint_axis_probe. reproduce_family_in gains a WalkForward arm reconstructing
each OOS window's windowed slice from the member manifest's window bounds (winner
params via the shared manifest->cells recovery, as the MonteCarlo/Sweep arms do).

Panic-safety: blueprint_sweep_over returns Result<_, BindError>; a bad/unknown
--axis is a clean in-closure exit 2 (mirroring the hard-wired arm's select_winner
handling), never a panic — pinned by the rejects_an_unknown_axis E2E. A no-axis
invocation (nothing to re-fit) is a usage error; a malformed blueprint is rejected
at the dispatch boundary (exit 2, no panic). Reduce-mode members carry R-metrics
(no raw pip curve) → empty stitched equity; oos_r is the meaningful summary (C10
R-first yardstick).

Held quality finding adjudicated: run_oos_blueprint's plan-prescribed redundant
`pip: f64` (== data.pip_size()) removed — derived internally to match its siblings
run_oos_r / blueprint_sweep_over, which also drops the too_many_arguments
band-aid #[allow] (8->7 args). Two plan transcription typos the loop caught and
fixed verified correct: BindError renders via Debug ({e:?}), parse_select via
map_err (Result, not Option).

Verified myself: cargo build --workspace, cargo clippy --workspace --all-targets
-- -D warnings, cargo test --workspace all green; 2 new unit tests + 5 new
cli_run E2Es (persist shape, reproduce bit-identical, no-axis, malformed-safety,
unknown-axis) pass; no regression in the hard-wired walkforward / sweep / mc /
reproduce suites. Engine/registry untouched (invariants 1/2/8/9).

closes #173
2026-07-01 16:38:58 +02:00
Brummel bd2003e217 feat(0096): list a loaded blueprint's sweepable axes — aura sweep <bp.json> --list-axes
A data-level author can now discover a loaded blueprint's open sweepable knobs
before sweeping: `aura sweep <blueprint.json> --list-axes` prints one
`<name>:<kind>` line per open knob, in param_space() order, then exits 0. Before
this the only way to learn the names was to guess a `--axis` and read the
MissingKnob/UnknownKnob error — which names only the rejected knob, never the
valid set. Prerequisite for #173 (IS-refit walk-forward must know which axes to
re-fit per in-sample window).

Design (derived, recorded on #169): the query lands on the SWEEP verb, not
`graph introspect`, because the accepted axis names are produced by the sweep's
own harness-wrapping (wrap_stage1r nests the loaded signal as a
BlueprintNode::Composite named "stage1_signal", so collect_params prefixes them
— stage1_signal.fast.length, not the raw fast.length). Only the sweep verb owns
that wrapping. A new blueprint_axis_probe helper single-sources the wrapped
probe and now subsumes the two previously-inline copies (blueprint_sweep_family
+ blueprint_mc_family), so listed == swept by construction (and stays so across
#159's harness retirement) — no second source of truth.

--list-axes is a standalone query (valueless flag handled before the per-flag
value-consume; rejected with exit 2 if combined with any other flag). A closed
blueprint prints nothing (exit 0). A malformed blueprint is rejected at the
existing dispatch boundary (exit 2, UnknownNodeType), never a panic.

Tests: unit blueprint_axis_probe (prefixed names + empty for closed) + unit
parse grammar (standalone), and five cli_run E2Es — prints names, closed-empty,
rejects-other-flags, malformed-safety (no panic), and a round-trip that DERIVES
the names from --list-axes and feeds them back into a working sweep (pins the
listed==swept invariant #169 exists to close). Verified myself: cargo build
--workspace, cargo test --workspace, cargo clippy --workspace --all-targets -D
warnings all green; no regression in the refactored sweep/mc probes. Engine and
registry untouched (invariants 1/8/9).

closes #169
2026-07-01 14:54:45 +02:00
Brummel 14e27a978b feat(0095): monte-carlo over a loaded blueprint — aura mc <bp.json> --seeds N
The MC half of #170 (World/C21): `aura mc <closed blueprint.json> --seeds N`
builds a FamilyKind::MonteCarlo family from a loaded CLOSED blueprint. Each seed
draws a distinct synthetic price walk (synthetic_walk_sources, the mc_family
pattern), so members differ in the DATA (not just a manifest label) — the sim is
deterministic (C1), so MC variation must come from re-drawing the input. Draws
run disjointly in parallel via the shared monte_carlo helper (invariant 1). The
canonical blueprint is stored once keyed by the shared topology_hash (the 0094
hook), and reproduce_family_in gains a MonteCarlo realization branch that
reconstructs each member's seed-driven walk from manifest.seed — so a persisted
mc family 'aura reproduce's bit-identically (the store-hook proof).

Closed-blueprint only: MC binds no axis, so a free knob has no binder; an open
blueprint yields a named Err (rendered to stderr + exit 2 at the run_blueprint_mc
boundary, mirroring blueprint_sweep_family — no hidden process exit in the pure
builder), pre-empting the compile_with_params arity panic.

Two quality-gate-driven improvements over the ratified plan, both applied and
verified: (1) delegate the disjoint draws to the engine monte_carlo seam instead
of a hand-rolled loop (my plan's 'closure-shape mismatch' rationale was wrong —
the Fn(u64,&[Scalar]) closure ignores the unused base_point, exactly as mc_family
does); (2) the closed-blueprint guard returns Result rather than exiting inside
the builder, matching the sweep sibling's contract and making the rejection
unit-testable. Engine + registry untouched (invariants 8/9).

Verified: cargo build --workspace clean; cargo clippy --workspace --all-targets
-- -D warnings clean; cargo test --workspace green (5 new tests: the parser,
anti-degenerate family, MC reproduce round-trip, and two E2Es incl. the open
rejection; no regressions).

refs #170
2026-07-01 13:18:00 +02:00
Brummel cec7811c32 audit(0094): cycle close — content-addressed reproduction; ledger refreshed, coverage closed; rm ephemeral spec/plan
Architect drift review (4751d4b..HEAD): drift_found — ledger-currency only; the
code is clean.

What holds (architect, confirmed by diff + green tests):
- Inv 8 / C9: aura-engine byte-untouched; content_id + sha2 in aura-cli, the
  store in aura-registry.
- Inv 9: Registry::{put,get}_blueprint is a dumb bytes-by-key store (opaque
  String keyed by String hash; no sha2, no parse, no sha256==hash verify — the
  caller owns the hash). No domain knowledge crept into the engine or registry.
- C1: the run_blueprint_member extraction is behaviour-preserving (keystone
  blueprint_sweep_member_equals_single_run_and_shares_topology_hash unchanged +
  green); serde_json/float_roundtrip serves determinism (disk==recomputed), not a
  mask (1-ULP canary green).
- C11/C12: one blueprint stored per family, keyed by the shared topology_hash.

Resolutions:
- [ledger] Refreshed C18 + C24: added the cycle-0094 Realization (the
  content-addressed store runs/blueprints/<hash>.json + aura reproduce), and
  retired the now-false "content-addressed identity / full reproduction deferred"
  claims (INDEX.md:1261, C24 deferred block). Only the structural-axis /
  whole-harness content-id (and the debug-name-in-id question) genuinely remains,
  filed forward (#171, #170).
- [debt->closed] Added the two untested refuse-don't-guess E2E arms
  (aura_reproduce_rejects_a_non_generated_family — a hard-wired sweep member has
  no topology_hash; aura_reproduce_rejects_a_missing_stored_blueprint — the store
  was deleted), and a cross-surface unit pin
  (topology_hash_is_the_content_id_of_the_canonical_form) so the two acc-1 paths
  cannot silently drift.
- [scaling] MC/walk-forward-from-blueprint (#170) will need the same put_blueprint
  hook or reproduce won't cover them — noted on #170.

No regression scripts in this project (Test is the gate): full workspace suite
green (51 suites), clippy -D warnings clean, binary verified end-to-end.

Ephemeral spec/plan git-rm'd per the lifecycle convention; durable rationale is in
the ledger. The World/C21 milestone stays open (#170 remains).

closes #158
2026-07-01 03:15:39 +02:00
Brummel 717a0b70af feat(0094b): content-id surface + Tier-1 stability — aura graph introspect --content-id (iter 2)
Completes #158 acceptance (acc 1 + acc 3) on top of iteration 1's reproduction:

- acc 1: `aura graph introspect --content-id` reads an op-script and prints the
  content id of its canonical blueprint — the SHA256 (hex) of the same
  blueprint_to_json bytes `graph build` emits. Extracted the single `content_id`
  primitive shared by this surface AND topology_hash, so the two command paths
  (this surface / hashing a `graph build` output) agree by construction. Verified
  live: both e2be81b2… for the SMA-cross op-script. Deterministic + distinguishes
  topologies; a malformed op-list fails cleanly (exit 2, no partial hash).
- acc 3: a Tier-1 optional field the blueprint does not use is tolerated by the
  loader (#156) and absent from the canonical omit-defaults form, so the content
  id is unchanged (content_id_is_stable_across_a_tolerated_tier1_field); plus
  content-id stability across the serialize -> reload -> re-serialize store
  round-trip (#164), the property reproduction rests on.

Finding (documented, filed forward): an op-script and the Rust `stage1_signal`
builder produce DIFFERENT canonical forms — the composite debug-name ("graph" vs
"stage1_signal"), a named vs unnamed Sub, and an unbound vs bound Bias.scale — so
they are different topologies by the byte definition and get different content
ids. Content-addressing keys on the canonical form, which currently includes the
composite debug-name (a non-load-bearing symbol, invariant 11). Whether the
content id should exclude the debug-name (a topology-canonical form distinct from
the byte-canonical form, which would also change the shipped cycle-0092
topology_hash values) is a forward design question, not this cycle.

Verified: full workspace suite green (51 suites); clippy -D warnings clean.

refs #158
2026-07-01 03:05:37 +02:00
Brummel 008692c043 feat(0094): content-addressed reproduction — aura reproduce <family-id> (iter 1)
Restore C18 reproducibility for generated (blueprint-sweep) runs (#158, acc 2):
a sweep's topology is stored once, content-addressed by the topology_hash the
manifest already carries, and `aura reproduce <family-id>` re-derives every
persisted member bit-identically (C1) from the stored blueprint.

- aura-registry: a dumb content-addressed bytes store — Registry::put_blueprint
  / get_blueprint / blueprints_dir over runs/blueprints/<hash>.json. No sha2 dep
  (caller owns the hash; reproduction's bit-identical compare is the integrity
  check); absent id -> Ok(None) (treat-as-empty, like load).
- aura-cli: run_blueprint_member extracted from blueprint_sweep_family's member
  closure (behaviour-preserving; keystone stays green) so the sweep AND reproduce
  re-run the identical reduce-mode path — bit-identity by construction. The store
  write hooks the sweep persist seam (one blueprint per family, shared topo).
  reproduce_family_in loads each member's blueprint by hash, reconstructs the
  point from the recorded params (point_from_params, inverse of zip_params, over
  the WRAPPED signal's param_space so the prefixed knob names match), re-runs, and
  compares metrics. `["reproduce", id]` dispatch arm; refuse-don't-guess on an
  unknown id / missing stored blueprint (exit 2), DIVERGED -> exit 1.

Dependency decision (per-case review, ratified): enabled serde_json
`float_roundtrip`. Stored member metrics round-trip through families.jsonl as
f64 JSON; the default parser can be 1 ULP off, which would make disk-loaded !=
recomputed and "bit-identical" (C1) physically impossible. The feature guarantees
exact f64 round-trip. Blast-radius clean (full workspace suite green); it serves
determinism, the invariant reproduction rests on. Canary:
f64_blueprint_param_survives_store_round_trip_bit_identically.

Two plan bugs caught + fixed by the implement loop and verified: reproduce's
param_space must come from the wrapped signal (prefixed names, the #167 lockstep),
and the family id is 0-indexed (live-0 / smacross-0, not -1).

Verified: full workspace suite green (51 suites); clippy -D warnings clean; and
the binary end-to-end — `aura sweep` (3 members, one shared stored blueprint) then
`aura reproduce live-0` (3/3 bit-identical incl the open-at-end member), unknown
id -> exit 2. acc 1 (graph introspect --content-id) + acc 3 (Tier-1 id stability)
land in iteration 2 (plan 0094b).

refs #158
2026-07-01 02:55:52 +02:00
Brummel 481add0ca2 feat(0093): sweep-from-blueprint — aura sweep <blueprint.json> over named axes
Cycle 2 of the World/C21 milestone (#166): the World now orchestrates FAMILIES
from blueprint-data. `aura sweep <blueprint.json> --axis <name>=<csv> …` loads an
OPEN signal blueprint, grids the by-name axes against its param_space, builds each
member via cycle-1's wrap_stage1r seam, and aggregates to a FamilyKind::Sweep
family — byte-identical in shape to the hard-wired sweep.

- `parse_sweep_blueprint_args` + `SweepBlueprintArgs`: repeatable `--axis name=csv`
  (lexed i64-then-f64; resolve_axes kind-checks), `--name`/`--trace`, the shared
  `--real/--from/--to` accumulator → DataChoice.
- `blueprint_sweep_family` (pure) + `run_blueprint_sweep` (IO) mirror
  stage1_r_sweep_family with three deviations: by-name axes (verbatim param_space
  names, e.g. `stage1_signal.fast.length`), reload-per-member (Composite is !Clone —
  its build closures are Box<dyn Fn>, #164 — so a fresh signal is re-loaded from the
  doc per member), and a per-member shared topology_hash. Members share the hash
  (same signal topology, only params vary); member_key distinguishes them.
- The `aura sweep <file.json>` `.json`-discriminator arm mirrors cycle 1; the
  hard-wired `--strategy` sweep is untouched (#159).

Task-3 design gap resolved: a sweep needs an OPEN blueprint (open knobs to grid),
distinct from cycle-1's fully-bound run fixture (a bound blueprint has an empty
param_space). New `tests/fixtures/stage1_signal_open.json` =
blueprint_to_json(stage1_signal(None,None)); a sweep must bind ALL open knobs, so
the E2E grids fast×slow (2×2 = 4 members).

Verified end-to-end by running the binary: `aura sweep stage1_signal_open.json
--axis stage1_signal.fast.length=2,4 --axis stage1_signal.slow.length=8,16 --trace f`
persists a discoverable 4-member Sweep family; an unknown axis fails clean. Member
equivalence vs run_signal_stage1r + shared-hash/distinct-member_key unit-tested.
Full workspace suite green; clippy --all-targets -D warnings clean.

refs #166
2026-07-01 00:27:16 +02:00
Brummel 2b1c24668d test(0092): run-from-blueprint E2E + bit-identical keystone + demo fixtures
Closes the cycle-1 test coverage for #165. The `aura run <blueprint.json>` CLI
arm itself (the .json dispatch + parse_blueprint_run_args + BlueprintRunArgs)
shipped in ff4a1b3; this adds the coverage that proves it and the in-repo demo
blueprint:

- crates/aura-cli/tests/fixtures/stage1_signal.json — the canonical SMA-cross
  signal (blueprint_to_json of stage1_signal(2,4)); emit_demo_signal_fixture
  (#[ignore]) regenerates it.
- crates/aura-cli/tests/fixtures/unknown_node.json — one type set to "Nope" for
  the fail-clean guard.
- loaded_signal_runs_bit_identical_to_rust_built (the keystone, C1): a signal
  serialized -> loaded via blueprint_from_json -> run is bit-identical to its
  Rust-built twin, incl. topology_hash (64-hex).
- topology_hash_is_stable_and_distinguishes (#158 anchor property).
- cli_run.rs E2E: `aura run <demo>` exits 0 with topology_hash in the manifest;
  an unknown-node blueprint fails clean with UnknownNodeType (the #160 closed-set
  guard at the data-plane face, invariant 9).

Verified end-to-end by actually running the binary: `aura run stage1_signal.json`
-> RunReport with a 64-hex topology_hash, sim-optimal+risk-executor broker, R
metrics; `aura run unknown_node.json` -> UnknownNodeType("Nope"), exit 2. Full
workspace suite green; clippy --all-targets -D warnings clean.

refs #165
2026-06-30 20:48:02 +02:00
Brummel ff4a1b3d4a feat(0092): run-from-blueprint infrastructure — restructure + topology_hash + shared seam (Tasks 1-3)
Cycle-1 infrastructure for #165 (World/C21). Behaviour-preserving + workspace
green; the CLI arm + tests (Tasks 4-5) follow.

Task 1 — restructured stage1_r_graph into stage1_signal() (the serializable
signal leg: SMA-cross -> Bias, a `price` input-role + a `bias` output) +
wrap_stage1r(signal, ...) (broker/sinks/exec/cost around a nested signal), with
stage1_r_graph() = wrap_stage1r(stage1_signal(...)). DEVIATION from the plan's
"callers untouched" claim, verified behaviour-preserving: nesting the signal
prefixes its param-space names (stage1_signal.fast.length), which broke the
sweep's bare .axis("fast.length"); fixed exactly as the #137 stop-knob machinery
does — FAST_LENGTH_SUFFIX/SLOW_LENGTH_SUFFIX suffix-resolution + stage1_r_friendly_name
arms mapping the prefixed names back to the bare manifest names. All observable
behaviour (manifest names, member keys, metrics, traces) byte-identical; the flat
graph and every metric unchanged (full suite green, incl. sweep/walkforward/MC).

Task 2 — RunManifest.topology_hash: Option<String> (Tier-1 optional, serde
default/skip — old manifests byte-identical, #156), threaded None into all 24
literal sites across 7 crates. Also fixed the aura-registry RunManifestRead
read-mirror to carry topology_hash (it was hardcoding None on load) — else a
stored hash would silently drop on the registry round-trip once Task 3 stores a
real one.

Task 3 — the shared run_signal_stage1r seam (hash the signal, wrap, compile with
params, bootstrap, run, manifest with params from param_space + topology_hash) +
the sha256_hex topology_hash helper (sha2 in aura-cli, off the frozen engine,
invariant 8). run_stage1_r now carries its topology_hash too (every run becomes
self-identifying, #158). The seam is unwired until Task 4 (transient
#[allow(dead_code)], retired there); RED-first seam tests added.

Verified: cargo test --workspace green (51 suites, 0 failures); cargo clippy
--workspace --all-targets -D warnings clean.

refs #165
2026-06-30 20:23:42 +02:00
Brummel 4a3ae05db2 test(graph_model): cover a producer backing >1 re-exported output field
#47 — close a coverage gap in the graph model serializer: a single interior
producer node backing more than one re-exported output field was never
exercised by a fixture (every existing one maps each output to a distinct
producer or a single field).

The issue was filed at cycle 0022 against `aura-cli/src/graph.rs`'s
`render_definition` / `output_binding` helper, which rendered a textual
`(n1, n2) := <producer>` tuple binding. That file and helper no longer exist —
the textual definition renderer was replaced by the JSON model emission now in
`aura-engine/src/graph_model.rs`, where the multi-OutField case is plain flat
iteration over `c.output()` (one `[name,kind]` entry + one `#N` boundary
binding per `OutField`, by `(node, field)`), not a special tuple-binding arm. So
the original branch is gone; this guards the flat emission that replaced it.

New test `multi_outfield_producer_emits_one_binding_per_field` (+ two fixtures):
a two-field producer (`hi`:f64, `lo`:i64 — distinct kinds, so the per-field
`field_kind` lookup is exercised) re-exported as both composite outputs, nested
in a root so it renders through `composite_def_json`. Asserts both fields
surface with their own kind (`"outputs":[["hi","f64"],["lo","i64"]]`) and both
bindings reference the same producer at distinct ordinals (`0.o0`/`#0`,
`0.o1`/`#1`).

Test-only; no production code touched (the behaviour was already correct).
Verified: cargo test -p aura-engine green (237 in the lib suite); clippy clean.

closes #47
2026-06-30 16:03:29 +02:00
Brummel b8b83cf1c9 feat(0091): byte-canonical blueprint emit; record the equality-surface decision
#164 — tighten the C24 "canonical, versioned" blueprint contract on two corners
the milestone fieldtest surfaced. Both forks were derivable; resolved with
rationale (recorded on #164).

Fork 1 — canonical artifact carries no trailing newline. `blueprint_to_json`
returns the JSON value with no trailing newline (647 bytes for the SMA-cross
fixture); `aura graph build` had framed it with a `println!` (648). The library
serializer is the single canonical source — the form content-addressed topology
identity (#158) hashes — so the CLI is a transport that must not mutate the
bytes: `build_cmd` now `print!`s the canonical bytes verbatim, making the CLI and
library emit paths byte-identical. RED-first: the new E2E
`graph_build_emit_is_byte_canonical_no_trailing_newline` pinned the trailing
newline (FAIL), green after the one-line change. The two cycle-0088 `.out.json`
goldens are re-recorded at 647 bytes. The loader stays lenient (a trailing
newline on input still parses, Tier-1 robustness; the milestone fieldtest's
`mt_4` tolerance check is unaffected).

Fork 2 — the canonical JSON is the blueprint equality/identity surface; no
second in-memory PartialEq/Debug is added. `Composite`/`BlueprintNode` cannot
derive them: `PrimitiveBuilder` (aura-core node.rs) holds a
`build: Box<dyn Fn(&[Cell]) -> Box<dyn Node>>` closure, neither comparable nor
printable. Equality could only be defined over the serialized data, of which
`blueprint_to_json` is already the single source; a separate in-memory equality
notion would be a redundant second source of truth and a drift hazard against
the form #158 hashes. Recorded the decision (discharges acceptance box 2's "or
JSON-string is the intended equality surface" arm) in the C24 ledger; no code
added for it.

Verified: cargo test --workspace green (51 suites, incl. the new RED->green E2E);
cargo clippy --workspace --all-targets -D warnings clean.

closes #164
2026-06-30 15:57:15 +02:00
Brummel cdd2da6337 feat(0090): blueprint format forward-compat — Tier-1 tolerance pinned, two-tier discipline codified (closes #156)
The blueprint data format (C24) extends additively under one codified
two-tier discipline:
- Tier-1 (additive-optional): a new optional field/section is tolerated by an
  older reader (serde ignores unknown fields — no deny_unknown_fields), with no
  format_version bump; by C1 a new optional field defaults to prior behaviour,
  so an old blueprint reproduces the same graph.
- Tier-2 (must-understand: a new node type, edge semantics, or structural-axis
  kind): bumps format_version, so an old reader refuses cleanly
  (LoadError::UnsupportedVersion / UnknownNodeType) rather than silently
  building a different graph.

Most of #156 already shipped with #155 (cycle 0087): the envelope, omit-defaults
canonical form, and both Tier-2 load-path refusals were present and green. This
cycle pins the one previously-unproven property — Tier-1 forward-tolerance of an
unknown optional field on the current loader — and codifies the discipline.

Changes:
- New characterization pin (in-crate, blueprint_serde.rs):
  unknown_optional_field_is_tolerated_byte_identically — an unknown optional key
  at doc and primitive level loads byte-identically and runs bit-identically.
- Two public-seam E2E pins (blueprint_serde_e2e.rs, added by the E2E phase):
  unknown_envelope_field_tolerated_through_public_api and
  unknown_primitive_field_tolerated_through_public_api — split the two distinct
  tolerance surfaces (envelope vs primitive) across the World/cdylib crate
  boundary, where a deny_unknown_fields added to one would break forward-compat
  while leaving the in-crate test green.
- LoadError::UnsupportedVersion doc reworded from the deferred "is #156" note to
  the settled two-tier discipline statement.
- C24 ledger (docs/design/INDEX.md): Status codification sentence added; #156
  dropped from Remaining (#158/#159 stay).

Derived fork (recorded on #156): the per-section required-flag mechanism (PNG
critical-vs-ancillary) is NOT built — the version envelope already gives the
must-understand refusal at version granularity, no current Tier-2 section
validates a finer scheme, and C1 holds either way.

The pin is a characterization test (GREEN on first run) — it documents existing
serde silent-ignore behaviour, no production code changed. Verified: cargo test
-p aura-engine green (all targets, three new tests confirmed by name); cargo
clippy --workspace --all-targets -- -D warnings clean.
2026-06-30 13:28:25 +02:00
Brummel 3c4b667955 feat: construction add names a node via name, mirroring the builder (#157)
The op-script authoring form is meant to be the Rust builder lifted into data —
every op is a 1:1 image of a GraphBuilder method. The one un-builder-ish key was
`add`'s `"as"`: the builder names a node with `.named("fast")`, so the data echo
is `"name"`, not the SQL-ish aliasing word `as`. Renamed the `add` wire key
`as -> name`.

`expose` keeps `as` — there it is a genuine alias (`expose bias.bias as bias`,
mirroring the builder's positional name arg). `source`/`input` keep their terse
verbs: those are plain shortenings of `source_role`/`input_role`, not a foreign
concept the way `as`-for-naming was.

CLI-side only: a one-attribute serde rename on the `OpDoc` Add DTO
(`#[serde(rename = "name")]`), since the engine `Op` is serde-free (the engine
field stays `as_name`, untouched). Updated the op-list test documents to the
`name` key (add ops only; expose's `as` left intact) and added an explicit pin
(`add_names_node_via_name_key`). The cycle-0088 fieldtest corpus
(fieldtests/cycle-0088-construction-op-script/*.json) is left on the old `as`
form as historical cycle-0088 evidence (it is not run by the suite).

Verified: full workspace suite + clippy green; `aura graph build` on a `name`-keyed
doc names the node and emits it in the #155 blueprint.
2026-06-30 12:22:45 +02:00
Brummel f1ab81d1b8 feat(0089): construction diagnostics read by-identifier (closes #162)
The cycle-0088 fieldtest found three presentation defects on the construction
op-script surface, all where the surface dropped out of its by-identifier register.
Presentation only — every fault still fires identically (same exit code, same
op/finalize attribution); only the rendered cause changes.

- Item 1 (finalize by-identifier). The holistic finalize fault leaked the raw
  index Debug form (`UnconnectedPort { node: 2, slot: 1 }`). `GraphSession::finish()`
  now translates the three reachable index-carrying CompileErrors into new
  by-identifier OpError variants (UnconnectedPort{node,slot} / RoleKindMismatch{role}
  / UnboundRootRole{role}) using the session's `ids`/`schemas` and the composite's
  `input_roles()` — the holistic gate calls (validate_wiring,
  check_param_namespace_injective, check_root_roles_bound) stay byte-for-byte
  unchanged (C24: no second validator; only the `.map_err` closures changed).
  `aura graph build` now prints `finalize: slot sub.rhs is unconnected`.
- Item 2 (bind mismatch prose). `format_op_error`'s BadParam arm matches the
  BindOpError variants instead of `{:?}`: `op 1 (add): param fast.length expects I64
  but got F64`, matching the connect mismatch register.
- Item 3 (discoverable bind form). `introspect --node` shows the typed-Scalar bind
  form: `param length:I64  (bind {"I64": <v>})`.

Item 1 spans both crates atomically (adding OpError variants forces the exhaustive
format_op_error match; the finish() translation flips behavioural test pins in both
crates). Five test touches: the two in-file unit tests, the two E2E twins plan-recon
found that the spec's testing strategy under-enumerated
(construction_e2e.rs, tests/graph_construct.rs), and a new unbound-root-role test;
the implementer added RoleKindMismatch / Ambiguous/UnknownParam coverage too. The
CLI finalize test was tightened (orchestrator) to pin `sub.rhs` and the absence of a
raw `node:` index.

Spec boss-signed on a grounding-check PASS (docs/specs+plans/0089). Verified: full
workspace suite + clippy green; the three messages reproduce exactly against the
built binary on the fieldtest fixtures.
2026-06-30 11:49:03 +02:00
Brummel 16520429be fix(0088): reject dataflow cycles in construction build (closes #161)
The construction op-script service accepted a dataflow cycle and exited 0,
emitting a non-DAG blueprint — violating domain invariant 5 / C9 (the graph is a
DAG; the only feedback path is an explicit delay/state node). The cycle-0088
fieldtest surfaced it (fieldtests/cycle-0088-construction-op-script/FINDINGS.md).

The construction gate set had no acyclicity check: a cyclic interior-edge set
passed both the eager per-op gates and the holistic finish, so replay() returned
Ok. Fix: an eager reachability check at the connect op that closes the cycle —
GraphSession::closes_cycle(from, to) asks "does the `to` node already reach the
`from` node through the edges added so far?" and rejects with a new by-identifier
OpError::WouldCycle { from, to } ("connecting from -> to would close a cycle"),
threaded through the CLI's exhaustive format_op_error. The fault is attributed to
the offending connect op, consistent with the surface's eager per-op diagnostics.

Eager rather than a holistic topological check at finish: a cycle is closed by
one specific connect, and naming that connect is the sharpest by-identifier error.

RED-first: two tests pin the symptom (a 2-cycle a<->b and a self-loop), both red
before the fix and green after; the 19 existing construction tests, the full
workspace suite, and clippy stay green.

Scope is the cycle only. The validity-floor cases the fieldtest also noted (an
unfed declared source role, an output-less graph) are degenerate-but-well-formed,
not invariant violations, and are deferred (refs #161) pending the build->consume
edge (#28) that defines what a valid emitted blueprint must guarantee.
2026-06-30 10:48:03 +02:00
Brummel 86841b0740 audit(0088): cycle close — drift fixes + C24 ledger refresh; close construction service
Architect drift review of cycle 0088 (#157, the introspectable fail-fast
construction service: §A shared gate predicates, §B the GraphSession/Op/replay
per-op surface + introspection, §C the aura graph build/introspect CLI).

What holds (confirmed against the diff): no-second-validator (C24) — edge_kind_check
is the single kind-check site called by both validate_wiring and
GraphSession::connect, and validate_wiring / check_param_namespace_injective are
reused verbatim in finish(); engine domain-free (invariant 9) — construction takes an
injected Fn(&str)->Option<PrimitiveBuilder>, no registry, the closed match +
std_vocabulary_types live in aura-std; topology-as-data / no DSL (C24/C17/inv 10) —
the engine Op is serde-free, the OpDoc wire DTO is CLI-side, ops are static and
non-Turing-complete; replay-equals-builder byte-identity (construction_e2e.rs) protects
C9/C19/C23.

Resolution:
- fix (drift-med): the root-role-boundness gate was the one holistic check finish()
  re-implemented instead of sharing — extracted check_root_roles_bound (blueprint.rs)
  and called from BOTH compile_with_cells and finish(), so finish() now reuses ALL the
  holistic gates and the no-second-validator claim is literally true. Behaviour-
  preserving (same UnboundRootRole variant; unbound_root_role_is_rejected stays green).
- fix (drift-med): removed the #![allow(dead_code)] on construction.rs — empirically
  unnecessary (the pub re-exported surface is reachable; clippy -D warnings green
  without it) — and its now-inaccurate comment (it claimed removal "when the consumer
  lands", but the consumer is a separate crate). Also tidied a stale graph_construct.rs
  module-doc line.
- fix (ledger): C24 Status refreshed — #157 moved out of "Remaining" with a cycle-0088
  realization note (the construction service: eager/holistic gate split, build-free
  introspection, emits the #155 blueprint); #159 reframed as paired with #157.
- carry-on (debt-med, forward-noted): the std_vocabulary roster is now a third hand-kept
  copy (match + test list + std_vocabulary_types) and introspect --vocabulary escalates
  #160's gap to user-facing — noted on #160; still fails safe.
- carry-on (scaling-low, forward-noted): the engine Op -> wire OpDoc direction is
  unguarded for future Op variants — noted on #156 (the format-extension track).

No architect-sweep / regression script is declared in the project facts, so the suite
is the gate: cargo test --workspace 51 suites green; clippy --all-targets -D warnings
clean. Cycle drift-clean.

Ephemeral cycle artifacts removed (lifecycle convention): docs/specs/0088-*,
docs/plans/0088-*.

closes #157
2026-06-29 21:48:40 +02:00
Brummel 25e452aaf7 feat(0088): §C construction op-script CLI — aura graph build/introspect
Iteration-2 §C of the construction service (#157), the CLI shell over the
iteration-1 engine core (ea1ca32 + 27ac4dc): a declarative, replayable JSON
op-list document drives the engine's per-op-fallible construction surface.

- graph_construct.rs (new, aura-cli): an `OpDoc` serde DTO (`#[serde(tag="op",
  rename_all="lowercase")]` + field renames type/as) deserializes the document
  and maps 1:1 into the serde-free engine `Op` — the wire format is a CLI
  concern, the engine owns no second representation. Bind values use the typed
  Scalar form (`{"I64":2}`), kinds the capitalized #155 form (`"F64"`).
- `aura graph build`: read the op-list from stdin, replay through std_vocabulary,
  emit the #155 blueprint JSON on success; on the first failing op print
  `op N (kind): cause` to stderr (the op-kind recovered from the retained parsed
  list) / `finalize: cause` for a holistic fault, and exit non-zero. A CLI-side
  format_op_error phrases each OpError (the engine error types stay Display-free).
- `aura graph introspect`: `--vocabulary` (std_vocabulary_types), `--node <T>`
  (a type's ports/kinds + param paths off the pre-build schema), `--unwired`
  (a partial document's open slots via GraphSession::unwired) — all build-free.
- Two new argv arms over the flat match; the bare `["graph"]` HTML-render arm
  (sample_blueprint, #159's concern) is left intact.

#157 acceptance now demonstrated through the CLI (E2E in tests/graph_construct.rs,
8 tests): a document builds to the #155 blueprint that compiles identical to its
Rust-built twin (C1); an invalid op is rejected at the op, named; introspection
answers vocabulary / ports+kinds / unwired slots without a build. Full suite
green (51 suites); clippy --all-targets -D warnings clean.

The JSON op-list reuses #155's Scalar/ScalarKind serde shapes; full harnesses
(SimBroker/Recorder construction-arg builders) await #156. Wire-format forks
derived + recorded on #157.

refs #157
2026-06-29 21:30:46 +02:00
Brummel 27ac4dc537 feat(0088): §B construction op-script surface — GraphSession/Op/replay + introspection
Iteration-1 §B of the construction service (#157), on the §A shared predicates
(ea1ca32): a declarative, replayable by-identifier op-script that builds a
runnable blueprint through validated ops, reusing the engine's existing gates at
two cadences — no second validator (C24).

- construction.rs: `Op` (Source/Input/Add/Feed/Connect/Expose, 1:1 with the
  document, dotted by-identifier ports), `OpError` (by-identifier causes),
  `GraphSession` (per-op-fallible accumulator) and the free `replay` driver
  (stop-at-first-failing-op, naming it by `(op_index, OpError)`).
  - Eager (per-op): name resolution + edge_kind_check + the >1-cover
    DoubleWiredPort arm + try_bind — all calling the §A shared predicates.
  - Holistic (finish): the 0-cover totality arm, param-namespace injectivity, and
    root-role boundness — the SAME unchanged engine gates
    (check_param_namespace_injective / validate_wiring), now at end-of-document.
  - Build-free introspection: `unwired()` (a partial document's still-open slots).
- aura-std std_vocabulary_types(): the enumerable companion to the closed
  std_vocabulary match (a `fn(&str)->Option<…>` resolver cannot be listed),
  lockstep-tested in the list->resolver direction; the reverse fails safe (#160).
- aura-engine exports replay/GraphSession/Op/OpError + re-exports BindOpError.

Acceptance (engine level): a sink-free price->fast/slow SMA->Sub->Bias signal
root built through the ops compiles to a FlatGraph byte-identical (edges +
sources) to its GraphBuilder twin (C1); an invalid op is rejected at the op,
named; vocabulary + unwired slots answer without a build. Pinned by
construction.rs unit tests + a construction_e2e.rs integration test over the
public seam the iteration-2 CLI will consume.

Notable judgement calls folded in from the implement loop: feed is all-or-nothing
(two-phase resolve-then-commit, no partial wiring on a mid-fan-out fault); the
node identifier is stamped onto the builder (`named(&id)`) so two same-type nodes
get distinct param paths and do not trip the injectivity gate prematurely; a
module-level `#![allow(dead_code)]` covers the public surface until the
iteration-2 CLI consumes it. Full suite green; clippy --all-targets -D warnings
clean.

Iteration 2 (the JSON op-list encoding + `aura graph build`/`introspect` CLI)
remains; #157 stays open. refs #157
2026-06-29 20:28:41 +02:00
Brummel ea1ca32dbd feat(0088): §A shared gate predicates — edge_kind_check, resolution helpers, try_bind
Iteration-1 §A of the construction service (#157): the surface-agnostic shared
predicates the eager op-script path and the holistic finalize gates both call —
no second validator (C24).

- edge_kind_check (blueprint.rs): the per-edge producer/consumer kind check,
  lifted verbatim out of validate_wiring's edge loop into a pub(crate) fn that
  validate_wiring now calls — behaviour-preserving (same Bootstrap(KindMismatch)
  variant; the existing compile-time gate test stays green).
- resolve_input_slot / resolve_output_field (builder.rs): the exactly-one-match
  name→index resolution extracted as pub(crate) fns over (&NodeSchema, &str), so
  the runtime-String op-script names share GraphBuilder's resolution; GraphBuilder
  delegates and maps to the unchanged BuildError variants.
- try_bind + BindOpError (aura-core node.rs): the fallible Result twin of bind
  (bind keeps its panic contract and pinned messages verbatim; try_bind is a
  separate method reporting the same three conditions as values).
- check_param_namespace_injective + validate_wiring widened to pub(crate) for the
  finalize-stage reuse by the upcoming GraphSession::finish (§B).

Partial iteration: §B (the GraphSession/Op/replay surface + introspection) and
the §A→§B integration land next (plan Tasks 4-8). compile_with_params /
check_ports_connected are behaviourally unchanged. Full suite green; clippy clean.

Plan correction folded in: Task 3's test originally referenced aura_std::Sma —
infeasible inside aura-core (aura-std depends on aura-core; the reverse edge is a
dependency cycle). Adapted to a local PrimitiveBuilder probe with identical
assertions.

refs #157
2026-06-29 18:38:34 +02:00
Brummel d5602ec5ad feat(0087): blueprint serialization & loader — close the graph-as-data loop
C24 first cut (#155): a blueprint — the param-generic, named graph-as-data a
Rust builder produces — is now a first-class serializable data value with a
canonical, versioned format, and the engine has the missing load path
(data -> blueprint -> FlatGraph), not only the Rust-builder construction path.

What ships:
- `blueprint_to_json` / `blueprint_from_json` in a new `aura-engine`
  blueprint_serde module: a faithful serde DTO projection (the build closures
  dropped; re-derived on load), top-level `format_version` envelope, canonical
  compact JSON (struct field order, defaults omitted via skip_serializing_if).
- A resolver seam: the loader is generic over an injected
  `Fn(&str) -> Option<PrimitiveBuilder>`; the concrete closed `match` over the
  aura-std vocabulary lives in aura-std (`std_vocabulary`), never in the engine
  (the engine stays domain-free: lib deps aura-core + aura-analysis only, never
  the node-vocabulary crate). This is C24's compiled-in closed-set referenced by
  type identity, NOT a dynamic/marketplace node registry (domain invariant 9).
- serde derives on the serialized plain types (Edge, Target, OutField, Role,
  BoundParam, ScalarKind); BoundParam additionally re-exported from aura-core's
  root (an additive fix the plan's file list missed).

Load-bearing scope decisions (derived, logged on #155):
- Sinks are excluded from the serialized blueprint — a recording sink captures an
  mpsc::Sender (runtime identity, not param/topology, C19 value-empty); sink
  addressing is the C18-registry / #101 concern. The round-trip test attaches
  identical sinks post-load to both twins.
- Construction-arg builders (LinComb(arity), CostSum(n), SimBroker(pip),
  Session(..)) are out of the round-trippable set: their structural args are a
  C20 structural-axis concern the param-generic format does not yet encode; an
  absent type_id fails the load cleanly (UnknownNodeType), never a silent wrong
  graph. #155's vocabulary is the 22 zero-arg aura-std builders. Additively
  extensible later (#156).

Orchestrator hardening on review: the serializer now emits bound params in
ascending original-slot order (mirroring the loader), so the canonical form is
bind-order-independent — a precondition for content-addressing (#158). Identity
today (every #155 node has <=1 param); holds by construction for future
multi-param nodes.

Acceptance (all green): a serialized blueprint runs bit-identical (C1) to its
Rust-built twin; serializer/loader are inverse (canonical idempotence, incl. a
recursive nested composite); unknown-type and unsupported-version fail named,
never panicking. cargo test --workspace 748 passed; clippy --all-targets
-D warnings clean.

closes #155
2026-06-29 14:54:45 +02:00
Brummel d925a747cb feat(0086): cost-flag CLI ergonomics — units note, named diagnostics, non-R-harness guard
#153 (friction surfaced by the milestone-#148 close fieldtest). Three cost-flag
fixes on `aura run`, all on the pure parse_run_args grammar:

- Units discoverable: a COST_FLAGS_NOTE appended to the run usage + --help
  (price units, charged in R as cost/|entry-stop|, per-ENGINE-cycle carry,
  >= 0, stage1-r only).
- A negative rate names the cause + flag ("<flag> must be non-negative, got <v>")
  via a factored parse_nonneg_rate helper, not a bare usage dump.
- A cost flag on a non-R harness (sma/macd) is refused (exit 2, "cost flags
  require an R-evaluator harness (stage1-r)") instead of silently ignored — the
  named spec_gap, decided reject/fail-loud (refuse-don't-guess, recorded on #153),
  pinned by a new C10 ledger note.

Internal RunArgs/RunData/HarnessKind gain #[derive(Debug)] (behaviour-neutral) so
the pure parse_run_args unit tests can use expect_err.

Verified independently: cargo test -p aura-cli green, the eight stage1-r cost
goldens byte-unchanged; clippy -p aura-cli --all-targets -D warnings clean. Only
the grammar/usage strings and the previously-unguarded sma-cost path changed;
the cost-model graph and R math are untouched.

closes #153
2026-06-29 12:12:22 +02:00
Brummel f5e00a9c72 feat(0085): per-cycle-held accrual — CarryCost + ChargeMode (approach B)
Cycle 5 of milestone #148 (cost-model graph in R): the per-cycle-held accrual
mechanism — C10's "per-cycle-held factors accrue over the hold". A carry/funding cost
is charged for every cycle a position is held, so the net_r_equity equity curve bleeds
continuously over the hold (approach B, "ja, mach B") rather than stepping at close.

Mechanism (logged on #148): ChargeMode { AtClose, PerHeldCycle } is a property of the
cost factor, read by the one shared CostRunner (no second runner type). The AtClose arm
is the pre-cycle-5 eval tokens VERBATIM (IEEE-754 byte-identity). The PerHeldCycle arm
accrues `per` into `acc` each held cycle, dumps the accrued total into `cum` at close
(resetting acc), and marks the open position via a growing open_cost_in_r. The bleed
lives in open_cost_in_r, which the net_r_equity tap already subtracts — so summarize_r
and the CLI net_eq wiring are UNCHANGED, and the cycle-0083/0084 net_expectancy_r
goldens (-614.3134020253314 flat, -615.0304388396047 composed) + the C18 no-cost golden
stay byte-identical.

CarryCost is a ConstantCost twin differing only in charge_mode() -> PerHeldCycle (a
labelled stress parameter, the flat base of the accrual family). Wired through the
existing cost_graph/CostSum aggregation; a per-trade ConstantCost and a per-held-cycle
CarryCost compose in one run (the costs sum per-field — the composed golden is exactly
the sum of the two single-cost charges). New run-path --carry-per-cycle flag
(sweep/walkforward/mc pass None, as the existing cost flags do).

Tests: 2 RED-first unit B-proofs (open_cost grows over the hold, dumps the total at
close, acc resets between trades — the discriminator a scalar net_expectancy_r cannot
see); the CarryCost twin's 5 unit tests; 2 captured net_expectancy_r goldens
(-768.2095026600887 carry, -1383.7939051991182 composed); a net_r_equity-bleeds-over-
the-hold integration test (the terminal open hold's r_equity-net_r_equity gap strictly
grows); plus negative-rate-refused-exit-2, monotone-in-rate, and zero-rate-exactly-free
guards.

Verified: cargo test --workspace (0 failures), cargo build --workspace, cargo clippy
--workspace --all-targets -- -D warnings clean. summarize_r / aura-analysis untouched.
Deferred to later #148 cycles (logged there): notional-based carry, the calendar-aware
overnight swap, sweep-path cost.

refs #148
2026-06-29 01:00:51 +02:00
Brummel face1879a6 audit(0084): cycle close — drift-clean (code); C10 cycle-0084 note + doc fixes
Cycle 4 of milestone #148 (cost-graph composite-builder). Architect drift review
over fc52b4f..d5c44dd: drift_found, every item dispositioned.

What holds (architect, evidence-of-review): C16/C9 — cost_graph lives in
aura-composites, the factors in aura-std, aura-engine stays domain-free; the
composite is ordinary downstream nodes. C11 behaviour-preservation is structural,
not just asserted — the composite inlines to the same flat fan-in as the deleted
CLI block (identical node set/order, cost[k].field slot map, geometry fan, net_eq
terms/signs, cost_rec cols), so the cycle-0083 net_expectancy_r goldens stay
byte-identical. C23 — role/port names non-load-bearing; 4 fixtures pin the role-set
+ output triple.

Resolved this commit:
- [high] ledger C10: the cycle-0083 note's "Still deferred (decision E)" is removed
  (E shipped this cycle) and a cycle-0084 realization note added.
- [medium] cost_graph .leak() provenance (aura-composites/src/lib.rs): the
  "risk_executor precedent" was imprecise — that .leak() lives in risk_executor's
  TEST (one-shot, leak-safe), not its production builder. Doc corrected: the leak is
  fine for one-shot run-path construction but must be interned (the COL_PORTS
  production pattern) before cost reaches the sweep path.
- [low] cli_run.rs golden docstring cited the deleted `slot * COST_WIDTH + f` CLI
  wiring; updated to the cost_graph composite's per-node cost[k].<port> wiring.

Deferred (tracked, not pending) -> #152:
- [medium] the cost[k].<port> index-namespacing restated across CostSum / cost_graph
  / the CLI (a build-validated lockstep — a divergent name fails loudly at g.build(),
  NOT the silent positional kind 0083 collapsed), AND the per-build .leak() that
  becomes an allocation regression on the per-member sweep path. Both are harmless on
  the current run-only cost path; the proper fix is one sweep-safe single-source
  cost-port-name contract (interned), best built with the deferred sweep-cost cycle.

Regression gate: cargo test --workspace (0 failures), clippy --workspace --all-targets
-- -D warnings clean, cargo doc clean — the architect is the primary gate (no dedicated
regression script). Ephemera (spec 0084 + plan 0084) git rm'd.

Milestone "Cost-model graph (in R)" remains OPEN.

refs #148
2026-06-28 22:06:06 +02:00
Brummel d5c44dd1ea feat(0084): cost-graph composite-builder
Cycle 4 of milestone #148 (Cost-model graph in R), decision E.

New `cost_graph(Vec<PrimitiveBuilder>) -> Composite` in aura-composites: it fans
the 4 PM-geometry inputs to N cost nodes, surfaces each node's extra inputs
(discovered via schema introspection past GEOMETRY_WIDTH) as `cost[k].<port>`
roles, sums them through CostSum, and exposes the 3-field aggregate. Replaces the
CLI's manual slot-indexed cost-wiring + the hardcoded MAX_RUN_COST_NODES=2 cap
with one principled composite of arbitrary arity. GEOMETRY_WIDTH re-exported from
aura-std (first cross-crate consumer). Runtime port names .leak()'d (the
risk_executor precedent), a bounded one-time cost at blueprint construction.

Behaviour-preserving at the value level: the composite inlines at bootstrap (C11)
to the same flat fan-in, so the two cycle-3 net_expectancy_r goldens
(-614.3134020253314 flat, -615.0304388396047 composed), the C18 no-cost golden,
and the full suite stay green verbatim. Honours C9 (a composite of cost nodes is
ordinary downstream nodes), C16 (wiring lives in aura-composites, not aura-engine),
C23 (label drift under cost_graph nesting is permitted; no with-cost graph
label/shape golden exists).

Four aura-composites unit tests pin the exposed contract: the two planned (n=2
heterogeneous role set; n=1 no-extra shape) plus two the implementer added that
strengthen the headline — arbitrary-arity per-node namespacing (n=3, two
VolSlippageCost -> distinct cost[1]/cost[2].volatility, the property that retires
the 2-node cap) and geometry fan-once (n=2 extra-free -> only the 4 geometry roles).

Accepted nit (held, non-gating): the CLI reconstructs the `cost[k].volatility`
role name to feed the vol node's extra input — a string coupling to the composite's
public role-naming convention. Kept: role-based composite wiring addresses roles by
name (as the CLI already does for "closed"/"bias"/"price"); the convention is tested
+ build-validated; a decoupling accessor would widen this cycle's scope.

Verified: cargo test --workspace (0 failures), clippy --workspace --all-targets
-D warnings clean, cargo doc -p aura-composites clean.

refs #148
2026-06-28 21:52:06 +02:00