Commit Graph

248 Commits

Author SHA1 Message Date
Brummel 2be6cf8860 feat(cli): dissolved --trace requests only producible taps (#240)
persist_taps_from now filters the tap vocabulary through campaign_run's
own tap_channel classification (exposed pub(crate)) and drops the net
channel: every generated sugar campaign carries an empty cost section by
construction, so net_r_equity was unproducible there and the executor's
skip notice named a remedy (add a cost block to the campaign document)
unreachable from the sweep/walkforward verb. The authored-campaign path
keeps the notice verbatim — there the user wrote the document and the
remedy is reachable. The two full-vocabulary pins move to the producible
subset (deliberate pin move, documented in their doc comments).

closes #240
2026-07-11 16:16:04 +02:00
Brummel 955c909769 test(cli): RED — dissolved --trace requests the unproducible net tap (#240)
The generated sugar campaigns carry an empty cost section by construction,
so net_r_equity can never be produced on the sweep/walkforward --trace path;
requesting it makes the executor print a remedy (add a cost block to the
campaign document) unreachable from the invoked verb.

refs #240
2026-07-11 15:44:05 +02:00
Brummel 5616aa6c6a fix(registry,cli): chart resolves the depth-2 sweep --trace family layout
TraceStore::name_kind and read_family resolved taps exactly one
directory below the family handle, so the per-member fan-out layout
sweep/walkforward --trace writes since #224
(<name>/<cell>/<member>/index.json) classified as NotFound and
`aura chart <printed handle>` exited 1. Both sides now resolve depth-1
(campaign nominee layout) and depth-2, with depth-2 members keyed
<cell>/<member> (deterministic sort, C1); the on-disk layout is
unchanged. emit_chart's not-found message no longer suggests re-running
with the unknown handle as --trace (the data-creating command cannot
take an output handle as input); the message prefix stays pinned.
Milestone-fieldtest finding B1 (fixtures: 09da04f); verified against
the fieldtest's own on-disk family and the campaign depth-1 sibling.
2026-07-11 15:05:31 +02:00
Brummel abb6fbaa87 test(cli): RED — chart cannot open a sweep --trace family by its printed handle
Milestone-fieldtest finding B1: TraceStore::name_kind/read_family
resolve taps exactly one directory below the handle, so the depth-2
per-member layout sweep/walkforward --trace writes (#224,
<name>/<cell>/<member>/index.json) classifies as NotFound and
`aura chart <handle>` exits 1 — with a remedy string that suggests
re-running the very command that produced the data. The new autonomous
test fabricates the depth-2 family via TraceStore and pins: chart on
the printed family handle exits 0 and emits the HTML.

refs milestone fieldtest 09da04f
2026-07-11 14:27:28 +02:00
Brummel d3ad9f4e73 feat(cli): generalize no-window fallback resolves the intersection of all symbols' windows
The no---from/--to fallback in dispatch_generalize probes EACH listed
symbol's full archive window (same partial bounds threaded through) and
resolves the one shared campaign window as their intersection — latest
start, earliest end — instead of symbols[0]'s window alone, so the
cross-instrument floor is measured over a genuinely common period.
Disjoint archives refuse at runtime (exit 1) naming each symbol's span.
The intersect-or-refuse arithmetic lives in the pure
intersect_shared_window helper (unit-tested: single symbol, overlapping,
disjoint); the explicit --from+--to path stays byte-identical. The
superseded shape test is renamed to
generalize_without_explicit_window_resolves_a_shared_window_and_completes
with its doc comment reworded to the intersection contract.

closes #213
2026-07-11 14:02:41 +02:00
Brummel bb8c01895b test(cli): RED — no-window generalize resolves symbols[0], not the intersection
The new archive-gated e2e pins the #213 contract: a multi-symbol
generalize without --from/--to must resolve the shared campaign window
as the intersection (latest start, earliest end) of the listed symbols'
full archive windows. On the current tree it resolves symbols[0]'s full
window (AAPL.US listed first keeps AAPL's wider span on both bounds), so
the test fails RED. Expected windows are derived from the live archive
via single-symbol sweep probes, never hard-coded.

refs #213
2026-07-11 13:14:03 +02:00
Brummel aaca18c6f6 audit: net-r cycle close — ledger reconciled, drift items resolved
Architect review over dd23ea3..HEAD (the #234/#152 cycle, 6 commits).

What holds (architect-confirmed): CostSpec is a closed, deny-unknown-
fields vocabulary with typed slots mirroring RiskRegime, field names
conforming to the builders' ParamSpec names; cost-less docs hash
byte-identically (C18); cost threads through the single MemberRunner
seam so every campaign stage nets uniformly; costed families reproduce
bit-identically incl. Carry; the #221-deleted leg is rebuilt optional
(net = gross under the empty model); both cost_graph leaks replaced by
the interned single source (#152).

Resolved this close: [high] the C10 realization notes contradicted
shipped reality — the two 'wired on the run path via --cost-*' claims
carry superseded-annotations, the 0084 carried-debt note a discharged-
annotation, and a new cycle-net-r realization documents the campaign
cost block, the net-by-default decision, the manifest/reproduce
round-trip, and the returned net_r_equity tap; [medium] two dangling
persist_traces_r docstring references reworded to what exists;
[medium] the one-knob-per-cost-node invariant behind the manifest
round-trip is now unit-pinned (exactly one distinctly-named knob per
shipped builder).

Cycle spec and plan (git-ignored working files) discarded per
convention.

refs #234
2026-07-11 12:28:15 +02:00
Brummel c32c01a63c test(cli): archive-gated e2e — a costed campaign nets members and reproduces (#234 task 7)
The closing proof: a GER40 campaign with a constant+vol_slippage cost
block runs to exit 0; every member's net_expectancy_r < expectancy_r
(costs bite), the same doc without the cost block reproduces today's
exact gross numbers (golden parity), net_r_equity.json persists on
request, and the costed family reproduces bit-identically. One plan
literal corrected in place: the member-line filter exact-matches the
line shape so the campaign_run summary record (which nests a family_id)
no longer false-matches.

Verified: e2e green on the real archive, full workspace suite green,
clippy -D warnings clean.

closes #234
2026-07-11 12:16:57 +02:00
Brummel da7d54774b feat(cli): TapChannel::Net — the net_r_equity tap routes and persists (#234 task 6)
TapChannel gains Net; tap_channel routes the full closed vocabulary
(producibility moved to the caller); the trace path drains the sixth
channel and persists the cost-adjusted net curve. A cost-less document
requesting net_r_equity keeps a skip notice, reworded to name the
remedy (add a cost block) — not a refusal. Two deliberate pin moves:
the absence pin re-pins to routed, the skip-notice e2e re-pins to the
reworded form. Post-review: positive write-path e2e added (a costed
GER40 campaign lands net_r_equity.json with the curve differing from
r_equity by the accrued costs).

Verified: routing pin + positive e2e green (real archive), full
workspace suite green, clippy -D warnings clean.

refs #234
2026-07-11 11:46:22 +02:00
Brummel bb1f6c8bdd feat(cli): campaign cost models reach the member run and both re-run sides (#234 tasks 4-5)
cost_nodes_for (beside stop_rule_for_regime) is the one CostSpec ->
bound-builder binding — every component fully bound, so the wrapped
param space stays cost-invariant. run_blueprint_member joins the cost
rows into summarize_r (member metrics genuinely net) and stamps the
cost components into the manifest params; CliMemberRunner threads the
doc's cost. The re-run sides re-derive the model from the manifest:
reproduce via cost_specs_from_params (the #233 stop pattern — a costed
family reproduces bit-identically) and persist_campaign_traces binds
the same model so the C1 drift alarm compares like with like.
Hand-computed pin: a constant cost model yields
net_expectancy_r == expectancy_r - cost_per_trade/|entry-stop| on a
synthetic member.

Verified: full workspace suite green, clippy -D warnings clean; in-loop
spec + quality gates passed per task.

refs #234
2026-07-11 10:41:16 +02:00
Brummel a1afc2fd02 refactor(cli): wrap_r's optional cost leg — behaviour-preserving rebuild (#234 task 3)
wrap_r accepts an optional CostLeg (cost node builders + the tx_cost/
tx_net channels); non-empty rebuilds the #221-deleted wiring — the
cost_graph fed by the executor's four geometry outputs, the vol-proxy
RollingMax/RollingMin/Sub (back from cfg(test) to production) feeding
each vol_slippage component, the interned cost[k].* names, a gated
4-col cost recorder in reduce mode (the summarize_r join input), and
the LinComb(4) net_r_equity curve in !reduce. All callers pass None —
behaviour-preserving; the entire suite green unchanged is the gate.
Co-temporality of cost rows with R rows unit-pinned.

Verified: full workspace suite green unchanged, clippy -D warnings
clean; in-loop spec compliant + quality approved (one held Minor: the
CostLeg doc's forward reference to cost_nodes_for, which task 4 lands).

refs #234
2026-07-11 08:33:22 +02:00
Brummel bdcf0452f4 feat(research,cli): the CostSpec campaign vocabulary (#234 task 2)
CampaignDoc gains an additive cost: Vec<CostSpec> block — a closed,
externally tagged vocabulary over the three shipped cost nodes
(constant/vol_slippage/carry, field names = the builders' ParamSpec
names), mirroring the risk block: serde default + skip-if-empty (cost-
less documents keep their content ids, pinned), DocFault::BadCost
validation (finite/bounds per component), std::cost construct-
vocabulary block + SlotKind::Costs + open-slot mirror, prose arm, and
all CampaignDoc struct literals across crates carry cost: vec![]
(compile-driven).

Verified: aura-research suite green, campaign validate e2e green, full
workspace suite green, clippy -D warnings clean; in-loop spec review
compliant and quality review approved (real diff fingerprint — the
worktree-anchored review prompt works).

refs #234
2026-07-11 07:24:15 +02:00
Brummel dd23ea34a5 test(cli): e2e evidence pin — a project node's open param through sweep and campaign
The defining loop of a genuine external project, proven end-to-end and
green on arrival: a fresh aura-new project authors an OPEN blueprint
over its own node (knob_lab::Scale, factor unbound); --list-axes shows
the axis, a real-data GER40 sweep over it yields two members whose
manifests carry the project-node binding, the family persists and
reproduces 2/2 bit-identically, and one campaign cell over the same
blueprint runs to exit 0 linking a sweep family. No implementation was
needed — the loop already worked; this pin is the previously-missing
evidence (the deepest exercised path before was a single param-less
aura run). The axis-discovery leg is un-gated so the test stays
meaningful on a data-less host.

closes #235
2026-07-11 05:28:36 +02:00
Brummel 7748b53a10 feat(cli,docs): the r_channel OHLC example + ledger amendment C26 (#231 tasks 6-7)
The acceptance proof: hl_channel — a causal Donchian channel (Delay(1)
excludes the current bar, C2) consuming high/low/close roles, built from
rostered vocabulary, shipped as the r_channel/r_channel_open example
pair (builder + regenerator + serialize pins, the r_* pattern). Proven
at three layers: a non-gated loaded-vs-carve equivalence over inline
sources (non-zero-bias hardened per review), the unconditional synthetic
multi-column refusal, and archive-gated CLI e2e — aura run on GER40
end-to-end plus a sweep whose members run and reproduce 2/2
bit-identically.

Ledger: new C26 entry (the binding vocabulary — closed column set +
price alias, name-driven default + campaign data.bindings override,
canonical order = the C4 tie-break, Blockly-litmus argument, the #71
extension point for recorded non-price sources) and the C20/C24
scaffolding-clause annotations: the single-price data weld is retired,
wrap_r's remaining R-scaffolding retirement stays #159.

Verified: all proof tests green, full workspace suite green, clippy -D
warnings clean; independent quality review approved.

closes #231
2026-07-11 05:10:05 +02:00
Brummel 43b1c7ff5d feat(research,registry,cli): campaign data.bindings — the 6b rebind seam (#231 task 5)
DataSection gains an additive bindings block (role -> column; serde
default + skip-if-empty, so binding-less documents keep their content
ids — pinned). Validation is two-tier along the existing line: the
intrinsic tier checks binding VALUES against the column vocabulary
(DocFault::UnknownBindingColumn, alias-annotated display register); the
resolver tier checks binding KEYS against the loaded strategies'
input_roles() (RefFault::BindingRoleUnknown — a key must name a role of
at least one campaign strategy). A cross-surface pin keeps the doc-tier
vocabulary and the CLI binding module from drifting.

CliMemberRunner threads the campaign overrides into resolve_binding on
BOTH halves (column opening and wrap), keeping the C1 drift alarm
comparing like-with-like; the verb sugar passes no bindings (name
defaults rule).

Proof: an archive-gated campaign e2e rebinds price -> open and yields
different realized metrics from the close-bound run; refusal prose
exact-pinned at both tiers.

Verified: full workspace suite green, clippy -D warnings clean;
independent quality review, all findings repaired.

refs #231
2026-07-11 04:35:01 +02:00
Brummel 1d14ebc1cd feat(cli,engine): multi-column opening on every real-data path (#231 task 4)
The six Close-only open sites route through the resolved binding's
column set via open_columns (probe_window, open_real_source,
run_sources/windowed_sources, the campaign member open, the trace
re-run open) — a strategy declaring high/low/close roles gets exactly
those columns, merged in the canonical C4 order. Synthetic data refuses
any binding beyond {close} with the honest --real remedy (the walk
generates a close series only); the sweep/mc family builders ride their
established Err contract, run/walkforward/reproduce refuse exit-1.

Proof at two layers: an engine e2e composes an OHLC blueprint from data
and runs it over four inline VecSources to hand-computed rows (the
merge-order pin), and an archive-gated CLI e2e sweeps a high/low
blueprint over GER40 opening the High and Low columns.

Verified: engine OHLC e2e green, full workspace suite green, clippy -D
warnings clean.

refs #231
2026-07-11 03:37:14 +02:00
Brummel 99a7f6603c refactor(cli): wrap_r declares its root roles from the resolved binding (#231 task 3)
Behaviour-preserving threading: wrap_r's hard-wired
source_role("price") weld is gone — root roles now come from a
ResolvedBinding, one per entry in canonical column order, each fed into
the signal's same-named port; the guaranteed close entry always feeds
broker + executor (whose 'price' is a node-schema port name, not a
role). Every existing blueprint resolves to the identical single-price
plan, so the entire suite is the gate: green unchanged.

Threaded through all callers: run_signal_r, run_blueprint_member (+ its
production and test callers), blueprint_axis_probe (lenient probe
binding), reproduce_family_in, the sweep/oos/walkforward/mc family
builders, CliMemberRunner::run_member, persist_campaign_traces.

Vocabulary role names use their static form at the port seam; only an
override-renamed role leaks its name once per graph build (post-review
polish capping the per-member leak).

Verified: full workspace suite green unchanged, clippy -D warnings
clean.

refs #231
2026-07-11 02:27:05 +02:00
Brummel 595c98b264 feat(cli,ingest): binding module + generalized column openers (#231 tasks 1-2)
crates/aura-cli/src/binding.rs: the closed column vocabulary (open/high/
low/close/spread/volume, price as the close alias) with ResolvedBinding
— one ordered plan both halves of the single-price weld will share.
resolve_binding applies override-wins-over-name-default, refuses unknown
roles / bad override values / the price+close double-alias / a rebound
reserved close role, synthesizes the broker-only close entry, and sorts
into canonical M1Field order (the C4 merge tie-break contract). 12
exact-prose unit tests. Module-wide dead_code allow is scaffolding until
the wrap seam consumes it next.

aura-ingest: open_columns / open_columns_window open any field set in
the given order (None if any field lacks an overlapping archive file);
open_ohlc now delegates with the fixed OHLC order — behaviour-identical,
still guarded by the open_ohlc_seam bit-identity test.

Post-review polish: RESERVED_CLOSE_ROLE constant couples the synthesized
close's two sites; dead_code note reworded condition-based.

Verified: binding 12/12, ingest absence test green, full workspace suite
green, clippy -D warnings clean; independent quality review approved.

refs #231
2026-07-11 00:49:22 +02:00
Brummel 00e15b9371 feat(cli): sweep/walkforward --trace writes every member's tap series (#168 refusal lifted)
--trace <fam> now translates through the verb sugar into a persist_taps
campaign-presentation block (was persist_taps: vec![] — the #168 no-op)
and persist_campaign_traces writes EVERY member of a selection-free
swept cell, one subdirectory per member under the cell dir, keyed by
the member's manifest params label in reproduce's exact format
(filesystem-sanitized, ordinal fallback); nominee cells keep their flat
layout. Members re-run at the cell's own stop regime and resolved pip,
streaming one member at a time, with the C1 drift alarm preserved per
member. The walkforward sibling lifts identically; run/mc --trace stay
refused by design (no campaign context / no family-name concept — their
help was already honest). Clap help lines and the chart NotFound hint
restored; the old #168 refusal pins re-pinned to the delivered
behaviour.

Post-review repairs: member_trace_key + cell_member_fanout extracted
pure and unit-pinned (6 tests), persisted_cells counts cells again,
cell-invariant space/geo hoisted out of the member loop, verb_sugar
tail comment corrected.

Verified: headline e2e green (GER40 archive), cli_run 124/0, full
workspace suite green, clippy -D warnings clean; independent spec
review compliant, quality review findings all repaired.

closes #224
refs #168
2026-07-10 22:16:09 +02:00
Brummel fe4ea0a08d test(cli): RED — sweep --real --trace refuses instead of writing tap series
Pins the #224 delivery headline: the #168 refusal lifts and at least
one non-empty tap-series file lands under runs/traces/. Layout-agnostic
on purpose — the per-member key convention is the GREEN slice's call.

refs #224
2026-07-10 21:13:52 +02:00
Brummel e44b0cbb3e feat(cli): warn when the loaded project dylib is stale against the source tree
Editing a project's src/ without cargo build silently ran the previous
dylib with plausible stale numbers — the role-2 authoring-loop footgun.
The loader now compares the newest mtime under src/ + Cargo.toml against
the dylib's mtime right after the ArtifactMissing check and prints one
stderr warning naming both timestamps (hand-rolled Hinnant
civil_from_days render, no new dependency), then proceeds unchanged:
warn, never refuse — a stale run is still deterministic and
manifest-stamped. Every I/O failure in the scan degrades to
skip-and-continue; staleness can never become a refusal. Fresh builds
stay silent (inverse unit-pinned).

Verified: headline e2e green, project_load 8/8, full workspace suite
green, clippy -D warnings clean; independent quality review approved.

closes #237
2026-07-10 21:04:00 +02:00
Brummel 143c87c0ae test(cli): RED — a stale project dylib loads silently with no staleness warning
Editing project src/ without cargo build silently runs the previous
dylib with plausible stale numbers (role-2 authoring-loop footgun).
Pins: source mtime newer than the dylib -> stderr warning naming both
timestamps, run proceeds unchanged (warn, not refuse — a stale run is
still deterministic and manifest-stamped).

refs #237
2026-07-10 20:16:02 +02:00
Brummel 99237e1d0a feat(std): fill the by-chance vocabulary gaps — Const, Div, Abs, Max, Min
Five new rostered node types (count-pin 23 -> 28, both the in-crate
shape test and the cross-boundary CLI vocabulary e2e): Const is unary
with an f64 'value' param — the clock input drives it, its value is
ignored, since a zero-input node never evaluates in the total-push
engine — mirroring EqConst's constant-as-param pattern; Div is binary
IEEE-754 (x/0 -> signed inf, 0/0 -> NaN, unit-tested, no error
channel); Abs unary mirroring Sqrt; Max/Min binary pairwise, distinct
from the windowed RollingMax/RollingMin.

Acceptance proof: the committed executable spec composes an RSI-class
gain/loss-split-and-ratio signal purely from blueprint data through
std_vocabulary and runs it to hand-computed RS values — the r_meanrev
constant-folding workaround is no longer forced.

Verified: headline test green, aura-std 163/0, full workspace suite
green (independent mini-verify), clippy -D warnings clean.

closes #236
2026-07-10 19:59:10 +02:00
Brummel c1971d4ed7 fix(cli): the graph render positional shape-discriminates op-scripts like its siblings
aura graph <op-script.json> exited 2 mislabeling the valid JSON array
as 'blueprint document is not valid JSON' — dispatch_graph parsed the
render positional with the envelope-only blueprint_from_json while the
introspect siblings (register, --params, --content-id) route through
composite_from_any. The render positional now uses the same
shape-discriminating path (made pub(crate)); an op-script renders
byte-identically to its built envelope, and a genuinely-bad document
gets the shared honest error including the unresolved-namespace hint
(preserved verbatim in composite_from_any's envelope arm).

Verified: RED test green, full workspace suite green (independent
mini-verify).

closes #226
2026-07-10 19:20:33 +02:00
Brummel 0d9fdf8f05 test(cli): RED — the graph render positional rejects op-scripts its siblings accept
aura graph <op-script.json> exits 2 mislabeling the valid JSON array as
'blueprint document is not valid JSON'; register/--params already
shape-discriminate via composite_from_any. Pins render parity: op-script
and its built envelope render byte-identically at exit 0.

refs #226
2026-07-10 18:58:45 +02:00
Brummel fb145f5a15 fix(cli): reproduce re-derives the stop regime from the member manifest
reproduce_family_in hardcoded the default StopRule::Vol{3,2.0} at both
the param-space probe and the member re-run, so a family minted under a
non-default vol-stop (campaign risk-regime cell, or wf/mc/generalize
with --stop-length/--stop-k) spuriously reported DIVERGED.

New stop_rule_from_params re-derives StopRule::Vol from the manifest's
stamped stop_length/stop_k, falling back to the defaults when the keys
are absent (pre-stamp members) — the same one-directional widening
point_from_params applies to missing manifest params, mirroring
campaign_run's stop_rule_for_regime None arm. The stop rides outside
the wrapped param space, so point_from_params could never recover it.

Verified: RED test green, full workspace suite green (independent
mini-verify), clippy -D warnings clean.

closes #233
2026-07-10 18:53:01 +02:00
Brummel ab14eeed3c test(cli): RED — reproduce re-runs members under the default stop regime
A member minted under a non-default vol-stop (manifest stamps
stop_length/stop_k) spuriously reports DIVERGED on reproduce: both the
param-space probe and the member re-run hardcode StopRule::Vol{3,2.0}.
Closed blueprint isolates the stop as the only differing dimension.

refs #233
2026-07-10 18:34:28 +02:00
Brummel ed1e8a0d10 fix(cli): thread the resolved pip into wrap_r so real-data runs hit the graph broker
wrap_r hardwired SimBroker::builder(SYNTHETIC_PIP_SIZE); the resolved
geometry-sidecar pip reached only the manifest broker label, so every
real-data run computed total_pips/max_drawdown at 0.0001 regardless of
instrument (GER40 pip=1: inflated 10^4) while the label claimed the
real pip.

wrap_r now takes pip_size. Real-pip call sites pass the resolved pip:
run_signal_r (resolve_run_data reordered before the wrap),
run_blueprint_member, and the campaign trace re-run in
persist_campaign_traces (instrument_geometry lookup mirroring the
run_member refusal — required so the C1 drift alarm compares re-run
and nominee at the same pip). Probe-only call sites (param-space and
axis probes; metrics discarded) keep SYNTHETIC_PIP_SIZE explicitly.

The two golden stitched_total_pips pins in cli_run.rs rescaled by
exactly the 10^4 bug factor while n_trades/expectancy_r stayed
byte-identical — confirming R metrics were never pip-contaminated.

Alternative rejected: dropping pip-denominated metrics from real-data
reports — the manifest already stamps the resolved pip, so running the
graph at it restores label truthfulness without shrinking the report.

Verified: full workspace suite green (0 failed incl. the real-data e2e
suite), clippy -D warnings clean.

closes #232
2026-07-10 18:28:42 +02:00
Brummel eb5fc2145b test(cli): RED — real-data member pips ignore the resolved sidecar pip
The in-graph SimBroker divides by SYNTHETIC_PIP_SIZE regardless of the
resolved instrument pip; the sidecar pip reaches only the manifest label.
Pins the pip-invariance property total_pips * pip == raw price move.

refs #232
2026-07-10 17:30:29 +02:00
Brummel 3016bd6b11 fix(cli): reproduce re-derives real-data families from the member manifest (#229)
reproduce_family no longer hardcodes DataSource::Synthetic: it reads the
family's instrument + window from the persisted manifest and rebuilds the
same local-archive source the dissolved verbs run over; a missing archive
refuses with the verbs' established named-data refusal (exit 1) instead of
silently re-running over wrong data. Sweep families now reproduce N/N
bit-identically; WalkForward families yield a verdict instead of the
empty-OOS-window panic. The synthetic-family path is byte-unchanged.

Accepted minimal duplication: reproduce_family performs its own family
lookup to read the instrument before delegating to reproduce_family_in,
whose signature and unit-test seam stay byte-identical (constraint:
minimal fix).

MonteCarlo reproduce still runs the synthetic seed walk regardless of a
real manifest — pre-existing, semantically distinct (bit-identity over
bootstrap resamples), filed forward.

Verified: both #229 RED tests green over the GER40 archive, full workspace
suite 1107/0, clippy -D warnings clean.

closes #229
2026-07-10 15:32:39 +02:00
Brummel 0408fc41a2 test(cli): RED — reproduce over real-data families diverges/panics (#229)
Two failing e2e pins (skip-on-no-data, GER40 archive present here):
- reproduce_real_sweep_family_re_derives_bit_identically: a --real sweep
  family must reproduce N/N bit-identically; currently 0/N DIVERGED because
  reproduce_family re-runs members over DataSource::Synthetic.
- reproduce_real_walkforward_family_does_not_panic: a --real WalkForward
  family must yield a verdict or a clean refusal; currently panics at the
  empty-OOS-window expect (main.rs:1123) for the same reason.

refs #229
2026-07-10 14:47:23 +02:00
Brummel fc9cf23b87 feat(cli,docs): ship the ganged open examples + document the gang construct (#61 tasks 7-8)
The two open examples now expose their author-intended single knobs — the
closed builders always bound these pairs to one value by hand; the open forms
falsely offered them as independent axes:
- r_breakout_open: channel_hi.length + channel_lo.length -> channel_length
  (the Donchian channel is structurally ONE parameter)
- r_meanrev_open: mean_window.length + var_window.length -> window; the band
  factor stays an independent axis
Regenerated via the emitters (never hand-edited); the closed examples are
byte-unchanged. The carve builders gang in the open branch only.

Test migration: the two axis-namespace pins, five --real e2e invocations
(single gang axis, the 10,20 diagonal for the campaign pair — the mismatched
20,40/10,20 grid was exactly the configuration space the gang retires), and
the param_stability row counts (4 -> 3). The r-sma walkforward golden anchor
is untouched (it never swept a ganged pair).

Docs: authoring-guide gains the seventh op + the third param state
(open/bound/ganged) + the gang wrap note; README op list + Axis concept;
glossary gang entry; ledger C24 records the gang verb and the pre-ship
Tier-2 dormancy (no format-version bump while no out-of-repo reader exists).

Verified: full workspace suite 1104/0 (--real e2e included, local data
present), clippy -D warnings clean, cargo doc clean; live acceptance:
introspect --params prints channel_length:I64 alone / window:I64 +
band.factor:F64.

closes #61
2026-07-10 14:07:22 +02:00
Brummel bc88e18247 feat(engine,cli): GraphBuilder gang cadence + render carrier (#61 tasks 5-6)
Builder: NodeHandle::param -> ParamRef, GraphBuilder::gang, build() resolves
the gang table (collect-then-reject, BuildError::{UnknownParam,AmbiguousParam,
BadGang}) and terminates in Composite::with_gangs — the same predicate the
op-script and serde boundaries use. C24 lockstep extended: an op-script gang
replay serializes byte-identically to the GraphBuilder twin (FlatGraph edges +
sources + canonical JSON).

Render: model_to_json emits a per-scope gangs fragment (name, kind, [node,pos,
"param"] members) for ganged scopes only — the gang-free model stays
byte-identical (model_golden untouched). graph-viewer.js annotates a ganged
open param with its public knob name; guarded by a viewer_gang_param.{mjs,rs}
pair RED-first per the established viewer_bound_param convention (a welcome
extra beyond the plan's file list).

Ratified deviation: the model's kind string uses the serde form ("I64") — the
plan's implementation snippet (kind_str -> "i64") contradicted its own test;
resolved toward the test + the blueprint_serde form. Two quality nits held as
principled plan-holds (inline resolution block; the unreachable unwrap_or
placeholder check_gangs refuses first).

Verified: aura-engine lib 264/0 (model_golden, lockstep, builder, carrier all
green), viewer_gang_param 1/0, workspace clippy -D warnings clean. Slice
resumed across a transient API failure via workflow resume (cached prefix).

refs #61
2026-07-10 13:03:10 +02:00
Brummel 521cfc8b5b feat(engine,cli): gang serde + identity + the gang construction op (#61 tasks 3-4)
Serde: CompositeData carries an additive-optional gangs section (omitted when
empty — every existing v1 document round-trips byte-identically, NO
format-version bump per the #61 decision; BLUEPRINT_FORMAT_VERSION doc now
records the pre-ship Tier-2 dormancy). project() emits gangs canonically
(members by (node,pos), gangs by first member) so declaration order never
reaches the content id; reconstruct() re-mints through Composite::with_gangs
and a corrupted section refuses as the new typed LoadError::Gang.
strip_debug_symbols blanks gang + member names, keeps (node,pos,kind) — the
identity id stays gang-name-blind and gang-structure-sensitive.

Construction: Op::Gang { as_name, into } with the feed-style two-phase eager
arm (member resolution against the post-bind schemas, kind uniformity,
single-gang membership; OpError::{GangKindMismatch,AlreadyGanged,GangArity});
finish() routes the assembled composite through with_gangs, and gang-name
collisions surface via the existing injectivity gate on the projected space.
CLI: OpDoc::Gang ({"op":"gang","as":…,"into":[…]}), kind_label/From/
format_op_error arms, and format_load_error phrases gang faults as prose
(gang_fault_prose) instead of leaking Debug — a ratified improvement over the
plan bytes.

Held quality nit (principled plan-hold): gang()'s inline collect-then-reject
param resolution is the module's established convention (bind/try_bind); a
shared cross-crate helper is out of this task's scope.

Verified: workspace build clean, aura-engine lib 258/0, aura-cli
graph_construct 48/0 (incl. the new gang-op e2e), clippy -D warnings clean.

refs #61
2026-07-10 11:57:16 +02:00
Brummel 962b249814 fix(cli): aura graph rejects a bad blueprint arg, not a silent sample (#28)
The #28 cycle-close audit found aura graph <file> silently rendered the embedded
sample at exit 0 when the positional named an unreadable blueprint (typo,
nonexistent path, wrong extension): is_blueprint_file returns None both for "no
positional" and for a bad arg, and dispatch_graph's None arm conflated them —
re-introducing the exact #28 "I asked for my graph, got the sample" surprise in
the error path, and diverging from every sibling verb (a bad blueprint arg is a
usage fault, exit 2, per C14). Split the None arm: no positional -> the sample
(unchanged); a positional that is not a readable .json -> a usage error naming
the arg, exit 2, mirroring dispatch_run.

Also close the paired doc drift the audit flagged: the README blueprint-verb
table and the authoring guide now document the aura graph <file> render surface,
absent while graph was the last sample-bound verb.

refs #28
2026-07-10 04:10:02 +02:00
Brummel 692760d3b0 test(cli): RED — aura graph with a bad blueprint arg must be a usage fault (#28)
The #28 cycle-close audit found the just-shipped `aura graph <file>` silently
renders the embedded sample at exit 0 when the positional names an unreadable
blueprint (typo, nonexistent path, wrong extension): is_blueprint_file returns
None both for "no positional" (the legit sample default) and for a bad arg, and
dispatch_graph's None arm conflates them — re-introducing the exact #28 "I asked
for my graph, got the sample" surprise in the error path. This pins the fix: a
named-but-unreadable arg exits 2 like its siblings, while bare `aura graph` still
renders the sample.

refs #28
2026-07-10 03:46:30 +02:00
Brummel 518c5e9017 feat(cli): render a consumer's own blueprint with aura graph <file> (#28)
aura graph was the last verb still sample-bound: its no-subcommand arm always
rendered the embedded r_sma_open.json, so a downstream researcher who authored
their own blueprint could introspect it as data but not RENDER it to see
mis-wiring before a run — the Construction-layer milestone payoff only half
realized. Give GraphCmd a first-positional blueprint (args_conflicts_with_subcommands
so it coexists with build/introspect/register), and branch the no-subcommand arm
on is_blueprint_file, mirroring dispatch_run: read the file, reload it via
blueprint_from_json against the project/std vocabulary, and render_html it; the
no-argument form still renders the embedded sample.

The library-packaging half (a downstream-linkable render crate) is dropped — the
ratified consumer surface is CLI-over-blueprint-data (decision on #28). The
nested-composite render is not a concern: the old graph.rs ascii render that
carried the unimplemented! was retired, render_html serializes via model_to_json
with no panic, and #26 is already closed.

closes #28
2026-07-10 03:39:31 +02:00
Brummel 7cc986486f test(cli): RED — aura graph must render a consumer blueprint, not the sample (#28)
aura graph is the last verb still sample-bound: its no-subcommand arm always
renders the embedded r_sma_open.json, ignoring any blueprint path, while
run/sweep/mc/walkforward/generalize consume a consumer blueprint file via
is_blueprint_file. This pins the target: aura graph <blueprint.json> renders
that file's own graph (its inlined model reflects the file's nodes, e.g.
r-breakout's channel_hi), not the embedded sample.

refs #28
2026-07-10 03:14:44 +02:00
Brummel 94cdbf90cc feat(campaign,cli): permit --select plateau in the walk_forward stage (#215)
The campaign walk_forward stage picked its per-window in-sample winner by
optimize_deflated (argmax) unconditionally, dropping the stage's `select`
field, and both the preflight and the `walkforward` verb's `--real` campaign
sub-branch refused plateau:* with a forward-pointer here (the #210 decision-4
deferral; the synthetic non-`--real` sub-branch has accepted plateau since
#173). Honour plateau selection when no gate precedes the wf stage.

- Preflight: the plateau-in-wf refusal is narrowed to gate-preceded stages.
  With no preceding gate the survivors are the full sweep grid, so the
  parameter lattice (grid.axis_lens) is exact and optimize_plateau is sound; a
  gate filters survivors below the grid, breaking the lattice, so plateau after
  any gate stays refused. The refusal is static — document validity must be
  data-independent (a gate's filtering depends on the data) — and keeps the
  existing "a gated survivor subset has no parameter lattice" message, which now
  describes exactly the only case it fires.
- Executor: run_walk_forward_stage threads the per-cell grid.axis_lens and picks
  each window's in-sample winner via select_sweep_winner — the same dispatch the
  sweep stage uses. (Argmax, deflate=true) is byte-identical to the old
  optimize_deflated call, so the argmax grade and content-id pins are unchanged;
  the plateau arms read axis_lens and ignore deflate.
- CLI: the walkforward verb threads --select through translate_walkforward /
  run_walkforward_sugar (a walkforward-local param, not the shared four-verb
  SugarInvocation), mapping the parsed Selection to the research SelectRule via
  select_rule_of; the refusal and its select_is_plateau helper are deleted.
  Scope is walkforward-only — the mc verb's wf-stage select is untouched.

The plateau:worst grade anchor (stitched_total_pips = -9683776.67) is distinct
from the argmax anchor (-10398606.67), proving the plateau path genuinely
changes winner selection rather than falling back to argmax. A gate-free plateau
process validates clean while a gate-preceded one is refused (a new
campaign-validate e2e). Argmax defaults stay byte-identical.

closes #215
2026-07-10 03:05:35 +02:00
Brummel 67f98b6bc5 feat(cli): swap the scaffold's starter node for a param-bearing Scale (#183)
The `aura new` scaffold shipped one node, a param-less Identity, so a
downstream author had no public example of a build closure consuming a
bound param — the highest-value gap the project-environment fieldtest found
(F2): a newcomer could copy a param-less node but had to read engine source
to write their first *tunable* one, which the consumer contract forbids.
Swap that single starter node for a Scale node (mirroring the std gain
operator): its schema declares a `factor` ParamSpec and its build closure
reads the bound value (`|p| Box::new(Scale::new(p[0].f64()))`), and the
starter blueprint binds `factor` so `aura run` exercises the bound-param
path with zero hand-editing.

factor is bound to 1.0 — a neutral identity default the newcomer then tunes,
not the only C7-safe value (only |factor|>1 would push the terminal `bias`
output past the bounded [-1,+1] contract; the knob is genuinely read and
multiplied each cycle). The param-reading closure is the copyable template
content #183 delivers; a hypothetical `|_|` regression is caught by the
scaffold unit test's source guard, and tests/project_new.rs re-validates the
swap by building and running the scaffold end to end.

Template-side only (#181): the frozen 0102 demo-project fixture is untouched.

closes #183
2026-07-10 00:52:29 +02:00
Brummel 93a2e2bb60 test(cli): RED — scaffold's starter node must read a bound param (#183)
The `aura new` scaffold ships one node, a param-less Identity, so there is
no public example of a build closure consuming a bound param — the F2 gap
the project-environment fieldtest rated its highest-value authoring miss.
This pins the target: the rendered starter lib.rs declares a ParamSpec and
reads p[0] in its build closure, and the starter blueprint binds `factor`
on the project's own node, so `aura run` exercises the bound-param path
with zero hand-editing.

refs #183
2026-07-10 00:17:47 +02:00
Brummel f2526b1720 docs(cli,ledger): #168 cycle-close audit — reconcile the --trace surfaces (closes #168)
The #168 refusal left sibling --trace surfaces still lying and the design ledger
still describing CLI --trace as a live persist path (surfaced by the cycle-close
architect drift review). Reconcile them to record reality:

- the chart NotFound hint no longer points at `aura run --trace` (itself refused);
  it names the live trace-writer (a campaign document's persist_taps presentation).
- RunCmd/McCmd --trace help stop promising persistence (both flags already refuse).
- README drops --trace from the live naming flags.
- the ledger records the CLI --trace retirement: per-member --trace was never
  wired to the blueprint sweep (`let _ = persist`) and was silently dropped at
  #159/#220 — it never migrated as the old HISTORY note claimed; the single live
  TraceStore::write is the campaign persist_taps; restoration is deferred to #224.

Workspace suite green (62 groups / 0 failed), clippy -D warnings clean.

closes #168
2026-07-10 00:04:27 +02:00
Brummel abbfbdc208 fix(cli): refuse --trace on sweep/walkforward instead of silently dropping it (#168)
The clap help promised "persists each member's/OOS window's taps" and the chart
NotFound hint recommended `aura sweep --trace`, but every blueprint-mode arm
discarded the flag (run_blueprint_sweep's `let _ = persist`, the --real sugar's
persist_taps: vec![]) — the surface advertised trace-recording the code dropped.
Refuse --trace up front on both verbs (exit 2, "--trace is not yet available on
<verb>; see #224"), mirroring the existing aura run / aura mc refusals; drop the
false promise from the two help lines and the chart NotFound hint. Delivering
per-member trace-writing is deferred to #224.

Collateral: four synthetic-sweep tests that used --trace as a family-name handle
move to --name (same handle, still accepted); the walkforward name/trace test now
pins the up-front #224 refusal.

Workspace suite green (62 groups / 0 failed), clippy -D warnings clean.

refs #168
2026-07-09 23:47:32 +02:00
Brummel 53bbcedeec test(cli): RED — sweep/walkforward must refuse --trace with a #224 pointer (#168)
The clap help promises "persists each member's taps" and the chart hint
recommends `aura sweep --trace`, but every blueprint-mode arm discards the flag
(run_blueprint_sweep's `let _ = persist`, the --real sugar's persist_taps: vec![])
— the surface advertises trace-recording the code drops. Pin the decided honest
behaviour: sweep/walkforward refuse --trace up front (exit 2, naming the flag and
pointing forward to the deferred trace feature #224), mirroring the aura run / mc
refusals.

refs #168
2026-07-09 23:04:12 +02:00
Brummel f6d80486cd fix(cli): gate dissolved verbs on project presence, matching campaign run (#218)
A dissolved verb (sweep/generalize/walkforward/mc --real) run outside a project
silently created a content-addressed runs/ store in the cwd and ran to
completion — Env::runs_root falls back to a relative ./runs with no project, and
the four dispatchers skipped the provenance gate that `campaign run` applies
first (campaign_run.rs:322-333). It is now refused up front: one gate in the
shared validate_and_register_axes chokepoint, after axis validation and before
the first store touch (put_blueprint), via a new AxisRegisterError::NoProject
mapped to exit 1 carrying "<verb> needs a project …".

Placement is load-bearing: axis validation (an UnknownAxis usage error, exit 2)
stays ahead of the project gate, so a malformed --axis still exits 2 regardless
of project — a malformed command is malformed in any environment and already
writes no store. Four per-verb pins assert the refusal (exit 1, "needs a
project", no store); the six exit-2 usage-refusal tests stay green.

Workspace suite green (62 groups / 0 failed), clippy -D warnings clean.

closes #218
2026-07-09 22:40:48 +02:00
Brummel 2162bd6d97 test(cli): migrate dissolved-verb e2e tests into the project fixture (#218 prep)
The four dissolved verbs (sweep/generalize/walkforward/mc --real) are about to
gain a project gate that refuses outside a project (#218). 25 cli_run.rs tests
today run a well-formed verb to completion in a bare cwd and pin its grade /
store effect — i.e. they pin the pre-gate behaviour. Relocate them into the
built project fixture (fresh_project() over the demo-project cdylib, serialized,
store reset per test) so they still exercise the verb path once the gate lands.
Grades are byte-identical in-project (the fixture Aura.toml overrides no
instrument or data path — DEFAULT_DATA_PATH is absolute), verified against the
exact pins; local run 113 passed / 0 failed / 0 skipped with archive data.

Two new tests pin that the in-project run stamps manifest.project provenance
(the sweep chokepoint and generalize's run_blueprint_member path), a path no
cli_run.rs test previously exercised. The built_project/project_lock helpers are
duplicated into this binary to keep the change inside cli_run.rs; a shared
tests/common extraction is a separate hygiene item.

refs #218
2026-07-09 21:17:45 +02:00
Brummel 6e22c98e23 test(cli),docs: risk-axis introspection pins + guide/glossary coverage (#216 slice 2)
closes #216

The remaining introspection e2e pins: the bare-{} envelope probe (the
issue's exact reproduction) lists the optional risk slot; a bound
(non-empty) risk list closes it (no open slots); an EXPLICIT empty
'risk': [] still counts as open (pins the non-emptiness branch, which an
is_none()-based probe would get wrong); the --block std::risk assert is
sharpened to pin the optional rendering and the block doc line (the old
bare contains("risk") matched std::risk trivially).

Docs: the authoring guide's bare-{} transcript gains the risk line, the
§3 worked example gains a two-regime risk section (heading updated), a
prose paragraph explains the structural risk axis (stop defines R;
absent = the same default vol regime the verbs bind), and both validate
transcripts show the new matrix counts. The glossary gains the
'risk regime' entry (canonical term; avoid: stop regime, risk section).

Verified: cargo test --workspace 62 result groups / 0 failures; clippy
-D warnings clean; cargo doc 0 warnings; the guide's transcript strings
grep-match the shipped hint/summary bytes (1/2/1).
2026-07-09 18:19:01 +02:00
Brummel a74c3d116e feat(research,cli): make the risk axis discoverable — probe, roster, validate counts (#216 slice 1)
The structural risk-regime axis worked end to end but was invisible on the
public surface (refs #216): the --unwired probe swept only required slots
(risk is #[serde(default)]), no vocabulary block described RiskRegime, and
the validate summary counted axes/windows but no regimes or cells.

- open_slots_campaign gains its one optional-slot probe: a risk-less (or
  empty/non-array) document lists
  'open slot: risk (optional, list of stop regimes { vol: { length, k } };
  absent = one default regime)'; a bound list closes the slot. Never a
  required flip — content-id parity of risk-less documents is load-bearing.
- CAMPAIGN_SECTIONS gains std::risk (SlotKind::Regimes, required: false) so
  --vocabulary lists it and --block std::risk describes the regime shape.
- The intrinsic validate summary now reports the realized matrix, mirroring
  the exec cross-product exactly (strategies x instruments x windows x
  regimes; empty risk = one default cell, marked '(default)'). Points stay
  a grid property, deliberately not a cell factor.

Sanctioned migrations (recorded on #216): the two unit pins encoding the old
invisibility — open_slots_report_partial_documents_by_path now pins exactly
one open slot on the complete risk-less fixture (doubling as the probe's
unit pin; fixture bytes unchanged), and the presentation-hint test migrates
from full-vector equality to locating its entry (its purpose is the hint
text, not the vector length).

Verified: cargo test -p aura-research (29/0), -p aura-cli --test
research_docs (60/0), clippy -D warnings clean.
2026-07-09 17:51:22 +02:00
Brummel 68317ec95d chore(cli,ledger): #217 cycle-close audit — coverage backfill + stop-knob helper
Cycle-close audit for the stop-default cycle (af8564d RED + 2102305
GREEN). Architect review: what holds — the argv extractors and the
campaign member runner default to the same single-sourced regime
constants; the multi-value refusal and the C10/C20 regime semantics
survive; manifests stamp the resolved stop (C18). Three drift items,
all resolved fix-path here:

- generalize's default path was shipped untested; added the
  generalize_args_from_defaults_missing_knobs_to_the_regime unit test
  mirroring mc's sibling.
- The None=>default / Some=>parse+single-value stop block recurred 4x
  across the walkforward/mc extractors; extracted into the generic
  stop_knob_or helper (refusal strings byte-identical; generalize's
  typed unwrap_or extractor deliberately not unified — no parsing
  there).
- Ledger stop-defaulting note now records the two default
  representations: sweep binds no regime (late-resolved by the member
  runner), the three verbs bind the default eagerly into the document
  — same R behaviour, deliberately different content ids.

Regression gate green, no baseline moved: full workspace suite 0
failures across all test-result groups; clippy -D warnings clean;
cargo doc 0 warnings. No spec/plan working files existed (tdd entry
path — the committed executable spec is the contract).

refs #217
2026-07-09 16:48:49 +02:00
Brummel 210230596e feat(cli): default the stop regime on walkforward/mc/generalize
GREEN side of the #217 executable-spec: --stop-length/--stop-k are now
optional on the three dissolved verbs; each missing flag independently
defaults to the single-sourced regime constants (length 3, k 2.0) at
the argv-boundary extractors — the same regime the campaign member
runner resolves for an unbound regime. The generated campaign document
still binds the regime explicitly, so the default spelling is
content-identical to an explicit --stop-length 3 --stop-k 2.0 (pinned
by registry dedup in the headline test). The multi-value stop refusal
survives byte-identically; sweep is untouched.

Sanctioned contract migrations (decision on the issue, 2026-07-09):
walkforward_dissolved_refuses_missing_knobs now pins the surviving
multi-value refusal; mc_args_from_refuses_missing_knobs pins the
default path. Usage strings name the defaults.

Held quality nit (deferred to cycle close, minimal-slice constraint):
the None=>default / Some=>parse stop block recurs 4x across the
walkforward/mc extractors.

closes #217
2026-07-09 16:38:05 +02:00