Commit Graph

13 Commits

Author SHA1 Message Date
claude 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.
2026-07-14 16:51:31 +02:00
claude ea4e79d73f feat(research,campaign,cli): std::grid — the enumerate-only leading stage
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).
2026-07-14 13:07:40 +02:00
claude bb0b0aeac2 feat(analysis,campaign,registry,cli): bootstrap net R through the process pipeline
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
2026-07-13 21:21:41 +02:00
claude 1ccce9ad32 fix(test): anchor all test sandboxes on reclaimable pid-free names
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
2026-07-13 20:15:31 +02:00
claude 1ebb94c1b8 feat(engine,cli): run manifests stamp untouched bound defaults (closes #249)
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.
2026-07-13 14:20:23 +02:00
Brummel bdcf0452f4 feat(research,cli): the CostSpec campaign vocabulary (#234 task 2)
CampaignDoc gains an additive cost: Vec<CostSpec> block — a closed,
externally tagged vocabulary over the three shipped cost nodes
(constant/vol_slippage/carry, field names = the builders' ParamSpec
names), mirroring the risk block: serde default + skip-if-empty (cost-
less documents keep their content ids, pinned), DocFault::BadCost
validation (finite/bounds per component), std::cost construct-
vocabulary block + SlotKind::Costs + open-slot mirror, prose arm, and
all CampaignDoc struct literals across crates carry cost: vec![]
(compile-driven).

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

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

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

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

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

refs #231
2026-07-11 04:35:01 +02:00
Brummel a0d8aec3f7 feat(campaign): expand the campaign matrix over risk regimes (#210 T3)
The regime becomes the fourth structural coordinate of the campaign matrix, a
kept-separate axis (a peer of window, not aggregated-over like instrument):

- aura-campaign: CellSpec gains `regime` + `regime_ordinal`; the executor
  resolves the regime list (absent/empty risk -> one default cell, ordinal 0),
  runs a cell per (strategy, instrument, window, regime), and keys the nominee
  map by the 3-tuple including the regime ordinal — so generalize aggregates
  across instruments WITHIN a regime, never across regimes. Family names gain a
  `-r{regime_ordinal}` discriminator so two regimes' families cannot collide.
- aura-registry: `CampaignGeneralization` gains `regime_ordinal` (with
  `#[serde(default)]`, so pre-feature stored records still parse — pinned by a
  new backward-compat test).

The existing single-regime execute tests shift their family ids by `-r0`; a new
test pins that two regimes yield distinct `-r0`/`-r1` families. `CellSpec.regime`
is write-only in this slice — its sole reader is the deferred member runner
(the next slice) — a held quality nit, seam-prep as the plan prescribes. Full
workspace suite green.

refs #210
2026-07-06 14:22:59 +02:00
Brummel 2f9f3571ae feat(research): RiskRegime vocabulary + CampaignDoc.risk field + validation (#210 T1-2)
The first slice of the risk-regime structural axis (spec/plan abab1f1):

- aura-research: a serializable `RiskRegime` closed vocabulary (externally
  tagged, sole variant `Vol { length, k }` — a content-addressable mirror of
  the shipped two-variant StopRule, so a future Fixed regime is additive), and
  an optional top-level `CampaignDoc.risk: Vec<RiskRegime>` that absent-
  serializes (the `description` precedent) — content-id parity for every
  risk-less document.
- validate_campaign refuses a non-positive regime (length < 1 or k <= 0, NaN
  included) via a new `DocFault::BadRegime`; the CLI renders it as prose at the
  research_docs seam (register/validate paths).
- The three CLI-seam E2E tests pin that a well-formed risk section passes
  `campaign validate` and a bad one is refused with prose (exit 1, no Debug
  leak) — the field is reachable at the seam a data author drives, not only in
  library units.

Adding the field is compile-breaking at every `CampaignDoc { .. }` literal
(serde default does not cover Rust construction), so this slice also threads
`risk: vec![]` into the sites across aura-cli and aura-campaign — the
field-addition's compile-gate completion the plan should have scoped here. The
BadRegime prose reads `risk[i]: ...` (the implementer's terser wording over the
plan's illustrative string; the spec pinned no exact text). Full workspace
suite green.

refs #210
2026-07-06 14:02:33 +02:00
Brummel c37abb2a27 feat(research,campaign): optional selection group on the sweep stage (#210 T1-T2)
The std::sweep selection triple (metric/select/deflate) becomes ONE
optional group, Option<SweepSelection>, serde-flattened so the wire
form keeps the flat slots: every existing document parses to Some and
its canonical bytes — and content id — are unchanged (golden pin
byte-untouched). A half-populated group is refused by the schema-strict
parser (all-or-nothing); a bare {"block":"std::sweep"} is the
selection-free sweep.

Campaign side: preflight permits a selection-free sweep only as the
pipeline's terminal stage (new ExecFault::SelectionFreeSweepNotTerminal,
prose in aura-cli's exhaustive exec_fault_prose); the executor's
selection-free arm appends the family exactly as before but records no
StageSelection and produces no nominee (recording one would fabricate
intent — #210 fork decision 1).

Introspection follows: the sweep block's schema table marks the group
optional; CLI introspection pins updated. New tests: group-or-nothing
refusals, selection-free round-trip + distinct content id, terminal-only
preflight accept/refuse, selection-free execute realization.

refs #210
2026-07-04 17:43:53 +02:00
Brummel b8ef77b1c7 feat(research,registry,campaign): tap vocabulary + trace_name record contract (0109 tasks 1-2)
Task 1: the tap namespace is a closed vocabulary — tap_vocabulary()
(the wrap convention's four sink names; escalation = new entry or an
authored blueprint sink, never an open node-path namespace) with
DocFault::UnknownTap in validate_campaign (index-addressed, prose
enumerating the vocabulary via the single source), SlotKind::TapKinds
on the persist_taps slot, open-slot hint naming the taps. The 0107
deferral seam test split into its two 0109 pins (unknown tap refuses
at validate; a valid tap passes validate and hits the member-data
seam) — the deferral eprintln itself survives until task 3.

Task 2: CampaignRunRecord.trace_name lands serde-default sparse; the
claim contract holds across the seam — execute stamps the sentinel iff
persist_taps is non-empty, append_campaign_run composes
Some("{campaign8}-{run}") onto the stored line (safe prefix take at
the registry seam), execute mirrors the same derivation onto the
returned copy; None stays None; the hand-seeded byte-identity dump pin
survives (absent -> skipped).

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

refs #201
2026-07-04 03:41:01 +02:00
Brummel a1cd7bc6f5 feat(campaign): monte_carlo stage arm + campaign-scope generalize phase (0108 tasks 3-4)
The mc arm bootstraps the stage's incoming R-evidence with one
semantics, input-shaped by position (#200 decision 1): after a
walk_forward it pools the wf family's per-window OOS trade_rs in roll
order into ONE r_bootstrap (PooledOos); after sweep/gates it
bootstraps each surviving member's fresh in-memory series
(PerSurvivor, ordinals into the population family; a zero-trade
member records the engine's defined all-zero degenerate). Seeded from
the campaign doc's seed (C1). The wf family is read by reference from
the cell's families vec — no clone. run_cell's Generalize arm is an
explicit no-op naming the campaign scope.

The generalize phase runs after the cell loop (#200 decision 2):
nominees (last wf window's OOS report, else the sweep winner; None on
gate truncation) grouped by (strategy, window) across instruments via
a BTreeMap (deterministic order); >= 2 winners feed the shipped
generalization(), fewer record the shortfall (generalization: None,
missing named) — divergent per-instrument winners are exposed via
their params, never averaged away.

Gates: campaign 40/0 (4 execute_mc + 3 execute_generalize new),
workspace build clean; the known-RED cli integration test flips in
task 5.

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

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

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

refs #198
2026-07-03 20:53:18 +02:00