78e68e6002fb4a2bb947b5dbc9d49651cb4a3cd7
491 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
78e68e6002 |
test: red for the enumerated family id rejected by load_family (#298)
Registry::load_family_members() exposes the bare name factor while
aura_runner::reproduce::load_family exact-matches only the derived C18
handle '{family}-{run}' — the natural list-then-reproduce workflow
dead-ends on its first attempt (found by the #295 fieldtest; predates
the extraction byte-identically). The test pins: the enumerated
identity of an unambiguous single-run family resolves through
load_family to the same family the canonical handle names.
refs #298
|
||
|
|
5006766579 |
audit: shell-boundary cycle close — drift resolved in-commit
Cycle-close audit (architect: drift_found; design core clean). What holds, architect-confirmed with the guard run and the diff read in full: the shell boundary is real and enforced (full-workspace c28_layering incl. completeness + shell-content checks, acceptance grep clean — no member-run symbol left under crates/aura-cli/src); aura-campaign keeps zero production dependency on the runner; the IC move is verbatim against the anchor; the new library-only E2E pins C1 (two independent runners, byte-identical RunReport). Drift items, all resolved as fixes in this commit: - design ledger: the C28 status sentence "No crate exists yet for measurement" contradicted this cycle's own phase-3 done line — now records aura-measurement as seeded (#295), execution still unbuilt. - the #147 registry-dispatch acceptance tests (IC deflation null, seeded determinism, cross-vocabulary refusal) were pure library properties stranded in the shell's test module — relocated to aura-measurement/tests/registry_dispatch.rs (engine/registry as dev-only, layering-exempt edges), per the cycle's own tests-move- with-their-module rule. - aura-runner's direct production edge on the external data-server tree was structurally invisible (the direction table checks aura-* keys only) and unrecorded — the C28 assembly prose now names it, and a new c28_layering test pins the external tree's entry points to exactly {aura-ingest, aura-runner, aura-cli}. - the C28 shell prose named three buckets that did not cover two real shell residents — it now names the op-script construction front-end (translation) and the `aura new` scaffolder (authoring-tooling, shell-resident like rendering until a second consumer wants it). Deliberately deferred, tracker homes exist (not drift): #297 (process::exit sites in aura-runner's single-run verb paths), #294 (IC duplicate-timestamp semantics), #288-era rustdoc unresolved-link warnings in aura-std/aura-backtest (pre-existing byte-identically at the anchor). No regression scripts are configured (the bench is report-only); the architect review is the gate. Verification: cargo test --workspace green (1474 passed, 0 failed) incl. the relocated registry-dispatch tests and the new data-server guard; clippy --workspace --all-targets -D warnings clean. refs #295 |
||
|
|
170c6c82dc |
feat: full-workspace C28 guard — the shell boundary is structural (#295 part 2)
Completes the shell-boundary cycle (tasks 10-13): - c28_layering now enumerates the FULL workspace: every crate row is reconciled against its real production [dependencies], a completeness assertion pins the table to the on-disk crates/ set (a new crate can no longer escape the guard silently), the shell/assembly imported-by- nothing rule is asserted, and a shell-content check pins aura-cli to no-[lib] plus a closed allow-list of argv/translation/presentation modules. A new domain module in the shell now fails the suite. - Ledger amendments: C28 gains the assembly position (aura-runner) and the corrected import-rule prose (the ratified aura-campaign -> aura-backtest production edge, #291/#292); phase 3 is marked done with the #297 process::exit residual named; a provenance note records this as structural-debt closure, not a demonstrated downstream blocker. C25 records the control-surface decision: the text artifact vocabulary is canonical, every control surface (CLI executor verbs, a future host, an MCP face, a World program) is a projection/executor over it — the which-projection-next ranking deliberately open on #295. C14 gets the executor-face amendment, C26 the binding-module relocation. - aura_campaign::MemberRunner's doc names the shipped default implementor (aura-runner::DefaultMemberRunner) while the column keeps zero dependency on it. - New library-only E2E fixture (aura-runner/tests/world_member_run_e2e): runs the canonical member recipe over a tiny synthetic archive with no aura-cli in the link graph, and pins C1 determinism (two independently constructed runners produce a byte-identical RunReport). - campaign_run.rs module doc: intra-doc MemberRunner link re-anchored to aura_campaign::MemberRunner (the impl moved out with part 1). Verification: cargo test --workspace green (1473 passed, 0 failed); clippy --workspace --all-targets -D warnings clean; cargo doc clean of NEW warnings (the five unresolved-link warnings in aura-std/aura-backtest predate this cycle byte-identically at the anchor — #288-era doc drift, left for the cycle audit). refs #295 |
||
|
|
5c2ac982bc |
refactor: extract the member-run recipe into library crates (#295 part 1)
The shell no longer defines what an aura backtest is. Tasks 1-9 of the shell-boundary cycle — structural extraction, behaviour byte-identical: - aura-runner (new; the C28 assembly position): input binding (the C26 module, moved whole), the C1-load-bearing param<->config translators, harness assembly (wrap_r / run_signal_r / run_blueprint_member and the probe/reopen cluster), axis + risk-regime conventions (bind_axes et al.), the campaign family builders + MC guards, reproduce (process::exit -> RunnerError, shell remaps to identical bytes), the measurement-run orchestration, project loading (Env / cdylib load / charter / staleness, moved whole), the coverage gap-walk (deduplicated onto interior_gap_months), and DefaultMemberRunner — the public implementation of aura_campaign::MemberRunner (ex CliMemberRunner). The MemberRunner trait stays in aura-campaign; the column still imports no harness/data-binding machinery. - aura-measurement (new; seeds C28 rung 3): the IcMetrics/IcKey vocabulary + information_coefficient, verbatim incl. serde derives (#294 duplicate-timestamp semantics move as-is, unresolved). - aura-backtest: the pure per-run scaffold (point_from_params, wf_ms_sizes / fit_wf_ms_sizes, intersect_shared_window). - shell residue: main.rs / campaign_run.rs keep argv/dispatch, argv->document translation, and presentation only; walkforward_summary_json_from_reports now calls the public aura_engine::param_stability instead of its inline twin. - worked example (crates/aura-runner/examples/world_member_run.rs): a World program runs a member backtest through the library alone. Held quality findings, adjudicated: the plan's literal pub-visibility list for binding.rs kept (cosmetic); ~20 refusal sites inside aura-runner (family/member/measure/translate) still process::exit — behaviour-identical today, conversion to RunnerError is filed forward (refs #297); rustfmt line-width drift on re-pathed call sites left for a repo-wide fmt decision. Verification: cargo test --workspace green (1471 passed, 0 failed); cargo clippy --workspace --all-targets -D warnings clean. Byte-identity is pinned by the untouched shell E2E suites (cli_run, measure_ic, run_measurement, research_docs, run_refuses_unrunnable_blueprint, tap_recording — zero assertion edits). Remaining in this cycle: full-workspace c28_layering + shell-content check, ledger amendments (C28 assembly position, C25/C14 control-surface consequence, C26 realization note). refs #295 |
||
|
|
2cf4574e33 |
audit: cycle metric-vocabulary tidy — ledger records the retired #147 deferral
Cycle-close audit (architect: drift_found, design core clean). What holds, architect-confirmed: C18 bit-identity for RunMetrics (prose derived byte-identically, rng order preserved, registry suite unchanged); the C10 wall (monomorphic R-gates, r_based in the R vocabulary, cross-vocabulary refusal tested both ways — no leak); C28 direction (trait in aura-analysis, vocabulary supplied from the outer rungs, zero Cargo edge changes); C1 determinism pinned through the generic path. Drift items, all resolved as fixes in this commit: - design ledger: the C28 #147 disposition now records item 2 SHIPPED (the A1 cut) with A2 still deliberately deferred; the #136 one-implementor clause carries a supersession note (the IC is the second implementor). - seven stale doc comments describing the pre-#147 or mid-cycle state (analysis trait + estimator docs, registry check_r_metric C9 claim, engine re-export note, campaign PER_MEMBER_METRICS roster note, member-seam guard comment, research vocabulary note) updated to the shipped state. - R_BASED_METRICS is now oracle-pinned against RunMetricKey::r_based() in the vocabulary test (it feeds the NonRMetric refusal prose; a divergence would have misreported the R-gate silently). Noted, not amended (history stays): commit 6744f67's body says 'three syntax-only edits' where the test module actually took five. No regression scripts are configured (the bench is report-only); the architect review is the gate. Verification: extended vocabulary test, campaign suites, analysis/research suites green; clippy --workspace -D warnings clean. refs #147 |
||
|
|
0651e16146 |
feat: the IC becomes the second metric vocabulary; rosters single-sourced
The A1 cut of #147 item 2, part 2 of 2 (the consumers), completing what part 1 (the MetricVocabulary substrate) set up: - aura-campaign's RANKABLE_METRICS hand-copy becomes a re-export of the backtest vocabulary's roster; the #190 guard test extends to pin the roster nesting (rankable == vocabulary roster, rankable within per-member, per-member within the research vocabulary) and the legacy exposure_sign_flips alias, so any drift fails a test loudly. The research and per-member rosters themselves stay hand-copies by ratified isolation (#190 accepted residual) — now oracle-pinned instead of trusted. - aura-cli gains IcMetrics + IcKey implementing MetricVocabulary: the second PRODUCTION implementor. The standalone `aura measure ic` reduction now computes its permutation p via the shared null_draw + one_sided_p_laplace building blocks (independent draws instead of chained shuffles — statistically equivalent, deterministic per seed; emitted p values may differ in the last digits from #290's). - Acceptance proven at the registry boundary: an engineered SweepFamily<IcMetrics> deflates through optimize_deflated to a LOW overfit probability and a noise family to a HIGH one (the permutation null, dispatched by the vocabulary — the deflation machinery is no longer baked-in R-only); same seed reproduces the identical FamilySelection; cross-vocabulary names are refused with the family's own roster in the prose (the C10 guard). A new cli_run E2E exercises the legacy alias through the real `aura runs family … rank` boundary. Deviation from the plan, forced by the compiler: IcMetrics additionally derives Debug (unwrap_err on the deflation Result needs it; mirrors RunMetrics). Verification: full workspace suite green (no FAILED, no compile errors), clippy -D warnings clean, C18 goldens (cli_run, measure_ic, c28_layering) green unchanged. A2 — measurement runs as sweep-family citizens — stays deliberately deferred until a concrete campaign demand exists. closes #147 |
||
|
|
6744f670b1 |
refactor: registry deflation dispatch generic over the MetricVocabulary trait
The A1 cut of #147 item 2, part 1 of 2 (the substrate): the registry's ranking/deflation machinery no longer bakes in the R metric vocabulary. - aura-analysis gains the deliberately narrow MetricVocabulary trait (name resolution, roster, direction, value read, one null draw) and the single-sourced one_sided_p_laplace estimator; re-exported through aura-engine's existing seam (no Cargo.toml edge changes anywhere). - aura-backtest now OWNS the R vocabulary: RunMetricKey (was the registry's private Metric enum, exposure_sign_flips alias preserved), r_based (the C10 classification), R_BASED_METRICS/RANKABLE_METRICS rosters, the centred moving-block null_draw, and member_metric_from_rs. - aura-registry: metric_cmp/rank_by/null_best_of_k/member_sd generic over M: MetricVocabulary; optimize/optimize_plateau/optimize_deflated over M: MetricVocabulary + Clone (SweepPoint<M>: Clone bounds on M: Clone); the private vocabulary deleted; check_r_metric/generalization stay monomorphic on the RunMetrics vocabulary (the C10 wall by type); RegistryError::UnknownMetric carries the vocabulary's roster so the refusal prose derives instead of being hand-strung (byte-identical for R). Bit-identity (C18): rng consumption order, centring, Laplace arithmetic, floors, tie rules, and refusal prose are all unchanged for RunMetrics inputs — the full registry suite (75 tests) passes unchanged except three syntax-only edits for the UnknownMetric variant reshape; clippy -D warnings clean on the three touched crates. Alternatives considered: a vocabulary table in aura-core (rejected: inverts C28's supplied-from-outer-rungs direction and cannot host the null model); a parallel measurement-side deflation path (rejected: duplicates the machinery and adds a fifth roster). Part 2 lands the consumers (campaign roster single-sourcing, the IC implementor, acceptance tests). refs #147 |
||
|
|
a9d36ddd70 |
feat: measurement's first deflatable metric — the Information Coefficient
Give a measurement run a standalone post-run quality score: the Information
Coefficient (IC), corr(signal_t, forward_return_{t+h}), with a permutation
null model. Before this a measurement run persisted only tap names and series
(MeasurementReport) — inspectable but not rankable or deflatable. IC is the
first metric measurement supplies: the tap-to-scalar reduction the C28
process-column / metric-interface seam anticipates ("a named metric vocabulary
supplied by measurement and backtest instead of baked in R-only").
Surface: `aura measure ic <run> --signal <tap> --price <tap> [--horizon]
[--permutations] [--seed]`. It reads the run's two recorded tap traces, builds
the forward-return series offline over the recorded price spine, aligns
signal -> return by exact timestamp, computes the IC, and prints it with a
one-sided permutation-null overfit probability (Laplace-smoothed, mirroring the
R deflation arm's formula in optimize_deflated).
Placement follows the C28 ladder: the generic pieces — pearson_corr and a
Fisher-Yates permute over SplitMix64 — land in aura-analysis (the domain-free
statistics foundation, beside resample_block); the IC semantics (forward-return
horizon, the offline join) and the CLI verb land in aura-cli (the shell). The
run path (run_measurement), MeasurementReport, the trace store, and the registry
metric vocabulary are untouched — existing `aura run` output stays byte-identical
(the C18 golden run_prints_json_and_exits_zero stays green).
Causality (C2): the forward-return read close_{t+h} is over a completed run's
recorded trace, not an in-graph node. A look-ahead signal is structurally
impossible in a causal run, so the engineered-vs-noise acceptance property is
unit-tested over hand-built offline series (perfect signal -> IC ~= 1, p < 0.05;
a varying but exactly-uncorrelated signal -> IC = 0, not significant;
constant/degenerate -> the (0.0, 1.0) floor), while the E2E validates the CLI
wiring, report well-formedness, determinism, and the error paths over a real run.
This lands the second, structurally distinct null-model computation (permutation,
beside the moving-block bootstrap behind R). That makes the deferred registry
deflation-vocabulary abstraction (#147 item 2) demand-driven rather than
speculative — the next cycle.
Verified: cargo test --workspace green (incl. the new aura-analysis unit tests,
the aura-cli ic_* unit tests, and the measure_ic E2E); cargo clippy --workspace
--all-targets -D warnings clean; the C18 byte-identity golden unchanged.
closes #290
refs #147
|
||
|
|
a56ab7859d |
refactor: cut the engine's backtest-metrics edge via RunReport<M>
C28 phase 2 (Stratification); realizes item 1 of the deferred #147. The engine's production surface no longer names a backtest-metric type: - RunReport becomes generic over its metric payload M; sweep/mc/walkforward/ blueprint thread the parameter (SweepPoint<M>, SweepFamily<M>, WindowRun<M>, WalkForwardResult<M>). RunManifest stays concrete and engine-owned (its selection: Option<FamilySelection> embeds the foundation-grade analysis type). - summarize and the MC assembly (McDraw/McFamily/McAggregate/RBootstrap/ r_bootstrap/monte_carlo) move to aura-backtest - McAggregate::from_draws reads RunMetrics fields by name, so generifying it is the phase-6 metric-vocabulary abstraction (#147 item 2), still deferred; wholesale relocation is the honest cut. The concrete instantiation lives in aura-backtest as `type RunReport = aura_engine::RunReport<RunMetrics>` + sibling aliases. - the statistics kernel (MetricStats/quantile/resample_block/SplitMix64) moves to the aura-analysis foundation; the engine re-imports it (inner->foundation, legal) and re-exports it so existing consumers stay source-compatible. Dependency inversion in one commit: aura-engine drops aura-backtest from [dependencies] (back to dev-deps for its SimBroker/RunMetrics test fixtures); aura-backtest gains aura-engine. Cycle-free for lib targets - the cycle closes only through the engine's dev-dep edge, the pattern aura-vocabulary already uses. aura-backtest reaches the kernel transitively through the engine re-export, so no aura-backtest -> aura-analysis edge exists (the C28 ladder permits backtest -> {core, engine} only). run_indexed / SplitMix64::next_f64 widened pub(crate) -> pub for cross-crate use. Consumers (registry/campaign/cli/composites/ingest/bench) rewired by import path only, no call-site logic changed. The c28_layering structural test extends to the full ladder: aura-analysis (no aura-* deps), aura-engine ⊆ {core, analysis}, aura-backtest ⊆ {core, engine}. Behaviour-preserving: 1448/0 tests, clippy -D warnings clean, serde shapes byte-identical (C18 - RunReport<M> keeps field order manifest,metrics; the CLI pre-serialized-splice contract unchanged), moved code traceable via git rename detection. Cycle-introduced broken intra-doc links fixed. closes #292 |
||
|
|
94aaa4cde8 |
refactor: split aura-analysis into statistics and backtest metrics
C28 phase 5 (Stratification): the backtest reductions - RunMetrics, RMetrics, summarize_r, r_metrics_from_rs, and the position-event table - move verbatim into aura-backtest::metrics, beside the position_management producer whose record layout they read (the r_col/cost_col lockstep contract is now intra-crate; its stale "aura-std" comment corrected). aura-analysis is reduced to the domain-free half: the multiple-comparison hurdle math (inv_norm_cdf, expected_max_of_normals) and the selection-provenance types (FamilySelection/SelectionMode - kept beside the statistics so RunManifest.selection embeds a foundation-grade type), [dependencies] = serde only. The engine re-export surface is name-unchanged (report.rs re-imports from both crates), so no indirect consumer needed a source edit; aura-backtest moves from aura-engine's dev-dependencies into [dependencies] as a commented TRANSIENT widening of the C28 violation, removed by the phase-2 edge cut on this branch. The campaign drift guard imports its pinned types from their new source crates. Behaviour-preserving: 1448/0 tests, clippy -D warnings clean, serde shapes byte-identical (C18), moved code traceable via git copy detection. closes #291 |
||
|
|
b39fd63396 |
refactor: split the aura-std roster into C28 layer crates
Phase 4 of the Stratification milestone. aura-std held four C28 ladder layers in one roster; this cuts them into layer-aligned, aura-core-only node crates so the import direction is enforced by the crate graph: - aura-std — engine nodes only (arithmetic/logic/rolling + sinks) - aura-market — session, resample - aura-strategy — bias, stops, sizer, cost-model machinery - aura-backtest — sim_broker, position_management - aura-vocabulary — the relocated closed std_vocabulary roster Node modules move verbatim (byte-identical renames); consumers are rewired by import path only. A new structural test (aura-vocabulary/tests/c28_layering.rs) asserts each node crate's [dependencies] stay within its C28-permitted inner set, catching the acyclic-but-outward violation the compiler misses. Behaviour byte-identical: full workspace suite green (1448 tests), no golden edited, clippy -D warnings clean. C28 Status block updated. closes #288 |
||
|
|
34ff539143 |
feat: measurement run verb — shape-dispatch aura run on blueprint output
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
|
||
|
|
592750b122 |
audit: close #284/#285 cycle — GraphBuilder::tap twin restores C24 symmetry
Cycle-close tidy for the #284 (tap construction op) + #285 (docs) cycle. The architect drift review (cb6211c..HEAD) came back drift-clean on the ledger contracts the cycle touched — C27 machinery untouched, a faithful `expose` twin (C25 preserved), lockstep op<->OpDoc / OpError<->format_op_error arms paired, and the #285 docs verified accurate against aura-std/src/session.rs — save one drift item, fixed here. Drift item (fixed): the op-script gained `Op::Tap` but `GraphBuilder` had no `tap()` twin, softening C24 (replay-equals-builder = one construction semantics) and C17 (builder-primacy) — every other op verb has a builder twin. Unlike the op-script's `doc` gap (a ledger-recorded #125 scope cut), this asymmetry had no rationale; a tap is a first-class authoring act on both surfaces, so denying only the fluent builder was an accident of incremental delivery, not a design cut. Fix: `GraphBuilder::tap` (builder.rs), the exact output-side mirror of `expose` — a `taps: Vec<(String, OutPort)>` accumulator resolved at `build()` into `Tap { name, from: TapWire }` and threaded via `.with_taps`. Plus a construction.rs lockstep test `tap_replay_matches_builder` proving the op-script `Op::Tap` replay and the `GraphBuilder::tap` twin serialize to byte-identical blueprint JSON and compile to identical `FlatGraph.taps` — the tap op is the by-identifier face of the same construction semantics, not a second one. Carry-on (architect debt-low, no action): commit 43a1cc4's body phrase "op-built composites silently lost their taps" over-claims a prior drop (no tap op existed pre-cycle); the "latent" qualifier keeps it honest, so the wording is left as-is rather than rewriting an already-landed commit. Verification: full `cargo test --workspace` green; `cargo clippy --workspace --all-targets -- -D warnings` clean. The bench is report-only by project convention and was not run — this cycle adds a construction-time op + docs, no hot-path change. Fieldtest: the #284 `tap` surface is exercised from the public interface by two E2E consumer-path tests (op-script -> `aura graph build` -> `aura run` -> persisted trace) plus two worked authoring examples hand-verified through the built binary; #285 is documentation. The per-cycle fieldtest is satisfied. Spec + plan working files (docs/specs/tap-construction-op.md, docs/plans/tap-construction-op.md) discarded per audit Step 5 (git-ignored, never committed; their design intent lives in the commit bodies + the ledger). |
||
|
|
43a1cc44d3 |
feat(graph): name-addressed tap construction op — declared taps without raw indices
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 |
||
|
|
cb6211c888 |
fix(cli): aura run refuses an unrunnable blueprint cleanly, not by panic (#282)
Two user-authorable blueprint shapes crashed `aura run` with an internal .expect instead of the CLI's clean aura: refusal register: - a tap wire pointing out of range: run_signal_r's compile now handles CompileError (TapWireOutOfRange) as an aura: refusal + exit 1, naming that the blueprint does not compile to a runnable harness. - an exposed output not named bias: a pre-check in dispatch_run (the single loaded-blueprint call site, before the free-knob axis probe that welds the hardcoded bias port and would itself panic) refuses with a message telling the user aura run expects a strategy with a bias output and listing what the blueprint exposes. The sweep path (run_blueprint_member) is untouched. Surfaced by the measurement-milestone fieldtest as the sharp edge of raw-index tap authoring (#284 tracks the name-addressed tap op that removes the miscount class at the source). closes #282 |
||
|
|
e768072ed0 |
test: red for aura-run panics on an unrunnable blueprint (#282)
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 |
||
|
|
cf670caf46 |
audit: cycle-close tidy for the measurement milestone (#280/#281/#282)
Architect drift review over 16ca4e1..HEAD: drift-clean but for one low citation slip, now fixed here — snap_to_nearest_minute's doc cited the record-then-replay boundary as C6 (firing policy); it is C11. What holds (architect-confirmed): C27 taps match the contract (pure declaration, compile-resolve+hoist through the shared lowering remap, caller-built aura-core-only sink, unbound-is-inert, Tier-1 additive serde); the four #281 cells are C8/C6-clean (<=1 record/eval, warm-up None, When's quiet-None is the Resample precedent, barrier stall correct); #280's snap respects C11 (node-local reads only, firing/barrier timestamps untouched, stream verbatim); FlatGraph and validate_wiring lockstep complete. Regression (report-only bench): exit 0, all five surfaces fingerprint OK, deltas within noise on a loaded box — no baseline update, nothing to ratify. Scaling follow-up filed as #283: the single-run tap drain buffers O(cycles) rows (the #138 profile) — fine for the bounded acceptance run, to be folded before a full-history declared-tap study. |
||
|
|
2e8d74903c |
feat(cli): record declared taps on the single-run path (#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 |
||
|
|
93d0ec45f2 |
feat(engine): FlatGraph::bind_tap + TapBindError (#282)
The run-mode-aware bind seam. bind_tap attaches a CALLER-BUILT Box<dyn Node> sink to a declared tap — appending the sink node + an edge from the tap's (node, field) to its input, before bootstrap so the Kahn sort orders it. The engine constructs no aura-std type (it takes a Box<dyn Node>), keeping its production dependency aura-core-only. bind_tap raises UndeclaredTap; duplicate detection across binds is the caller's (the engine keeps no cross-call state), mirroring how bind_sources' DuplicateFeed is a supply-level fault. TapBindError is exported for the CLI. A bound tap records exactly the producer's per-cycle output through the full public pipeline; an undeclared name is refused without mutating the graph. refs #282 |
||
|
|
f3daded514 |
feat(engine): compile resolves and hoists declared taps (#282)
The compile core. resolve_tap_wire resolves a blueprint Tap's interior
{node, field} through the lowering table exactly as OutField re-exports and
edges resolve — a leaf to its remapped index (output-arity checked), a nested
composite through its output remap. Taps are terminal (not re-exported through
the boundary): a flat_taps accumulator threads through the lowering recursion,
so an interior composite's taps hoist to the root FlatGraph.taps with remapped
wires — the case the CLI single-run wrapper relies on. validate_wiring gains a
tap leg (threaded at all three call sites incl. GraphSession::finish) and
CompileError::TapWireOutOfRange names a bad wire. Root-resolve, out-of-arity,
and nested-hoist are pinned green.
refs #282
|
||
|
|
17e125a90a |
feat(engine): FlatTap type + FlatGraph.taps field (#282)
Second slice: the resolved-tap carrier. FlatTap { name, node, field } is the
output-side twin of SourceSpec — its name survives compile, load-bearing for
by-name binding (#275). FlatGraph gains a taps field, threaded (empty) through
every construction site the compiler enumerates. Bootstrap ignores it (taps
materialize as real recorder nodes/edges before bootstrap in a later slice).
Purely additive: the inert-producer pin and full suite stay green.
refs #282
|
||
|
|
7239106f4e |
feat(engine): Tap/TapWire declaration on the blueprint format (#282)
First slice of declared taps: a Tap { name, from: TapWire { node, field } }
is the output-side twin of input_roles — a pure data declaration naming an
interior output wire, no sink endpoint. Composite gains a taps field
(with_taps/taps(), additive) and the serde mirror CompositeData carries it
(Tier-1 additive: no format-version bump, absent-taps documents byte-stable).
Tap/TapWire are re-exported from the crate root beside Role/OutField so
downstream crates can author tap-bearing blueprints.
refs #282
|
||
|
|
b11bcb6202 |
test(engine): pin that a no-out-edge producer runs inertly (#282 substrate)
Current-behaviour pin for the declared-taps design: a producer node with a non-empty output and zero out-edges compiles, bootstraps, and runs — its per-cycle output silently discarded — observably identical to the same graph with the producer removed. This is the exact shape of an unbound declared tap (a study wire left un-recorded in a sweep), and it establishes that the engine already tolerates it: no DCE is needed for correctness, because check_ports_connected gates only input slots and the Kahn sort emits a no-out-edge node as a valid sink. The build-time-elision soundness the tap mechanism will rely on. refs #282 |
||
|
|
c302c5d5a0 |
test(engine): pin When's gated-reducer and barrier-stall semantics
Two integration pins for the clock-enable design (#281), at the raw FlatGraph boot seam. The gated-reducer equivalence is the crux claim: SMA(When(x, gate), N) equals SMA(N) fed the gate-true subsequence directly — When's quiet cycles do not advance the reducer's freshness, so its state moves per forwarded sample, not per cycle. The barrier stall pins that a quiet When member keeps its whole Barrier(0) group from firing that cycle. An in-graph AboveGate fixture derives the Bool gate co-fresh with the value leg. refs #281 |
||
|
|
c8dc57e2aa |
feat(std): CumSum and When cells; roster complete at 33
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 |
||
|
|
40e962db89 |
feat(std): Sign and Select cells — the Bool→F64 bridge pair
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 |
||
|
|
b5c82f5d16 |
fix(std): snap time-bucket/session reads to the nominal minute
Provider M1 stamps carry sub-second jitter around minute boundaries (~55% of days stamp the 09:00 Berlin bar at 08:59:59.xxx), so any consumer dividing or truncating the raw stamp mis-buckets boundary bars: Resample and VolTfStop fold the bar into the previous bucket instead of rolling over, Session demotes the first in-session bar to at-open. Fix per the decision logged on the issue: one shared Timestamp::snap_to_nearest_minute in aura-core (round-half-up, rem_euclid so negative epochs snap correctly), applied consumer-side at the three membership computations. The recorded stream stays byte-verbatim — no ingestion rewrite, record-then-replay untouched; an ingestion-time rewrite was rejected because it would hide the raw time axis from every consumer and could reorder the k-way merge around boundaries. Verified: the three formerly-RED tests pass individually; full workspace suite green; clippy clean. closes #280 |
||
|
|
8db2824552 |
test: red for #280 — jitter-early boundary bars mis-bucketed by raw stamps
Three RED tests pinning the same root cause at its three node sites: Resample and VolTfStop bucket by raw integer division of the jittered stamp (fold-back instead of rollover at bucket boundaries); Session derives wall-clock minutes from the raw stamp (first in-session bar demoted to at-open). A bar stamped sub-second before its nominal minute must be bucketed by the nominal minute. refs #280 |
||
|
|
16ca4e103e |
fix(bench): make the cli_fixed_cost fingerprint invariant across build commits
Caught by the harness itself on its first post-commit run: the aura run record line embeds manifest.commit — the binary's compile-time build sha (crates/aura-cli/build.rs, the invariant-8 audit trail) — so hashing the raw line flipped the fingerprint on every rebuild at a new commit and reported a phantom correctness change (exit 1) at each cycle close. The rebase onto the #191 main exposed it; nothing in #191 touched the record. RED-first: run_line_fingerprint_is_invariant_across_build_commits pins the symptom (two lines differing only in manifest.commit must hash identically); the fix extracts run_line_fingerprint, which parses the line, blanks that one volatile field, re-serializes (serde_json maps are sorted, so the bytes are deterministic), and hashes. A missing manifest.commit is a hard error, so a future record-schema move fails loudly instead of silently passing. The cli_fixed_cost baseline is re-pinned under the new definition — ratify: the fingerprint DEFINITION changed (provenance excluded, results still covered, pinned by run_line_fingerprint_still_detects_result_changes); no measured behaviour of aura moved. refs #251 |
||
|
|
13d8500402 |
audit: cycle-close tidy for #251 — fingerprint honesty, lockstep + surface-list guards
Architect drift review (cycle bfb8648..0389399): no domain-invariant breach. What holds: aura-bench is bin-only dev tooling with zero production reverse deps (invariant 8/C13); every committed surface runs on seeded synthetic data in scratch temp projects (invariant 9); run_reps enforces cross-rep fingerprint identity and the campaign fingerprint carries integer ordinals across the child boundary, consistent with C1 and its cross-command ULP carve-out; zip/clap per-case dependency notes in place (C16), and the ledger correctly gains no entry — tooling lives in the project facts + README. Three drift items, resolved in this commit: - [medium] the winner_fingerprint doc claimed walk-forward regressions fail the bench; the walk-forward stage in fact emits an empty realization record, so coverage is only indirect (via the bootstrap trade count). Doc now states the gap honestly; the engine-side fix is parked on #279. - [low] the fingerprint's lenient JSON parse would silently erode on a registry-schema rename: a new lockstep test builds a real CampaignRunRecord from the aura-registry/aura-analysis/aura-engine types (new dev-deps) and asserts the extracted fingerprint, so a rename breaks this crate's tests instead. - [low] LIBRARY_SURFACES was hand-synced with measure_surface: a new test pins that every listed surface measures without a binary path. Regression gates on the closing tree: cargo test --workspace 1379 green, clippy -D warnings clean, doc build clean, aura-bench run exit 0 with five 'fingerprint OK' at <=2% drift. No baseline moved in this commit — the bench baselines were pinned and verified in the feature commit. refs #251 |
||
|
|
400105e2e2 |
feat(bench): CLI driver, fixed-cost surface, committed baselines + docs
Tasks 7-8 of the bench-harness plan plus the orchestrator's sizing pass — this completes the phase-1 harness (closes #251). Driver: run [--surface|--quick|--reps|--out] / pin over five surfaces, report-only comparison (drift NOTICE at >=10%, never a failure), fingerprint mismatch exits 1 with a baseline-vs-measured block, infra errors exit 2, debug builds refuse to measure. The Bench project-facts line and the crate README (discipline: quiet box, warmup+median, deliberate re-pins) land with it. Workload sizing, measured then corrected: the release engine outran the plan's guesses by ~100x, leaving sub-second child walls where spawn jitter reaches the 10% threshold and flaps the report. Full-size workloads now land in whole seconds — engine 10M bars (~0.7s), ingest 24 months x 20 fresh drains (~0.4s), campaigns on a 24-month archive with a ~22-month window and a 5x5 member grid (sweep ~1.4s, heavy ~5.5s), fixed-cost as in-rep medians over spawn batches (20x help, 10x run). Quick mode keeps the small E2E-fixture shape. The campaign fingerprint folds all three realization layers — sweep winner, gate survivor ordinals, bootstrap trade counts (pooled or per-survivor sum) — so a wrong-result regression in any stage fails the bench, not just a wrong sweep winner. Baselines pinned on this host (commit-stamped), verified by a follow-up run: five fingerprints OK at <=2% drift under load. Verification: cargo test --workspace 1379 green (bench tests all quick-mode; the release-binary E2Es are #[ignore]d behind -- --ignored per the suite-wallclock discipline), clippy -D warnings clean, doc build clean. |
||
|
|
3a25a4ccba |
feat(bench): host probes, child-process runner + the two campaign surfaces
Tasks 5-6 of the bench-harness plan (refs #251). Host context probes (hostname/nproc/loadavg/commit/date, all best-effort), the child runner (release aura binary resolved from cargo's compiler-artifact JSON messages; /usr/bin/time -v wraps the child for CPU% + peak RSS, wall measured in-process), and the campaign surfaces: - a scratch data-only project per repetition ([paths] only — no [nodes]; the std-vocabulary fallback covers the SMA blueprints), synthetic archives for 4 instruments, blueprints seeded via tiny synthetic sweeps, process docs registered through the real CLI; - campaign_sweep (sweep-only process — narrow per-cell fan-out) and campaign_heavy (sweep -> gate -> walk-forward -> 1000-resample bootstrap — wide fan-out): one campaign shape misreads regressions in the other; - fingerprint = winner ordinals parsed from the always-on final {"campaign_run":...} stdout line. The full campaign-sweep determinism E2E (real release binary, real scratch runs) is #[ignore]d out of the default suite — it costs a release build plus child campaign runs, which the suite-wallclock discipline forbids on every cargo test; run it via `cargo test -p aura-bench -- --ignored`. Held quality note: the heavy/quick process docs stay two verbatim consts (plan prescription; a format! builder would trade the uniform &str contract for cosmetic dedup). |
||
|
|
e015317183 |
feat(bench): surface framework + engine/ingest throughput surfaces
Tasks 3-4 of the bench-harness plan (refs #251). The surface framework (1 discarded warmup + N measured reps, median per metric, cross-rep fingerprint identity enforced — a surface disagreeing with itself is nondeterminism, never averaged away) and the two library surfaces: - engine_throughput: 1M-bar seeded walk through an SMA-cross bias graph (GraphBuilder -> compile -> bootstrap -> VecSource -> run), fingerprint = the SeriesReducer finalize row. - ingest_throughput: synthetic 48-byte M1 archive drained via open_columns with a FRESH DataServer per rep, so the measured drain pays zip inflate + parse instead of the in-memory FileCache; fingerprint = count + bit-XOR of the close column. Quick-mode determinism is pinned in-suite for both surfaces (C1 — the harness's premise as currently-green tests); full-size numbers wait for the release driver glue. |
||
|
|
0b116e4105 |
feat(bench): scaffold aura-bench — baseline compare core + synthetic data plumbing
Tasks 1-2 of the bench-harness plan (refs #251): the new dev-only workspace bin crate with the report-only comparison core (BaselineDoc serde, per-metric relative drift, 10% NOTICE threshold, fingerprint-equality exit code) and the deterministic synthetic inputs (seeded LCG price walk, the proven 48-byte Delphi-record M1 zip archive writer, self-deleting scratch dirs). The archive writer is deliberately a third copy of the test-fixture packer (aura-cli tests / aura-ingest each carry one), byte-compatible by construction and pinned here by a round-trip test through the real ingest reader. The binary refuses debug builds (exit 2) — wall-clock numbers from a debug profile measure the profile, not the code — pinned by an E2E spawn test. Temporary #![allow(dead_code)] markers cover the not-yet-wired core until the CLI glue lands; the surfaces and driver follow. |
||
|
|
aa94f51d8a |
fix(registry): converge the identity-index repair pass on twin stores
Drive the RED twin-store pin green: the repair walk now collects its identity->content mappings into a local last-wins map and appends after the walk only the entries whose final walk value differs from the pre-walk snapshot (collect-then-diff-append, decision on #191). The old per-entry comparison against the mid-walk-stale snapshot appended one alternating line per scan-triggering lookup while same-identity twins coexisted — the sidecar now converges to zero growth there too. Lookup answers (first read_dir match) and verify-on-hit untouched. Verification: workspace suite 1347 passed / 0 failed; clippy -D warnings clean. refs #191 |
||
|
|
35865bb934 |
test(registry): RED pin — twin-store identity index must converge, not grow
Executable spec for the audit follow-up on the identity-index cycle: once one full repair walk has run over a same-identity-twin store (two blueprints sharing one identity id, distinct content ids), a further scan-triggering lookup must leave blueprint_identity_index.jsonl's line count unchanged while the twin identity still resolves. RED against the current per-entry snapshot comparison (appends one alternating line per scan-triggering lookup while twins coexist). refs #191 |
||
|
|
88a1c28954 |
feat(registry): index-first identity resolution with self-repairing sidecar
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 |
||
|
|
bfb8648925 |
tidy: fieldtest follow-ups — domain prose for the zero bound, gate-emptied exit semantics documented
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
|
||
|
|
69bb2fc978 |
audit: cycle-close tidy for #277 — preflight duplicate refusal, zero-bound pin, C1 realization note
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 |
||
|
|
cf94377f30 |
feat(campaign): parallel cell loop on the shared rayon pool
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 |
||
|
|
7fa3ef4e26 |
feat(research,campaign,cli): chunked instrument-major cell walk + duplicate-instrument refusal
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 |
||
|
|
98ddcd3595 |
audit: cycle-close tidy for #276 — guard the third append path
Architect drift review for the #276 cycle (b048923..HEAD). What holds: C1 is preserved (the append_write_lock sits at the results-persistence edge, outside the lock-free sim loop); the mechanism is correct (the guard spans the full read-counter-then-write critical section in both locked functions, no re-entrancy via the load paths, poison entered by into_inner); both fixed paths are pinned by property tests. One [medium] drift item, resolved here on the fix path: Registry::append (the flat runs store, runs.jsonl) was the third identical unsynchronized OpenOptions::append + writeln! path, left standing unguarded beside its two newly-guarded siblings — and it is public API with external consumers (C18), so a concurrent caller would hit exactly the #276 corruption. The append_write_lock doc also enumerated the two guarded stores as if exhaustive. Fixed RED-first: the sibling property test failed 2/2 against the unguarded path (same "trailing characters" merged-line corruption), then append takes the same lock and the doc now names all three JSONL append paths. No other finding: put_blueprint / put_doc (content-addressed idempotent writes) and TraceStore::write (per-name whole-file writes) are race-tolerant by shape and correctly outside the append scope. Regression gate: no dedicated regression scripts configured; the project-facts gates all green — cargo test --workspace no failures (the three concurrency property tests among them), clippy --workspace --all-targets -D warnings clean, cargo doc --workspace --no-deps clean. Cycle #276 is drift-clean. refs #276 |
||
|
|
883401309a |
feat(registry): serialize the append write path for concurrent writers
Registry::append_family and append_campaign_run did unsynchronized OpenOptions::append + writeln! to their shared JSONL stores; concurrent writers through one shared &Registry interleaved one append's content and newline writes with another's, merging records onto one physical line and corrupting the store (the #276 RED test failed 3/3 with a "trailing characters" parse error). The planned parallel campaign cell loop (#277) makes exactly that caller shape reachable, so this is its prerequisite. Mechanism: one internal append_write_lock: Mutex<()> on Registry, acquired by both append functions and held across the whole read-the-run-counter-then-write critical section — serializing the write also serializes the read-before-write counter derivation, so concurrent appends under one name cannot double-assign a run index. Mutual exclusion around a per-cell-per-stage microsecond write, not a barrier: cells never rendezvous, and the sim event loop stays lock-free (C1's "without locking" governs the sims; this lock sits at the results-persistence edge outside the hot path). A poisoned lock is entered anyway (PoisonError::into_inner): every store line is independent, so one panicked writer must not wedge all later appends. Scope, per the decision log on #276: in-process synchronization owned by the Registry type itself; cross-process locking stays out of contract (the corrected doc comments now state the actual guarantee). The serial per-cell-buffer alternative stays open to #277 as an ordering decision on top — this lock does not preclude it. Adds the sibling test pinning append_campaign_run under the same concurrent load. Verified: the #276 headline test green 3/3 (was red 3/3), sibling green, cargo test --workspace no failures, clippy --workspace --all-targets -D warnings clean. closes #276 |
||
|
|
494b7885c7 |
test: red executable-spec for concurrent registry appends
Pin the #276 headline behaviour RED-first: N threads sharing one Registry handle and concurrently appending families must leave families.jsonl well-formed — every member of every successful append survives as its own complete, parseable JSON line (none torn, merged into a neighbour, or lost). Today's unsynchronized OpenOptions::append + writeln! interleaves concurrent writers' content and newline writes, so the test fails deterministically (verified 3/3 runs: "trailing characters" merged-line parse error). The sharing topology — one shared &Registry across scoped threads — models the planned parallel campaign cell loop's caller shape and admits any of the issue's candidate fixes. refs #276 |
||
|
|
b048923f1c |
feat(engine): shared rayon pool for the disjoint-parallel core
Replace the per-call std::thread::scope fan-out in run_indexed with one process-wide rayon pool. run_indexed's callers nest — sweep (grid points), monte_carlo (seeds), and walk_forward (window bounds), where a walk_forward window runs an inner sweep — and each std::thread::scope spawned its own available_parallelism() OS-thread batch, so the two levels multiplied to ~available_parallelism()^2 threads (measured 150-240 concurrent on a 24-core box during a 42-cell campaign, ~10x oversubscription). rayon's pool is process-wide and persistent: a nested par_iter work-steals within the same N workers instead of spawning, so live workers never exceed the pool size. Determinism (C1) is preserved bit-for-bit: run_indexed collects via IndexedParallelIterator in enumeration order (not completion order), and the float aggregation (summarize_r / McAggregate / stitch) runs after the ordered collect, so IEEE-754 non-associativity introduces no variance. The existing _with_threads(1) == _with_threads(8) == public determinism tests stay green, now driving local rayon pools of 1 and N workers via ThreadPool::install. Structural inversion: the public sweep/monte_carlo/walk_forward become the core (calling run_indexed on the ambient pool through a shared assemble_* helper); the _with_threads variants collapse into thin, test-only wrappers (#[cfg(test)]) that run the same assembly inside a local pool. run_one stays Fn + Sync (no + Send): run_indexed borrows it in the map closure and the wrappers borrow it into install — passing it by value would move it into rayon's Map and force F: Send, a bound the public callers do not carry (hence the load-bearing #[allow(clippy::redundant_closure)] on run_indexed). Adds a nested-oversubscription regression test (asserts nested run_indexed never exceeds the pool size — the pre-rayon shape ran POOL^2) and an ignored wall-time benchmark. rayon admitted under the amended-C16 per-case dependency policy; it stays within C1 (parallelism across sims, never within one). This is the shared pool only — the first of three steps toward saturating the cores. The diagnosis found the larger loss is the idle valleys (~42% of the box idle: the sequential cell loop and the single-threaded bootstrap), which the pool alone does not fill. Two follow-ups are filed and recorded on #268: the Registry write path is not concurrency-safe (append_family races on a shared families.jsonl), the prerequisite for parallelizing the C1-disjoint cell loop with a RAM budget bounding concurrently-active instruments (the FileCache retains a symbol's parsed files while any reference is live). Verified: cargo test -p aura-engine (276 passed, the C1 determinism tests green), the new oversubscription test green, the benchmark runs (no overflow), clippy -D warnings clean, cargo test --workspace no failures. closes #268 |
||
|
|
88667a65fd |
audit: cycle-close tidy for #275 (by-name source binding)
refs #275
Drift review (architect) over ad4249f..HEAD found the cycle drift-clean at the core
and confirmed what holds: C1 preserved (`run()`'s k-way merge / event loop has zero
diff — `run_bound` only reorders the supply via `bind_sources` then delegates to
`run`, byte-identity pinned); the C4 realization and C23 refinement ledger notes are
accurate and tightly scoped (`SourceSpec.role` is read in exactly one place, sourced
only from the lowering — no leak); and all three binding-carrying production run sites
moved to `run_bound` + `key_supply`, every remaining `.run(` being `#[cfg(test)]`. No
regression harness exists, so the architect is the sole drift gate.
One medium drift item fixed:
- FIX: the CLI's `.expect()` on `run_bound` covered Missing/Extra/Unnamed feed but not
`DuplicateFeed`. The blueprint path does not enforce root-role-name uniqueness (only
the construction op-script path does), so a duplicate-role blueprint would reach
`bind_sources`' `DuplicateFeed` and panic behind the `.expect()` instead of refusing.
Closed at the binding boundary: `resolve_binding` now refuses a duplicate input-role
name — a named `aura:`-register refusal on every CLI run path (single run, sweep, and
campaign all resolve the binding upfront), matching its existing price/close-ambiguity
refusal. The `.expect()` invariant is now genuinely true on all paths. Pinned by
`binding::duplicate_role_name_refuses`.
Two low items:
- `SourceBindError::Display` had no test (its renderer stays dead until a
decoupled-supply caller lands, #124). Added `source_bind_error_display_names_the_role`
pinning the message strings; reachability is deferred by design.
- The reverted fieldtest `SourceSpec` sweep leaves the corpus one more revival break
against the current engine API — carried, as the corpus is pre-existing bit-rot
unrelated to this cycle (documented in
|
||
|
|
9e30805fcc |
feat(cli,ledger): supply CLI run sources by role key; ledger note
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. |
||
|
|
0b620e1f26 |
feat(engine): bind ingestion sources by role key — SourceSpec.role + bind_sources/run_bound
refs #275 The engine-side half of by-name source binding. `SourceSpec` carries a load-bearing `role: Option<String>` — `Some(name)` for a spec lowered from a bound `Role` (carried through the blueprint lowering, previously dropped at compile), `None` for a hand-built raw-index spec. A pure `bind_sources(specs, keyed_supply)` resolves a `Vec<(role, Box<dyn Source>)>` into the positional `Vec` `run` consumes, in `SourceSpec` declaration order, erroring named (`SourceBindError`: Missing/Extra/Duplicate/Unnamed feed) on a mis-bind. `Harness::run_bound` is the ergonomic method the by-name production path calls. The raw-index `run(Vec)` primitive and the k-way merge / per-cycle event loop are untouched: a positional run stays byte-identical (C1, pinned by `run_bound_out_of_order_matches_positional_run`), and every existing `run(Vec)` caller keeps working (it ignores `role`). A `BTreeMap` index makes the Duplicate/Extra diagnostics deterministic. C4's tie-break guarantee ("source declaration order") is unchanged — `bind_sources` emits in `SourceSpec` order, so the merge's source-index tie-break resolves on declaration order regardless of the caller's supply order. The narrow C23 amendment (a `SourceSpec.role` is load-bearing for source binding, the rest of the flat graph stays raw-index) is deferred to the ledger note. The four error-path RED tests assert via `.map(|_| ()).unwrap_err()`: the `Ok` arm `Vec<Box<dyn Source>>` is not `Debug` (`Source` has no `Debug` supertrait), so a bare `.unwrap_err()` would not compile — a plan-byte defect corrected here. Also sweeps the 45 raw-index `SourceSpec { .. }` literals to `SourceSpec::raw(..)` across the workspace (mechanical, compiler-enumerated) and pins that a bound root role's name survives lowering. Verification: `cargo test --workspace` green (0 failed; incl. 5 `bind_sources` cases, the `run_bound` byte-identity test, the role-survives-lowering pin, and the four `.sources`-equality twins under the now-stricter `role` equality); `cargo build --workspace --all-targets` clean; `cargo clippy -p aura-engine --all-targets` clean. |
||
|
|
f0aadb54f8 |
audit: cycle-close tidy for the #256/#272 latecomer block
Drift review (architect) over a55e4cf..HEAD found the cycle substantially clean — C25 closed vocabulary, C1 determinism/behaviour-preservation, the additive-serde widening, exhaustive StageBlock matches, and the uniform exit-3 convention all hold. Two drift items resolved: - RATIFY: the `SilencedPanic` member-boundary panic-hook silencer (added by the #272 implementer, not named in the spec) is a legitimate mechanism — it keeps "recorded, campaign continues" observably true on stderr by suppressing the default crash backtrace around each contained `catch_unwind`. Its mutex serialises only the ref-count/hook-swap (O(1)), never member computation, so C1 disjoint-parallel determinism is preserved. Documented in the ledger's #272 realization paragraph rather than left as undocumented global state. - FIX: `cell_fault_kind_label` hand-wrote the snake_case strings that must match `CellFaultKind`'s serde `rename_all` (two sources of truth an aggregate over campaign_runs.jsonl could silently diverge from). Pinned with a test asserting each label equals the serialized form; the efficient `&'static str` stays. (The #272 commit's own concern-driven doc fixes and the wf panic-containment test landed in d3b1a1a; this commit carries only the two audit-phase items.) Suite: cargo test --workspace green (1311 tests, 0 failed); clippy clean. |
||
|
|
d3b1a1aead |
feat(campaign,registry,cli): per-cell fault isolation — a failed cell is recorded, never a global abort
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. |