Phase 3 of the Stratification milestone (contract C28). `aura run` now
shape-dispatches on the loaded blueprint: a `bias` output takes the
existing strategy path (wrap_r + R evaluation), byte-identical; a
blueprint that declares taps but exposes no `bias` runs bare via the new
`run_measurement` (bootstrap, run, drain the declared taps) with no
SimBroker, no risk executor, and no per-cycle equity/exposure/r
recorders; a blueprint exposing neither is refused. This gives the
measurement layer its own run verb and removes the O(cycles) recording
retention (the measured ~2 GB peak RSS over 12y UK100 M1) the mandatory
R scaffold imposed on a measurement-shaped run.
A new `MeasurementReport { manifest, taps }` (aura-engine::report, beside
RunReport) is the stdout/record shape — no R metrics, since a bare run
has no broker. The manifest's `broker` carries an interim "measurement"
sentinel; the honest `Option<String>` model is deferred to the #147
metric-genericity block, keeping this phase #147-free and the
strategy-path C18 goldens byte-identical.
`run_signal_r` is untouched (the tap machinery is duplicated into
`run_measurement`, not extracted). The measurement arm's closed-blueprint
guard uses `signal.param_space()` rather than `blueprint_axis_probe`,
which welds wrap_r and would panic on a no-`bias` signal.
refs #286
Declared taps (C27, #282) were authorable only by hand-patching the raw
blueprint envelope's `taps` array, where `node` is a raw interior index —
an index miscount was the expected mistake and the root friction behind the
two `aura run` panics the measurement-milestone fieldtest found.
Add a `tap` op to the construction op-script vocabulary — the exact
output-side twin of `expose`. It names its source wire by the dotted
`"node.field"` identifier the op surface uses everywhere (`split_port` +
`resolve_output_field` + `node_index`), so
`{"op":"tap","from":"fast.value","as":"fast_ma"}` replaces patching
`{"node":14,"field":0}`.
Engine (construction.rs): `Op::Tap`, its `tap()` handler (a verbatim twin
of `expose` — same resolvers, same name-dedup, appending a `Tap` to a new
`taps`/`tap_names` namespace), and `OpError::DuplicateTap` (the twin of
`DuplicateOutput`, stricter than the raw envelope, which enforces no
tap-name uniqueness). `finish()` now threads `.with_taps(self.taps)` into
the built `Composite` — fixing a latent drop #282 left (it built
`.with_gangs` but never `.with_taps`, so op-built composites silently lost
their taps).
CLI (graph_construct.rs): `OpDoc::Tap` (externally tagged, `"as"`-renamed
like `expose`) + the `kind_label` / `From` / `format_op_error` arms (the
last compiler-enumerated by the exhaustive match).
No change to the `Tap` / `FlatTap` / `bind_tap` / `aura run` machinery — the
op-declared tap flows through the existing #282 compile + record path
unchanged.
Verification: 7 new tests green (4 engine: resolve+append, bad-port /
ghost-node refusal, duplicate-name refusal, finish-threads-into-FlatGraph;
1 CLI serde round-trip; 2 CLI E2E over the real `aura graph build` /
`aura run` subprocess — one pinning the exact serialized
`"taps":[{"name":"fast_ma","from":{"node":0,"field":0}}]`, one proving an
op-authored tap round-trips through build -> run -> persisted trace). Full
workspace suite green; clippy clean.
The RED-before-GREEN step was verified by a temporary stash of the
production edits (the `Tap` / `with_taps` machinery pre-existed from #282,
so a fresh RED-only test could not be authored) — the new tests fail
without the edits, pass with them.
Docs (the authoring-guide `tap` row, the ledger op-list `tap` line, and a
worked tap-authoring example) are deliberately out of this commit's scope —
they land in #285, the docs follow-up in this same cycle.
closes#284
Two RED tests pinning that `aura run` refuses cleanly (exit 1 + an aura:
message) rather than panicking on a user-authorable blueprint: a tap wire
pointing out of range (run_signal_r's compile .expect swallows
TapWireOutOfRange into a panic, main.rs:1753) and an exposed output not named
bias (wrap_r's g.build().expect panics on UnknownOutPort from the hardcoded
bias weld, main.rs:1667). Surfaced by the measurement-milestone fieldtest.
refs #282
The payoff: a hand-authored blueprint's declared taps become recorded output
from one `aura run`. run_signal_r binds each tap (hoisted into flat.taps after
wrap_r nests the signal) to a fresh Recorder before bootstrap, drains the
series after the run, and persists each as a ColumnarTrace via the existing
env.trace_store() — the same trace surface the campaign path feeds, so the
existing chart tooling reads them. Duplicate tap names are refused (the
caller-owned DuplicateBind dedup) before anything persists. A tap-free run
writes no trace store — byte-identical to today. Sweep/reduce leaves taps
unbound (run_blueprint_member untouched). Covered end-to-end: a persisted tap
series, the tap-free no-write guard, and the duplicate-name refusal.
refs #282
Second half of the #281 clock-enable set. CumSum is the generic
run-length accumulator with O(1) Neumaier-compensated state — the
branching Neumaier variant, not Sma's plain Kahan, so it stays exact
when a large running total absorbs a tiny sample (the standalone
accumulator has no windowed-magnitude guarantee; module doc explains
why the helper is not reused). When is the clock-enable driver: forwards
value iff gate, else a quiet cycle (eval->None), so every stateful
reducer composes gated unmodified — SMA(When(x,g),N) is the gated SMA.
Rosters CumSum and When (both count pins 31->33) and ships two engine
E2E fixtures: When forwarding/quiet-downstream through the real
GraphBuilder->compile->run seam, and a quiet When leg suppressing a
whole Resample Barrier(0) group (the deliberate stall semantics).
refs #281
First half of the #281 clock-enable set: Sign promotes the private
sign0() semantics to vocabulary (strict comparisons; NaN and -0.0
fall to 0.0), Select is the stateless 2:1 mux that consumes Bool
back into the F64 plane. Both zero-arg, Firing::Any, rostered
(count pins 29→31); an engine E2E drives both through the real
GraphBuilder→compile→run seam.
refs #281
Replace the per-call O(store) scan in find_blueprint_by_identity with a
persistent identity-id -> content-id sidecar index
(blueprint_identity_index.jsonl, sibling of runs.jsonl). Lookups consult
the index first and VERIFY every hit by loading that one blueprint under
the current roster and recomputing its identity id -- the index is a
cache, never an oracle, so results stay scan-identical under roster
drift, store surgery, or index corruption (up to the same-identity-twin
choice, unspecified in both paths and documented on the fn). Any miss or
failed verification runs the old scan as a full-store repair pass: no
early return, best-effort append of every absent-or-different mapping,
last line wins so later repairs heal stale lines. No write-path, engine,
or caller changes; a pre-index store backfills itself on the first miss;
a read-only store keeps scanning as before.
Decided against put-time index maintenance: it would need a roster-free
doc-level identity function whose equivalence to the loaded-composite
path no green test ratifies (the composite path canonicalizes
structurally on load); the repair path is the load-bearing mechanism
either way. Decision log: the issue's comments.
Verification: 7 new aura-registry tests (backfill, verified-hit-no-walk
via a poison-dir probe, one-pass legacy backfill, stale-line healing,
unloadable-under-roster preservation, garbage tolerance) plus a CLI e2e
(campaign validate resolving an identity ref cold then warm); workspace
suite 1346 passed / 0 failed; clippy -D warnings clean; doc build clean.
closes#191
The two fieldtest findings, resolved:
- friction: --parallel-instruments 0 now refuses in domain terms ("must be
a whole number of at least 1 — it bounds how many distinct instruments
are resident in parallel") via a custom clap value-parser, instead of
leaking Rust's NonZeroUsize wording; the zero-bound test pins the prose.
- spec_gap: the authoring guide's exit-code contract now states that a
gate-emptied cell (a std::gate leaving no survivors) is a successful
cell — exit 0 with an informational aura: note and a truncated
realization — and that exit 3 is reserved for cells that could not be
evaluated. Records observed behaviour; no code change.
Gates re-verified: workspace suite green, clippy -D warnings clean.
refs #277
Resolutions for the architect's four drift items (all fix/document, none
ratified away):
- [medium] execute enforced family-name uniqueness only via the CLI's
validate tier: preflight now refuses duplicate campaign instruments
itself (defense in depth for direct callers), RED-first
(execute_refuses_duplicate_instruments).
- [medium] the parallel cell loop's C1 relationship lived only in the
git-ignored spec: C1 gains a realization note (docs/design/INDEX.md)
recording the chunked instrument-major schedule, the structural
residency bound, and the two scheduling-dependent fatal-path carve-outs
(fault attribution among completed cells; already-written family lines)
— both inert and outside the success-path bit-identity.
- [low] the fatal-path orphan-line honesty is part of that note.
- [low] the --parallel-instruments zero-reject acceptance criterion had no
protecting test: campaign_run_rejects_a_zero_parallel_instruments_bound
pins clap's NonZeroUsize usage error (exit 2).
Gates re-verified: workspace suite green, clippy -D warnings clean.
refs #277
The flip: within each K-instrument chunk (the structural residency bound
from the previous commit), all cells run concurrently on rayon's process-
global pool — the same pool the engine's member/window fan-out already
shares, so cells x members x windows feed one work-stealing scheduler and
the per-cell serial seams (ingest transpose, single-threaded bootstrap)
overlap with other cells' work. Results land in index-addressed slots and
the ordered outputs are rebuilt in document order: byte-identical outcomes
across worker counts and bounds (C1).
Fault routing, restructured for a loop that cannot mid-iterate abort:
member/window faults stay #272-contained inside run_cell; non-containable
faults (incl. registry writes — infrastructure, not cell pathology) latch
an atomic abort flag so unstarted cells never run, and after the join the
lowest document-order fault among completed cells propagates as the run's
error. Nothing partial is persisted at run level on the fatal path.
Property pins: determinism across thread counts and bounds (1-thread/K=1
== 8-thread/K=2), distinct live instruments never exceed K (refcounting
stub runner, 8-thread pool), a dead family store is run-fatal with no
partial campaign-run record, member faults stay contained under the
parallel loop. E2E fixtures (previous commit) pin the CLI prose and
document-order persistence across K.
Verified: workspace suite green, clippy -D warnings clean. rayon enters
aura-campaign under the amended C16 per-case policy (comment in
Cargo.toml, mirroring aura-engine).
closes#277
Two pieces of the parallel-cell-loop iteration (spec: parallel-cell-loop),
still sequential — behaviour-identical results, suite green unchanged:
- validate_campaign refuses a campaign document listing the same instrument
twice (DocFault::DuplicateInstrument + CLI prose). A duplicate enumerates
two identical cells and is the one input that could make two cells share a
registry family name; with distinct instruments the per-cell family name
(all four cell axes + stage ordinal) is unique by construction, so the
upcoming parallel appends can never race one name's run-index assignment.
- aura_campaign::execute flattens the 4-nested cell loop into a document-
order-indexed cell list, groups it by instrument ordinal, and walks
sequential chunks of K instrument groups; results land in index-addressed
slots and the ordered outputs (outcomes, realizations, nominee groups) are
rebuilt in document order. K arrives as a new NonZeroUsize parameter
(default DEFAULT_PARALLEL_INSTRUMENTS = 4), exposed as
--parallel-instruments on aura campaign run and threaded through the
run_campaign chain; the dissolved-verb sugar paths pass the default.
The chunk walk is the structural RAM bound for the parallel flip that
follows: no cell of an instrument outside the current chunk can be in
flight at all (chosen over a semaphore gate — blocking inside pool tasks
risks worker starvation; decision log on #277). Verified: workspace suite
green unchanged, clippy -D warnings clean, E2E fixtures pin the CLI prose
and the document-order persistence across K.
refs #277
closes#275
The CLI half of by-name source binding, plus the ledger record.
Every production run site that carries a ResolvedBinding — `run_signal_r`,
`run_blueprint_member` (sweep / reproduction), and the campaign re-run/trace path —
now keys its opened sources by role name via `key_supply(binding, sources)` and
drives the harness through `Harness::run_bound` instead of the positional
`run(sources)`. `key_supply` pairs each opened column with its declared role from
`binding.entries()` — the one place open-order and role-order meet, made explicit so
`bind_sources` verifies the wiring↔supply role match by name rather than by a
maintained canonical-order convention.
Because the current single-binding CLI derives both the `SourceSpec` roles (via
`wrap_r`) and the supply roles (via `key_supply`) from the same `binding.entries()`,
the bind cannot fail on this path — so the call site asserts the invariant with
`.expect`, matching the adjacent `close_handle.expect("ResolvedBinding guarantees a
close entry")` idiom. The named `SourceBindError` refusal path lives in
`bind_sources` for future decoupled-supply callers (independently-built multi-feed /
recorded sources, #124), where a mismatch becomes reachable.
Ledger: a C4 realization note (supply resolved by name into declaration order, so
supply order is no longer load-bearing; the tie-break guarantee is unchanged) and a
scoped C23 refinement (a `SourceSpec.role` is load-bearing for source binding; every
other flat-graph name stays a non-load-bearing raw-index symbol).
The fieldtest-corpus `SourceSpec` sweep (planned Task 5) was reverted: the fieldtest
packages already fail to build against the current engine API for reasons unrelated
to `SourceSpec` (bootstrap arity, removed `InputSpec`, drifted
`Recorder`/`SimBroker`/`RMetrics`, renamed `Scalar` helpers) — pre-existing bit-rot
from earlier cycles. A partial `SourceSpec` migration neither revives nor further
breaks them, so the scope decision to touch them (premised on their being
otherwise-buildable) was withdrawn; reviving the corpus is separate from #275.
Verification: `cargo test --workspace` green (0 failed; the real-data OHLC channel
e2e exercises the migrated `run_bound` path byte-identically); `cargo build
--workspace --all-targets` clean; `cargo clippy --workspace --all-targets -D
warnings` clean.
closes#272
A member fault (no-data, bind, run, or a caught panic) is now a recorded
per-cell outcome instead of aborting the whole campaign and discarding every
already-computed cell. The incident that motivated this (a 22-instrument
campaign lost ~36 healthy cells ~6.7 min in because Copper had an archive gap)
now completes: the healthy cells persist, the gap cell is recorded as failed,
and the run exits 3.
Direction (owner decision 2026-07-14): run to completion and report
compromised results; no coverage preflight, no window synthesis.
Containment granularity:
- The CELL for a sweep-stage member fault (a grid hole structurally
compromises winner selection, so the whole cell fails).
- The FOLD for a walk_forward member fault (independent time windows): the
surviving folds pool into the family, failed folds are recorded as
StageRealization.window_faults, and the summary names the ratio.
- aura-registry: additive CellFault / CellFaultKind (closed:
no_data|bind|run|panic|window) / WindowFault / CellCoverage, plus
fault/coverage fields on CellRealization and window_faults on
StageRealization — all serde-default-skipped, so pre-#272 campaign_runs
lines parse and round-trip byte-identical.
- aura-campaign: run_cell returns a fault-annotated CellRealization instead of
Err (execute's accumulate-then-append-once tail is unchanged and now
persists every healthy cell + the one run record); a `contain` split keeps
ExecFault::Registry and doc-shape preflight faults global while Member/Window
become per-cell/per-fold. Member panics are caught with
catch_unwind(AssertUnwindSafe) at all three member-run sites (sweep IS/OOS)
and recorded as MemberFault::Panic — a member panic no longer aborts the
process. The wf stage partitions Registry faults (global) from Member/Window
(per-fold) and filters faulted-fold placeholders (the
"faulted-member-placeholder" broker sentinel) out of the persisted family.
- aura-cli: exec_fault_prose gains the Panic arm; CliMemberRunner::window_coverage
derives effective bounds + interior gap months from the #264 archive
primitives; present_campaign prints per-cell failure notes + a completion
summary and threads the failed-cell count; a run with >=1 failed cell exits 3
("completed with failed cells") uniformly across `aura campaign run` and the
dissolved sweep/walkforward/mc/generalize verbs (exit_on_campaign_result).
Usage stays 2, refused-before-running stays 1, clean stays 0.
Tests: the global-abort pins flip to containment (execute + the two wf fault
tests → fold-containment + all-folds-fail-the-cell); new panic-containment
tests on both the sweep path (PanicRunner) and the wf path (this commit adds
the wf mirror the loop left uncovered); a new gapped-archive e2e (one covered
cell + one gap cell → exit 3); the ~14 CLI exit-1 pins move to the exit-3
register; a pre-#272-line byte-identical round-trip guard.
Suite: cargo test --workspace green (1309 tests, 0 failed); clippy clean.
Decision log: #272 comments (fork rationale, the fold Registry/Member split,
the placeholder sentinel, uniform exit-3).
Follow-up (minor, not blocking): the plan under-scoped Task 1 to aura-registry
though the additive fields also touch aura-campaign's exec.rs literals — the
loop absorbed it mechanically; a future plan for a cross-crate additive-field
change should scope every crate's construction sites in the first task.
closes#256
Fork B (owner decision 2026-07-14): the dissolved walkforward/mc
translations' leading sweep executed the full grid over the whole campaign
window and persisted a Sweep family, yet only the enumerated parameter
points ever crossed the stage seam (the wf stage re-sweeps them per IS
window itself). The leading stage is now the fieldless vocabulary block
std::grid: it enumerates, executes nothing, persists nothing.
- aura-research: StageBlock::Grid ({"block":"std::grid"}), schema-strict
parse arm (empty slot list: every key but "block" is refused by the
generic unknown-slot check), PROCESS_BLOCKS entry, intrinsic-tier no-op
arm; the vocabulary test's non-empty-slots guard carries a pinned
std::grid-only exception (a nominal slot would misdescribe the
vocabulary to describe_block consumers).
- aura-campaign: the inter-stage seam is a typed two-armed StageFlow
(points-only vs executed members); gate / mc-per-survivor fence the
points-only arm with defensive PipelineShape faults; preflight admits
std::grid only as the first stage and only immediately before
std::walk_forward (every other neighbor consumes executed reports).
- aura-cli: translate_walkforward / translate_mc lead with
StageBlock::Grid; the two family-shape E2E pins flip to zero Sweep
families; translate_generalize keeps its executed sweep(argmax) — its
generalize stage consumes the argmax winner report as the cell nominee.
- docs: dated #256 amendment in the ledger's verb-dissolution narrative;
the authoring-guide vocabulary transcript gains the std::grid line.
Behaviour preservation: the exact-grade real-data pins
(walkforward_real_e2e_pins_the_exact_current_grade,
mc_r_bootstrap_real_e2e_pins_the_exact_current_grade) pass unmodified —
survivor points reach the wf stage in the same odometer order as before.
Measured (the #256 acceptance measurement; debug build, real GER40 2025,
2x2 grid, `aura walkforward --real`, 3 runs each):
before (a55e4cf): 6.27 / 6.18 / 6.18 s
after: 4.52 / 4.49 / 4.45 s (~ -27% wall clock)
Suite: cargo test --workspace green (0 failed); clippy clean.
Decision log: #256 comments (fork rationale incl. the StageFlow seam and
the slot-guard exception).
The second risk-regime variant vol_tf{period_minutes, length, k}
computes the vol estimator over completed time buckets: an H1 signal
gets an H1-matched stop in the research matrix instead of a hand-scaled
minute stop (the issue's k-inflation workaround retires). Additive
externally-tagged serde (stored Vol documents keep their content ids);
the executor arm wires the VolTfStop primitive via the builder+bind
chain; validate checks period_minutes/length/k positivity per regime.
The member-manifest round-trip holds for both variants (C1): vol_tf
members stamp stop_period_minutes/stop_length/stop_k and the reproduce
path re-derives the VolTf stop whenever stop_period_minutes is present
— never a silent default-Vol fallback. The Regimes slot label and the
unit notes name the new variant (period_minutes IS the stop's
timescale); glossary, authoring guide, and design ledger record the
second variant. Default regime and sugar paths byte-untouched.
Coverage: node units (rollover-only emission, bucket-delta math, the
constant-|delta| correspondence with the per-cycle regime), executor
fold, serde/validate units, the manifest round-trip unit, and a
hostless two-regime e2e (distinct ordinals; vol_tf members stamp their
timescale, vol members do not).
closes#262
Each cost component's knob (cost_per_trade / slip_vol_mult /
carry_per_cycle) now accepts one number for every cell — today's form,
byte-identical on the wire so stored documents keep their content ids
(CostValue's custom serde, the Axis precedent) — or an instrument-keyed
map resolved per cell at member construction. A mixed-scale matrix
(GER40 ~2e4, EURUSD ~1.1) keeps consistent cost units in ONE document:
the N-copies workaround retires and generalize-under-constant-costs
becomes expressible again.
Validation is strict both ways at the intrinsic tier: a map missing a
campaign instrument, or naming one the campaign does not list, refuses
at validate with prose naming the component and instruments (no silent
default — a partial map would reproduce exactly the silent unit
inconsistency the issue reports). The instrument threads through
cost_knob/cost_nodes_for/run_blueprint_member from the cell (run side
and persist re-run side receive the same value — the C1 drift-alarm
lockstep); the manifest stamps the resolved per-cell value under the
unchanged cost[k].<knob> key, and the sugar/reproduce paths pass an
inert instrument (their specs are scalar by construction). Fork
decisions and the discarded first attempt: issue #260 comments.
New coverage: serde round-trips (map form; scalar stays bare), validate
cross-check units, both prose directions pinned, and two hostless e2es
over the synthetic SYMA+SYMB archive — per-cell resolution visible in
the member manifests, and the validate refusal.
closes#260
A Frankfurt-baked zero-arg Session preset (open 09:00 Europe/Berlin as
literals) joins the std vocabulary as SessionFrankfurt, declaring
period_minutes: I64 (default 15) as its one scalar knob — the
single-knob roster pattern the cost nodes established. A data-only
blueprint can now reach a session/time-of-day stream (bars_since_open),
unblocking time-of-day conditioners for the confirm/refute research
model. Roster count pins bump 28 -> 29 in both conscious-act sites.
Fork decision on the issue: the preset variant over a param-vocabulary
extension — a timezone param would need a string kind, breaching the
four-scalar discipline; the preset keeps the vocabulary closed and is
the additive extension the roster-exclusion comment anticipates. The
4-arg Session::builder stays untouched; in_session/session_open_ts
stay deferred (#154).
closes#261
Cycle-close audit over the /boss batch 84e1075..bdafbde (#258#259#264#266#247#265#269). Architect verdict: substance clean — C1
byte-identity (conduit serde-skipped, pinned floats verbatim, uncosted
bit-identical), C10 one-home-for-cost (all four conduit consumers read
the already-netted series), C18 (conduit off the wire, --version and
manifest single-sourced) all hold. Two medium debt items, both fixed
here rather than carried:
- render_bind_error's catch-all Debug-framed the remaining BindError
variants, making #269's no-identifier-on-stderr claim argv-gated
rather than structural. The match is now exhaustive: DuplicateBinding,
EmptyAxis, EmptyRange render as axis-usage prose; a Compile fault — a
blueprint defect, not a usage error — renders as a prose frame with
the compile detail explicitly labelled internal (per-variant
CompileError prose belongs to the graph-build surface, not this
boundary). Unit tests pin the arms.
- The #265 knob-unit notes covered constant and vol_slippage but not
carry.carry_per_cycle, which carries the identical price-unit-vs-R
trap; the note set and its pins now cover all three cost components.
No regression scripts are configured (the architect is the sole gate);
full workspace suite green, clippy clean. Spec and plan working files
discarded per the audit lifecycle.
refs #265 refs #269
render_bind_error's catch-all Debug-framed UnknownKnob although its
payload is already a fully-prosed message; the variant now renders the
payload alone. The two tests that pinned the frame verbatim repoint to
the prose fragments. Exit 2 unchanged. Completes the #247 defect
family: no Rust identifier reaches a user's stderr on either verb path.
closes#269
The rejection must reach stderr as the prose payload alone — no
UnknownKnob(..) Debug frame around it. Fails on today's frame; the
prose fragments already pass, so the sole pinned defect is the frame.
refs #269
The four silently-misreadable cost/risk knobs (cost_per_trade,
slip_vol_mult, vol.length, vol.k) must carry one-line unit/semantics
annotations in campaign introspect --unwired — blocking the price-vs-R
and length-as-timescale misreadings that produce no error today.
refs #265
Both blueprint-sweep bind faults (wrong-kind axis value; an open param
no axis covers) must print prose in the unknown-axis sibling's register
— axis name, kind/knob specifics, a --list-axes pointer — never the raw
Debug structs KindMismatch{..}/MissingKnob(..). Exit stays 2. Fails on
the Debug leakage present today, not on scaffolding.
refs #247
aura --version must print 'aura 0.1.0 (<commit>)', the commit sourced
from the same AURA_COMMIT build provenance the run manifest stamps —
a pre-run freshness probe distinguishing a stale binary from an engine
bug. Fails on the absent parenthesized commit, not on scaffolding.
refs #266
aura data list must print the archive's symbols sorted, one per line,
exit 0 — the discovery step before aura data coverage. Hostless RED over
the synthetic SYMA+SYMB archive; fails on the absent 'list' subcommand
(clap exit 2), not on scaffolding.
refs #264
aura data coverage <SYMBOL> must print the archive's first/last present
month and each interior missing-month range collapsed to one
'missing: YYYY-MM..YYYY-MM' line — the Copper failure mode (first/last
check passes while the window start has no data) made visible before a
campaign runs. Hostless RED over a new gapped synthetic-archive fixture
(fresh_project_with_gapped_data); fails on the absent 'data' subcommand
(clap exit 2), not on scaffolding.
refs #264
The OOS bootstrap conduit RMetrics.trade_rs becomes net_trade_rs and
carries the COST-NETTED per-trade R (r − cost_in_r, trade order). Every
conduit consumer is thereby net-when-costed: the monte-carlo pooled-OOS
and per-survivor bootstraps, the walk-forward oos_r pooling, and the
deflation null-max — which previously compared a net observed statistic
against a gross-resampled null whenever a costed campaign selected on
net_expectancy_r. An uncosted run is bit-identical (empty cost stream
⇒ cost 0.0 per trade), so every existing golden pin stays green.
Design: one conduit, no knob — an explicit net: knob would let a costed
campaign silently produce a gross headline again (the exact misreading
of the issue's evidence). Per-member RMetrics scalar fields stay gross;
net_expectancy_r keeps its meaning. Wire shape unchanged (serde(skip)).
The net series is materialized as a separate expression in summarize_r;
the pinned-float expressions (net_sum, the SQN pair, the lockstep
r_metrics_from_rs copies) keep their tokens verbatim. Fork decisions
and rationales: issue #259 comments.
New coverage, both hostless over the synthetic SYMA archive: a costed
campaign twin shifts the pooled-OOS bootstrap (sweep→gate→wf→mc) and
every per-survivor bootstrap (sweep→mc) below its gross sibling, with
the trade population unchanged; a unit test pins the conduit as the
cost-netted series with the empty-stream degeneracy. Existing conduit
tests renamed with the field.
Verified: full workspace suite green (71 result blocks), clippy clean,
zero bare trade_rs tokens remain, ledger conduit prose updated in place.
closes#259
Every hand-rolled test-sandbox helper keyed its directory on
std::process::id() under env::temp_dir(), so the pre-create wipe never
matched a prior run's path: one directory stranded per helper site per
test-binary invocation, >50k dirs / 15.5 GiB on the 16 GiB tmpfs by
2026-07-13.
The remedy extends commit 84e1075's knob-lab pattern to every site:
fixed, tag-keyed, PID-FREE names under the build-tree tmp anchor, each
site keeping its pre-create remove_dir_all — which now genuinely
reclaims the previous run. Integration/bench targets use
env!("CARGO_TARGET_TMPDIR"); src-internal unit-test helpers (cargo sets
that var only for integration/bench targets) derive the same anchor
from env!("CARGO_MANIFEST_DIR") + ../../target/tmp. Residue is bounded
to one directory per site, on disk instead of the tmpfs, per checkout.
One deliberate exception: project_new.rs's tmp() stays under
env::temp_dir() because two of its tests (new_outside_a_work_tree_*)
need a base genuinely detached from any git work tree, and target/
lives inside the checkout's work tree. Its name carries a per-checkout
hash discriminator instead, so concurrently running checkouts (which
share /tmp) cannot race on the fixed name while each run still
reclaims its predecessor.
tempfile-RAII was considered and rejected (issue #258 decision log):
it cannot cover the process-lifetime OnceLock scaffolds (statics never
drop), adds a dependency, and still strands kill-leftovers on the
tmpfs.
Verified: full workspace suite green (incl. the new RED regression
test temp_cwd_sandbox_does_not_leak_under_env_temp_dir), clippy clean,
sandboxes observed under target/tmp with stable names across runs.
Stale pre-fix PID-keyed dirs in /tmp are not swept by this change and
age out only by manual cleanup.
closes#258
The helper keys its sandbox on std::process::id() under env::temp_dir(),
so the pre-create wipe never matches a prior run's path: one directory
strands per test-binary invocation on the 16 GiB tmpfs (>50k dirs by
2026-07-13). The test pins the property at one representative site: the
returned path embeds no PID and lives off env::temp_dir().
refs #258
built_scale_project() scaffolded ~66 MB (built node crate) into a
pid-suffixed tempdir, so its wipe-before-scaffold never hit a previous
run's copy: one leaked directory per test-binary run. 159 of those
(~10 GB) filled the host's 16 GB /tmp tmpfs to 100%, breaking every
std::env::temp_dir() consumer. A fixed name under CARGO_TARGET_TMPDIR
(real disk, reclaimed by cargo clean) makes the existing wipe reclaim
the previous run — steady state is exactly one directory. Trade-off:
two concurrent cargo-test invocations over the same checkout collide
on the fixed name, the documented pre-existing caveat class for this
suite (#223).
closes#257
probe_window drained a source's entire close column just to learn its
first and last bar timestamp; both live callers (open_real_source and
campaign_window_ms on the campaign trunk) now resolve through
aura-ingest's new archive_extent(): list the symbol's SYM_YYYY_MM.m1
files, load only the boundary months via the existing loader seam, and
walk across gap months forward/backward — O(2 file loads) typical
instead of O(archive). Empty-window refusal semantics are unchanged
(pinned). Two characterization tests captured the resolved windows
against the OLD implementation over the synthetic per-test archive and
stay green unchanged after the swap.
Measured: the probe itself drops 366ms -> 13ms (~28x) on the full
2014-2026 GER40 archive; end-to-end command wall-clock moves only
~2-3% because the sim loop dominates — the fix removes an
O(archive-size) term, not the dominant cost.
closes#252
Env::data_path() handed paths.data to the data layer verbatim, so a
relative value resolved against the invoking process's cwd — aura run
from a project subdirectory silently read a different archive location
than from the root, while runs/ stayed anchored. Relative values now
join onto the project root, matching runs_root() and the [nodes]
pointer resolution; absolute values and the no-project default pass
through unchanged. RED-first unit test pins the root-anchored
resolution.
closes#254
The two generalize E2E tests whose property is no-window span
resolution (shared-window fallback, intersection semantics) no longer
stream the 6.6 GB host archive: fresh_project_with_data() generates a
deterministic two-symbol M1 archive (SYMA 2024-01..08, SYMB 2024-03..06
strictly inside it, so the intersection differs from symbols[0]'s
window on both bounds) in the exact zip/48-byte-record format
data-server reads, plus geometry sidecars, under the per-test project's
paths.data. Both tests drop their local_data_present gates — they now
run on any host, data mount or not.
~35s and ~50s become 0.74s and 1.14s; the full cli_run binary lands at
~25s (211s at the branch base). Full workspace suite: 247s -> 45.7s.
refs #250
Every E2E test that mutated the shared demo-project fixture store now
mints its own tempdir project whose 2-line Aura.toml points at the
once-built fixture crate by absolute [nodes] pointer; the runs/ store
follows the project root, so no two tests contend and libtest's
default thread parallelism applies. project_load keeps driving the
fixture directly (the suite's only proof of the inline-crate routing
arm); its single store-mutating test is that binary's only one, so no
lock is needed there either. This also closes the documented
cross-process race window (#223): there is no shared mutable store
left to race on.
Measured on the data-full host: cli_run 211s -> 57.2s (140 tests),
research_docs 21.1s -> 2.1s (73 tests), project_load green, clippy
clean.
refs #250
The two dissolved-walkforward flag tests assert window-independent
properties (plateau acceptance, per-flag stop defaulting) and now use
the ~135-day window the reproduce e2e already uses. The mc roller-fit
test replaces its full-archive probe sweep with a fixed ~30-day window
inside that same span: the property needs a data-carrying window far
shorter than the 90+30-day roller, not a live-span derivation, and the
archive only grows forward. Measured on the data-full host: 19.6s,
19.6s and 14.5s+ of serialized real-data compute drop to 1.1-1.9s each.
refs #250
RunManifest gains defaults: Vec<(String, Scalar)> — the wrap-prefixed
bound_param_space() of the signal, read after axis reopening, so a
bound param an axis overrode has already left the space and flows
through params instead (disjoint by construction; verified end to end:
a sweep member's overridden fast.length sits in params while
slow.length/bias.scale sit in defaults). params keeps its "what varied"
semantics and stays the reproduce input. One-directional serde
widening (#[serde(default)]) per the selection/instrument/topology_hash
idiom — old records deserialize with an empty defaults; unlike the
Option fields it always serializes, mirroring params. ~20
struct-literal sites across five crates gained the field
(compile-mandated breadth, no behaviour change at those sites).
The C14 ledger records the underlying decision (2026-07-13): generated
outcome records spend redundancy on direct readability (single writer,
cannot drift); authored intent artifacts admit none (every redundancy
is a drift site) — so the fix lands in the manifest, never the
blueprint.
Verification: RED test run_manifest_stamps_untouched_bound_defaults
green; cargo build --workspace; cargo test --workspace green; clippy
-D warnings on the touched crates; binary-level sweep exclusivity
check.
Executable spec for the defaults field: aura run on the fully bound
examples/r_sma.json must report a manifest whose params stays [] (the
"what varied" record) and whose new defaults field carries the
untouched bound params as wrap-prefixed (name, Scalar) pairs in
bound_param_space() order — fast.length I64 2, slow.length I64 4,
bias.scale F64 0.5. Asserts on the stdout JSON (C14: stdout shape ==
on-disk shape), so it compiles against the current RunManifest and
fails on the absent key.
The references to the relocated _open blueprints follow the plan's
two-way split: tests that only need a sweepable blueprint sweep the
closed twins via bound-override axes (#246) with their axis strings
byte-unchanged (the wrap prefix is the blueprint's internal name,
identical across twins); the 12 tests that genuinely exercise
open-param semantics (run/mc closed-guard refusals, subset-axes
MissingKnob, --list-axes open/bound line mix, gang dissolution and
campaign paths) read the fixtures under tests/fixtures/. The
research_docs/project_load store seeds sweep the closed twin (campaign
axis references resolve via the #246 reopen path); the INDEX.md history
note records the relocation.
Two deviations from the plan, both verified against the tree: doc
comments referencing the bare filename (no examples/ prefix) escaped
the global path swap and were reworded to stay truthful; the seed
variable open_bp was renamed closed_bp (the plan kept it, but the
rename touches only the binding and its uses and removes a
misdescription).
Verification: cargo test -p aura-cli --test cli_run (140 passed, 0
failed, real data exercised); --test research_docs; --test
project_load; the four byte-pinned exact grades unchanged; grep-clean
for examples/r_*_open.json across crates+docs; full workspace suite
green.
The four _open blueprints leave the user-facing gallery
(crates/aura-cli/examples/) and become test fixtures; the closed twins
are now the only example corpus (bound params are overridable defaults
since #246, so the closed files serve both run and sweep).
Compile-time embeds follow the move: the emit_* generators and the
shipped_*_serialize byte-pins target tests/fixtures/ (the open fixtures
stay builder-generated and byte-pinned — regenerating after the move
left the tree clean), load_open_r_sma and the axis-probe read the
fixture, and the walk-forward refit test plus the `aura graph` default
sample switch to the closed r_sma.json. graph_construct's open-fixture
tests swap example() -> fixture(), their names dropping the now-false
"shipped example" claim; a new e2e test pins the relocation property
(all four fixtures load and introspect, the gallery holds exactly the
four closed examples).
Intermediate state: cli_run.rs / research_docs.rs / project_load.rs
still reference the old example paths and fail at runtime until the
follow-up migration commit (MIGRATE sites move to the closed twins,
NEEDS-OPEN sites to the fixtures).
Verification: cargo build --workspace; cargo test -p aura-cli --bin
aura; cargo test -p aura-cli --test graph_construct; the three ignored
emit generators re-run byte-stable.
Architect drift review over 0ad8fc6..e4fb64d: the C12 amendment's words
match the shipped behaviour exactly (identity from the authored document,
run/mc closed-guards untouched, bound = overridable default; every
point-binding reload goes through the reopened probe). One medium item
fixed here: the unknown-axis test's rationale comment still described the
retired 'fully bound; nothing to sweep' refusal. Noted as low-priority
debt, unchanged: the four override-derivation helpers across the wrapped/
raw coordinate systems are candidates for consolidation when the next
family boundary appears.
refs #246
Task 6 of the bound-override cycle: SIGNAL_OPEN_JSON_STD and its write
retire — aura new scaffolds signal.json alone, and the project CLAUDE.md
quickstart advertises run AND sweep against that one file (the sweep line
overrides the bound fast.length; --list-axes shows open + bound axes).
The data-only e2e loop proves it end to end: scaffold, run the closed
starter, sweep it via an override axis, and assert signal.json is the
only starter blueprint. The authoring-guide's starter-pair sentence is
retargeted; the last signal_open reference outside the unrelated #196
fixture string is gone.
refs #246
Task 5 of the bound-override cycle. The campaign executor and its gates
accept an axis naming a bound param everywhere the open surface was
already accepted: CliMemberRunner::run_member re-opens the override set
on the cell's probe space and raw reload (raw_bound_overrides_of — the
campaign document's axis names live in strategy coordinates, matched
against bound_param_space directly), the P3 preflight in
validate_before_register threads the same set so the executable-shape
check passes, and aura-registry's validate_campaign_refs treats a bound
name as a valid axis (open space wins the arm order; the kind check
still faults a mismatched axis over a bound path).
The CLI-side helper family is named by coordinate system now:
wrapped_bound_overrides_of (wrap-prefixed, sweep/wf/reproduce) beside
raw_bound_overrides_of (campaign). A fresh post-block quality review
found no correctness defect; its two stale-comment findings (renamed
helpers cited under their old names) are fixed in this commit.
refs #246
Task 3-4 of the bound-override cycle. The family boundary derives the
override set from the invocation's axes (override_paths: an axis in the
open space passes through, one naming a bound param re-opens it in
strategy coordinates, one matching neither is refused with a message
pointing at --list-axes) and threads it through the axis probe and every
per-member reload (blueprint_axis_probe_reopened / reopen_all), so probe
and members stay slot-identical. The 'fully bound; nothing to sweep'
refusal retires; identity stays the authored document (the topology_hash
probe reload is never re-opened).
--list-axes now prints the bound surface after the open knobs — one
'<bp>.<node>.<param>:<KIND> default=<value>' line per bound param,
unconditionally (a bound param is an equally re-openable axis regardless
of how many knobs are open beside it).
Walk-forward (blueprint_sweep_over, blueprint_walkforward_family,
run_oos_blueprint) and reproduce (reproduce_family_in plus the
campaign-side trace re-run) derive the same set silently from recorded
manifest param names (bound_overrides_of), so a family that swept an
overridden bound param re-derives bit-identically.
refs #246
RED pin: graph_build_hints_attach_for_namespaced_ids_in_a_data_only_project
(graph_construct.rs e2e) — the op-script path reports OpError::
UnknownNodeType without consulting the tier-aware hint the blueprint
load path carries.
refs #244
aura new and aura nodes new follow their git init with a best-effort
initial commit (inline committer identity, warning-never-fatal, skipped
whenever the init itself was skipped per #204), so a fresh project's
first run stamps a real commit instead of an empty provenance block.
closes#243
aura new git-inits but never commits, so the first run stamps empty
project provenance. RED pin:
new_outside_a_work_tree_leaves_a_resolvable_head.
refs #243
An inverted or uncovered --from/--to window on a symbol the archive DOES
hold refuses via the symbol-absence message ("no local data for symbol")
instead of naming the window. RED pin:
run_real_empty_window_refusal_names_the_window_not_the_symbol
(archive-gated). Cause: probe_window routes both empty-in-window cases
through no_real_data although has_symbol already proved presence.
refs #242
- New clap family `aura nodes new <name> [--engine-path] [--namespace]`:
scaffolds a node crate as a SIBLING of the project root (Cargo.toml
with the aura-core path-dep, the parameterized Scale starter node,
roster macro, crate-scoped .gitignore + CLAUDE.md, own git repo) and
appends the `[nodes] crates = ["../<name>"]` pointer to the project's
Aura.toml. Refuses outside a project, on any existing [nodes] section
(incl. an empty crates array), and on an existing destination; joins
the load-phase bypass beside `aura new` (a scaffolder must work in an
unbuilt tree).
- The old single-tier scaffold() is gone; the crate templates live on
in scaffold_node_crate; dead-code bridges removed with their callers.
- tests/project_nodes.rs: attach + refusals + namespace override + the
full role-2 loop (scaffold, cargo build, vocabulary resolves
<ns>::Scale end to end).
- tests/project_sweep_campaign.rs fixture reworked to the two-tier
shape (aura new + aura nodes new --namespace knob_lab); the two
campaign acceptance tests are byte-unchanged and green.
refs #241
- NewCmd drops --engine-path/--namespace (they belong to the node-crate
scaffold); dispatch_new emits Aura.toml + .gitignore + two std-only
starter blueprints + CLAUDE.md, git init, no crate, no build step.
- Two starter blueprints because run requires a closed blueprint while
sweep requires open knobs: signal.json (closed, run target) and
signal_open.json (fast.length free) — the scaffolded CLAUDE.md
quickstart lines are both verified live.
- The crate templates survive untouched for the attach verb; old
scaffold()/scaffold_spec() carry a dead-code bridge until it lands.
- project_new.rs realigned: data-only file set, no-build run headline
(stdout report, C1 byte-identity, commit-only provenance, negative
runs/ assert), advertised-sweep headline (#218 gate passes, store
lands project-local).
refs #241
A project-namespaced (::) unknown type id now hints by tier: outside
any project the existing "no Aura.toml was found" text stays
byte-identical; inside a data-only project the hint names the missing
node crate and the attach verb (aura nodes new); a loaded node crate
gets no hint (the vocabulary error carries the message).
refs #241
- ProjectProvenance namespace/dylib_sha256 widen to Option (Tier-1
additive serde): a data-only run stamps commit-only provenance; old
string-field manifests keep parsing.
- The load boundary tier-selects: a [nodes] pointer list in Aura.toml
(one entry honored; MultiCrate / pointer-context refusals) loads the
pointed-at crate; a root Cargo.toml stays the pre-#241 native project,
unchanged; neither means data-only (std vocabulary, project-local
runs/, both-tier provenance).
- ProjectEnv splits into directory facts + Option<NativeEnv>; the crate
pipeline (metadata -> artifact -> staleness -> ABI -> charter)
relocates byte-identical into load_crate.
- e2e fixtures: dataonly-project, multicrate-project,
nested-nodes-project (a pointer crate resolves and stamps its
namespace end to end).
refs #241