From 179c2f8bf0dd979512c8e06c5da4d1debc17bba8 Mon Sep 17 00:00:00 2001 From: Brummel Date: Sat, 4 Jul 2026 04:22:41 +0200 Subject: [PATCH] audit: cycle 0109 tidy (drift resolved by ledger/glossary lift + tap-channel cross-pin) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Architect verdict: drift_found — the code holds (C1 honored not assumed: the nominee re-run's metrics-equality hard refusal; C22/C14 clean: existing TraceStore + unchanged viewer, serde-default widening round-trips, name composition single-sourced in derive_trace_name). Resolved here: - C18: the 0107 paragraph's 'persist_taps is deferred' points forward; new cycle-0109 realization paragraph records the closed tap vocabulary + UnknownTap tier, the nominee-only non-reduce re-run with the C1 metrics guard, the campaign trace family layout, the trace_name claim contract, the loud-skip lines, and the noted chart-over-family-root debt. - Glossary: tap entry names the closed vocabulary + escalation rule; campaign document's presentation clause references it; campaign run gains the trace_name pointer. - Debt fixed inline (architect med): tap_channel gains the emit_vocabulary-twin debug_assert cross-pin so a fifth vocabulary tap fails loudly instead of silently skipping. The consumed 0108 fieldtest spec is removed with the cycle's spec+plan (all its dispositions shipped: F6 #205, F8 #207, F11 #206, F7/F9/F10 doc-tightens). Regression: cargo test --workspace 1041/0; clippy -D warnings clean; cargo doc 0 warnings. refs #201 --- crates/aura-cli/src/campaign_run.rs | 11 + docs/design/INDEX.md | 26 +- docs/glossary.md | 6 +- docs/plans/0109-persist-taps.md | 1464 ---------------------- docs/specs/0109-persist-taps.md | 222 ---- docs/specs/fieldtest-annotator-stages.md | 133 -- 6 files changed, 38 insertions(+), 1824 deletions(-) delete mode 100644 docs/plans/0109-persist-taps.md delete mode 100644 docs/specs/0109-persist-taps.md delete mode 100644 docs/specs/fieldtest-annotator-stages.md diff --git a/crates/aura-cli/src/campaign_run.rs b/crates/aura-cli/src/campaign_run.rs index 7853117..58e7c1b 100644 --- a/crates/aura-cli/src/campaign_run.rs +++ b/crates/aura-cli/src/campaign_run.rs @@ -476,6 +476,17 @@ enum TapChannel { /// An unknown name cannot reach here (`validate_campaign` refuses it) and /// maps to `None` all the same rather than guessing. fn tap_channel(tap: &str) -> Option { + // The emit_vocabulary debug_assert's twin: every vocabulary tap must be + // either routable here or the one known cost-only tap — a future fifth + // vocabulary entry fails loudly instead of silently skipping with the + // hardcoded needs-a-cost-run message. + debug_assert!( + aura_research::tap_vocabulary() + .iter() + .all(|t| *t == "net_r_equity" + || matches!(*t, "equity" | "exposure" | "r_equity")), + "tap_vocabulary drifted from the channels campaign_run routes" + ); match tap { "equity" => Some(TapChannel::Equity), "exposure" => Some(TapChannel::Exposure), diff --git a/docs/design/INDEX.md b/docs/design/INDEX.md index ba5afde..142cad2 100644 --- a/docs/design/INDEX.md +++ b/docs/design/INDEX.md @@ -1418,8 +1418,8 @@ registry grew the **`campaign_runs.jsonl`** JSONL sibling (beside `runs.jsonl` / per-cell realized stage prefixes linking family ids, gate survivor ordinals, and sweep selections — over untouched family records, run-counted per campaign id. Zero survivors truncate a cell's realized prefix and exit 0 (a null result is a valid research result); -`emit` is honored (`family_table`/`selection_report` lines), `persist_taps` is deferred -LOUDLY on stderr (no name→tap wiring exists yet). The **blueprint on-ramp** (#196) closes +`emit` is honored (`family_table`/`selection_report` lines); `persist_taps` was deferred +LOUDLY on stderr in this cycle (the wiring shipped in cycle 0109, below). The **blueprint on-ramp** (#196) closes the F5 authoring gap: `aura graph register` (store put keyed by content id == topology hash), `aura graph introspect --params` (the RAW `param_space` namespace campaign axes are validated against), and a blueprint-file mode on `--content-id`. The @@ -1456,6 +1456,28 @@ strategy × window with `winners`/`missing`), both serde-default sparse — pre- (introspection doc-strings dropped "in v1" only). Noted debt: the mc arm detects the wf family by the stringly `block == "std::walk_forward"` literal (a pre-existing lockstep pattern with the realization's block strings). +**Realization (cycle 0109, #201 — persist_taps wired).** Campaign presentation persists +traces: the tap namespace is a **closed vocabulary** of the wrap convention's four sink +names (`equity`/`exposure`/`r_equity`/`net_r_equity`; `aura_research::tap_vocabulary`, +intrinsic `DocFault::UnknownTap` — the escalation for a new observable is a new +vocabulary entry or an authored blueprint sink, never an open node-path namespace). +Scope is the per-cell **nominee only**: after the pipeline settles, the CLI re-runs each +nominee once in non-reduce mode (all four channels drained; windowed to the nominee +manifest's own ns bounds) and **asserts metrics equality** against the recorded nominee — +the C1 drift alarm, hard refusal on divergence (the reproduce precedent, enforced). +Traces land in the existing TraceStore as +`traces/{campaign8}-{run}/{strategy8}-{instrument}-w{n}/{tap}.json` +(`ensure_name_free(Family)` once; window ordinal doc-derived), chartable by the unchanged +viewer. The record carries ONE sparse pointer, `CampaignRunRecord.trace_name` +(`Some("{campaign8}-{run}")` iff the doc requests taps — the claim-sentinel contract: +`execute` claims, `append_campaign_run` composes the name via the single-sourced +`derive_trace_name`, `execute` mirrors it onto the returned copy). Loud lines replace +the retired deferral: per-cell no-nominee skip, per-run unproducible-tap skip +(`net_r_equity` needs a cost leg; the campaign runner wires none), one summary. +`aura-campaign` stays trace-agnostic (the MemberRunner seam is unchanged; the stamp is a +pure name derivation). Noted debt: `aura chart` over the campaign family ROOT (cells +spanning instruments) is untested/semantically undefined — only per-cell read-back is +pinned. **Guarantee.** Construction is a distinct phase, recursive at every level. Each node type has a **factory** `params → sized concrete node` (e.g. `SMA(length)` sizes its ring buffer). A **blueprint** is the param-generic, input-role-generic diff --git a/docs/glossary.md b/docs/glossary.md index 5943aa0..55446c5 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -45,11 +45,11 @@ A downstream consumer node, never part of the strategy: the signal-quality side ### campaign document **Avoid:** experiment doc, campaign file -The role-6b research artifact (#188/#189): persisted experiment intent as closed-vocabulary canonical JSON — instruments × windows × strategy refs (by `content id` or `identity id`) × per-strategy param axes (each axis declares its `ScalarKind` once over bare values) × a process reference (content-id-only) × data-level presentation (taps to persist, tables to emit). Authored, validated, and executed headless (`aura campaign validate|introspect|register|run`), content-addressed beside blueprints in the registry store; carries P1 control constructs (bounded axes, gates, ladders) as intent, executed by `aura campaign run` (v2 pipeline shape `std::sweep [std::gate]* [std::walk_forward]? [std::monte_carlo]? [std::generalize]?` — the two annotators terminal, cycles 0107/0108, #198/#200) into a `campaign run` realization. +The role-6b research artifact (#188/#189): persisted experiment intent as closed-vocabulary canonical JSON — instruments × windows × strategy refs (by `content id` or `identity id`) × per-strategy param axes (each axis declares its `ScalarKind` once over bare values) × a process reference (content-id-only) × data-level presentation (taps to persist — the closed `tap` vocabulary — and tables to emit). Authored, validated, and executed headless (`aura campaign validate|introspect|register|run`), content-addressed beside blueprints in the registry store; carries P1 control constructs (bounded axes, gates, ladders) as intent, executed by `aura campaign run` (v2 pipeline shape `std::sweep [std::gate]* [std::walk_forward]? [std::monte_carlo]? [std::generalize]?` — the two annotators terminal, cycles 0107/0108, #198/#200) into a `campaign run` realization. ### campaign run **Avoid:** campaign execution record, realized campaign -One execution of a `campaign document` (`aura campaign run `; a file is register-then-run sugar — the content id is the canonical address). Realized once per (strategy, instrument, window) cell in doc order and recorded as a thin `CampaignRunRecord` line in the registry's `campaign_runs.jsonl` — linking the per-stage family ids, gate survivor ordinals, `stage bootstrap` annotations, and (campaign-scope, per strategy × window) `generalizations` entries (`generalization` with `worst_case`/`sign_agreement`/`per_instrument`, plus `winners` params and `missing` instruments on shortfall) over untouched family records, run-counted per campaign id. A cell whose gate leaves no survivors records its realized prefix and the run exits 0: a null result is a valid research result. Deterministic from doc + stores + data (C1): deflation and bootstrap nulls seed from the doc's `seed`. NB the stdout emit wraps each record as `{"campaign_run": …}`; the stored `campaign_runs.jsonl` line is the bare record. +One execution of a `campaign document` (`aura campaign run `; a file is register-then-run sugar — the content id is the canonical address). Realized once per (strategy, instrument, window) cell in doc order and recorded as a thin `CampaignRunRecord` line in the registry's `campaign_runs.jsonl` — linking the per-stage family ids, gate survivor ordinals, `stage bootstrap` annotations, and (campaign-scope, per strategy × window) `generalizations` entries (`generalization` with `worst_case`/`sign_agreement`/`per_instrument`, plus `winners` params and `missing` instruments on shortfall) over untouched family records, run-counted per campaign id. A cell whose gate leaves no survivors records its realized prefix and the run exits 0: a null result is a valid research result. Deterministic from doc + stores + data (C1): deflation and bootstrap nulls seed from the doc's `seed`. When the document requests `persist_taps`, the record's sparse `trace_name` (`"{campaign8}-{run}"`) points at the TraceStore family holding each nominee cell's persisted taps (0109/#201). NB the stdout emit wraps each record as `{"campaign_run": …}`; the stored `campaign_runs.jsonl` line is the bare record. ### cdylib **Avoid:** — @@ -289,7 +289,7 @@ An orchestration axis varying tuning params (grid or random) within a fixed stru ### tap **Avoid:** — -A named recorded stream produced by a recording `sink` — the addressable label (e.g. `equity`, `net_r_equity`) under which one sink's per-cycle output is persisted as a columnar (SoA) `ColumnarTrace` and selected for charting via `--tap`. Distinct from the `sink` node that emits it (a tap is the stream, the sink is the role) and from a whole recorded run (a bundle of taps); taps fire at their own cadences and are fused only by joining on the recorded timestamp, never by positional index. +A named recorded stream produced by a recording `sink` — the addressable label (e.g. `equity`, `net_r_equity`) under which one sink's per-cycle output is persisted as a columnar (SoA) `ColumnarTrace` and selected for charting via `--tap`. Distinct from the `sink` node that emits it (a tap is the stream, the sink is the role) and from a whole recorded run (a bundle of taps); taps fire at their own cadences and are fused only by joining on the recorded timestamp, never by positional index. In a `campaign document`, `persist_taps` names taps from the CLOSED vocabulary `equity | exposure | r_equity | net_r_equity` (`tap_vocabulary`, 0109/#201) — a new observable is a new vocabulary entry or an authored blueprint sink, never an open node-path namespace. ### topology hash **Avoid:** — diff --git a/docs/plans/0109-persist-taps.md b/docs/plans/0109-persist-taps.md deleted file mode 100644 index 880a8fe..0000000 --- a/docs/plans/0109-persist-taps.md +++ /dev/null @@ -1,1464 +0,0 @@ -# persist_taps Wiring — Implementation Plan (cycle 0109) - -> **Parent spec:** `docs/specs/0109-persist-taps.md` -> -> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run -> this plan. Steps use `- [ ]` checkboxes for tracking. - -**Goal:** `aura campaign run` honours `presentation.persist_taps`: closed tap -vocabulary validated intrinsically, nominee-only non-reduce re-run per cell -(METRICS-equality C1 guard), traces via the existing TraceStore under -`{campaign8}-{run}`, one sparse `trace_name` pointer on the record. - -**Architecture:** Task 1 lands the vocabulary + UnknownTap validation -(aura-research + CLI twins; the deferral eprintln STAYS until Task 3; the old -deferral test splits into its two 0109 pins). Task 2 widens the record and -lands the claim-sentinel → composed-name contract across registry and -executor. Task 3 ships `persist_campaign_traces` (the consumer wiring; the -deferral line dies; the Task-1(b) seam test tightens). Task 4 extends the -gated real-data e2e (trace files + chart read-back). - -**Tech Stack:** Rust workspace — aura-research, aura-registry, aura-campaign, -aura-cli. - -**Task-order constraints:** strictly sequential 1→4. Task 3's old-fences in -campaign_run.rs are valid because Tasks 1-2 do not touch that file; Task 3 -finds the Task-1(b) test by its fixture (`campaign_doc_json(.., (1,2), -"\"equity\"", "")`), not by name. - ---- -### Task 1: Tap vocabulary + UnknownTap intrinsic validation (aura-research + aura-cli twins) - -The tap namespace becomes a **closed vocabulary** (#201 decision 1, spec `docs/specs/0109-persist-taps.md`): `aura-research` gains `tap_vocabulary()` + `DocFault::UnknownTap` in `validate_campaign`, a new `SlotKind::TapKinds` for the `persist_taps` slot (mirroring `EmitKinds`), and the open-slot hint names the vocabulary. `aura-cli` gains the `doc_fault_prose` twin arm. The 0107 deferral seam test `campaign_run_persist_taps_deferred_loudly` splits into two pins. - -**Sequencing decision — state loudly:** this task **keeps** the loud-deferral `eprintln!` in `crates/aura-cli/src/campaign_run.rs` (lines 360–368) untouched. It still prints on any run with non-empty valid `persist_taps`. Therefore the new test (b) below asserts **nothing about the deferral line, in either direction** — it pins only validate-pass + the member-data-seam refusal + the absence of any trace output. Task 3 of this plan removes the `eprintln!` and tightens test (b) to pin its absence. Do not remove the `eprintln!` in this task; do not assert on `"not yet honored"` in test (b). - -**Files:** -- `crates/aura-research/src/lib.rs` -- `crates/aura-cli/src/research_docs.rs` -- `crates/aura-cli/tests/research_docs.rs` - -- [ ] **Step 1: RED — aura-research unit tests (vocabulary, fault, hint label, open-slot hint)** - - In `crates/aura-research/src/lib.rs`, inside the `#[cfg(test)] mod tests` module, insert four new tests immediately after the closing brace of `validate_campaign_accepts_the_fixture_and_reports_each_fault`. Anchor (byte-exact, the test's last lines): - - old: - ```rust - let mut emit = ok.clone(); - emit.presentation.emit.push("pie_chart".into()); - assert!(validate_campaign(&emit).contains(&DocFault::UnknownEmitKind("pie_chart".into()))); - } - ``` - new: - ```rust - let mut emit = ok.clone(); - emit.presentation.emit.push("pie_chart".into()); - assert!(validate_campaign(&emit).contains(&DocFault::UnknownEmitKind("pie_chart".into()))); - } - - /// #201 decision 1 (cycle 0109): the tap namespace is a CLOSED vocabulary — - /// the wrap convention's four persisted sink names. A genuinely new - /// observable is a new vocabulary entry (or an authored blueprint sink), - /// never an open node-path namespace in the document. - #[test] - fn tap_vocabulary_is_the_wrap_conventions_four_sink_names() { - assert_eq!(tap_vocabulary(), ["equity", "exposure", "r_equity", "net_r_equity"]); - } - - /// Each `presentation.persist_taps` entry outside `tap_vocabulary()` is an - /// intrinsic fault carrying its index (for path-addressed prose downstream) - /// and the offending name — mirroring the UnknownEmitKind loop. The - /// fixture's own "net_r_equity" tap is in-vocabulary, so the clean-fixture - /// assert in `validate_campaign_accepts_the_fixture_and_reports_each_fault` - /// stays green unchanged. - #[test] - fn validate_campaign_refuses_unknown_taps_by_index() { - let ok = parse_campaign(CAMPAIGN_FIXTURE).unwrap(); - let mut tap = ok.clone(); - tap.presentation.persist_taps.push("bias".into()); - assert_eq!( - validate_campaign(&tap), - vec![DocFault::UnknownTap { index: 1, tap: "bias".into() }] - ); - // every in-vocabulary name passes - let mut all = ok.clone(); - all.presentation.persist_taps = tap_vocabulary().iter().map(|t| t.to_string()).collect(); - assert_eq!(validate_campaign(&all), Vec::new()); - } - - /// The `std::presentation` persist_taps slot advertises the closed tap - /// vocabulary through the describe path (`describe_block` + - /// `slot_kind_label`); the label is cross-pinned against `tap_vocabulary()` - /// so the static label and the validator cannot drift apart. - #[test] - fn persist_taps_slot_advertises_the_tap_vocabulary() { - let block = describe_block("std::presentation").expect("presentation describable"); - let slot = block - .slots - .iter() - .find(|s| s.name == "persist_taps") - .expect("presentation has a persist_taps slot"); - assert_eq!(slot.kind, SlotKind::TapKinds); - assert_eq!( - slot_kind_label(slot.kind), - format!("list of: {}", tap_vocabulary().join(" | ")) - ); - assert_eq!( - slot_kind_label(slot.kind), - "list of: equity | exposure | r_equity | net_r_equity" - ); - } - - /// A draft missing its `presentation` section is pointed at the section - /// AND the closed tap vocabulary (the guide never advertises an open - /// namespace the validator would refuse). - #[test] - fn presentation_open_slot_hint_names_the_tap_vocabulary() { - let draft = r#"{ "format_version": 1, "kind": "campaign", "name": "draft", - "data": { "instruments": ["GER40"], "windows": [ { "from_ms": 1, "to_ms": 2 } ] }, - "strategies": [ { "ref": { "content_id": "9f3a" }, - "axes": { "slow": { "kind": "I64", "values": [10] } } } ], - "process": { "ref": { "content_id": "4e2d" } }, - "seed": 1 }"#; - let slots = open_slots_campaign(draft).unwrap(); - assert_eq!( - slots, - vec![open( - "presentation", - "required section: persist_taps (equity | exposure | r_equity | net_r_equity) + emit", - )] - ); - } - ``` - - Run: `cargo test -p aura-research` - Expected: **compilation failure of the test target** — errors naming `tap_vocabulary` (E0425, cannot find function), `UnknownTap` (E0599, no variant on `DocFault`), and `TapKinds` (E0599, no variant on `SlotKind`). This compile-fail IS the RED evidence: tests of a not-yet-existing API cannot fail at runtime. Do not "fix" this here; Step 3 turns it green. - -- [ ] **Step 2: RED — split the 0107 deferral seam test into the two 0109 pins (aura-cli)** - - In `crates/aura-cli/tests/research_docs.rs`, replace the entire `campaign_run_persist_taps_deferred_loudly` test (doc comment included) with the two new tests. The old deferral-prose assert dies here. - - old: - ```rust -/// Non-empty persist_taps defers LOUDLY, and the note's ORDER is pinned: it -/// prints before member execution, so it is asserted here on a run -/// that refuses at the member-data seam. The [1, 2] epoch-ms window (1970) -/// makes that refusal deterministic on every machine: a data-less host -/// refuses on missing geometry, a data-ful host on a window no archive file -/// overlaps — exit 1 either way, tap note already on stderr. -#[test] -fn campaign_run_persist_taps_deferred_loudly() { - let _fixture = project_lock(); - let dir = built_project(); - let runs_dir = dir.join("runs"); - std::fs::remove_dir_all(&runs_dir).ok(); - let _cleanup = ScratchGuard(vec![ - ScratchPath::Dir(runs_dir.clone()), - ScratchPath::File(dir.join("taps.process.json")), - ScratchPath::File(dir.join("taps.campaign.json")), - ]); - let bp_id = seed_blueprint(dir, "campaign-run-taps-seed"); - let proc_id = register_process_doc(dir, "taps.process.json", SWEEP_ONLY_PROCESS_DOC); - write_doc( - dir, - "taps.campaign.json", - &campaign_doc_json(&bp_id, &proc_id, (1, 2), "\"r_record\"", ""), - ); - let (out, code) = run_code_in(dir, &["campaign", "run", "taps.campaign.json"]); - assert_eq!(code, Some(1), "stdout/stderr: {out}"); - assert!( - out.contains("aura: persist_taps not yet honored (1 tap(s) ignored)"), - "the tap note must precede the member-data refusal: {out}" - ); - assert!( - out.contains("no recorded geometry") || out.contains("no data for instrument"), - "the refusal names the data condition: {out}" - ); -} - ``` - new: - ```rust -/// 0109 split (a) — the tap namespace is CLOSED: the 0107 deferral fixture's -/// made-up "r_record" tap now refuses INTRINSICALLY, exit 1, path-addressed -/// and vocabulary-enumerating. The old test drove `campaign run` on a file -/// target, so this pin drives the same path: the refusal fires at the run's -/// parse-valid gate (`parse_valid_campaign` — the same `validate_campaign` + -/// `doc_fault_prose` composition `campaign validate` shares), before -/// registration, before the referential tier, before any member runs — and -/// therefore before the (Task-3-doomed) loud-deferral note, whose absence is -/// pinned here. -#[test] -fn campaign_run_refuses_unknown_tap_at_validate() { - let _fixture = project_lock(); - let dir = built_project(); - let runs_dir = dir.join("runs"); - std::fs::remove_dir_all(&runs_dir).ok(); - let _cleanup = ScratchGuard(vec![ - ScratchPath::Dir(runs_dir.clone()), - ScratchPath::File(dir.join("badtap.process.json")), - ScratchPath::File(dir.join("badtap.campaign.json")), - ]); - let bp_id = seed_blueprint(dir, "campaign-run-badtap-seed"); - let proc_id = register_process_doc(dir, "badtap.process.json", SWEEP_ONLY_PROCESS_DOC); - write_doc( - dir, - "badtap.campaign.json", - &campaign_doc_json(&bp_id, &proc_id, (1, 2), "\"r_record\"", ""), - ); - let (out, code) = run_code_in(dir, &["campaign", "run", "badtap.campaign.json"]); - assert_eq!(code, Some(1), "stdout/stderr: {out}"); - assert!(out.contains("campaign document invalid:"), "stdout/stderr: {out}"); - assert!( - out.contains( - "presentation.persist_taps[0]: unknown tap \"r_record\" \ - (taps: equity | exposure | r_equity | net_r_equity)" - ), - "the refusal is path-addressed and enumerates the vocabulary: {out}" - ); - assert!( - !out.contains("not yet honored"), - "an invalid document must never reach the deferral note: {out}" - ); - assert!(!out.contains("UnknownTap"), "Debug leak: {out}"); -} - -/// 0109 split (b) — the control: an in-vocabulary tap ("equity") sails -/// through the new intrinsic gate (no UnknownTap prose) and the run proceeds -/// to refuse at the member-data seam, exactly as a tap-less run over the -/// [1, 2] 1970 epoch-ms window does (data-less host: missing geometry; -/// data-ful host: no archive file overlaps) — exit 1 either way, and no -/// trace output (tracing never starts on a run that dies before members; -/// there is no "traces persisted" summary line). NOTE: the 0107 -/// loud-deferral eprintln still fires in this task's tree and is -/// deliberately NOT asserted either way here — Task 3 deletes it and -/// tightens this test to pin its absence. -#[test] -fn campaign_run_valid_tap_reaches_the_member_data_seam() { - let _fixture = project_lock(); - let dir = built_project(); - let runs_dir = dir.join("runs"); - std::fs::remove_dir_all(&runs_dir).ok(); - let _cleanup = ScratchGuard(vec![ - ScratchPath::Dir(runs_dir.clone()), - ScratchPath::File(dir.join("taps.process.json")), - ScratchPath::File(dir.join("taps.campaign.json")), - ]); - let bp_id = seed_blueprint(dir, "campaign-run-taps-seed"); - let proc_id = register_process_doc(dir, "taps.process.json", SWEEP_ONLY_PROCESS_DOC); - write_doc( - dir, - "taps.campaign.json", - &campaign_doc_json(&bp_id, &proc_id, (1, 2), "\"equity\"", ""), - ); - let (out, code) = run_code_in(dir, &["campaign", "run", "taps.campaign.json"]); - assert_eq!(code, Some(1), "stdout/stderr: {out}"); - assert!( - !out.contains("unknown tap"), - "\"equity\" is in the closed vocabulary and must pass validate: {out}" - ); - assert!( - out.contains("no recorded geometry") || out.contains("no data for instrument"), - "the refusal names the data condition: {out}" - ); - assert!( - !out.contains("traces persisted"), - "no trace summary on a run that never reached a member: {out}" - ); -} - ``` - - Run: `cargo test -p aura-cli --test research_docs campaign_run_refuses_unknown_tap_at_validate` - Expected: compiles, **1 test FAILS** (RED). Today "r_record" passes validation, so the run refuses at the member-data seam instead — the `"campaign document invalid:"` and unknown-tap-prose asserts fail, and the deferral line is present (failing the `!contains("not yet honored")` assert). - - Run: `cargo test -p aura-cli --test research_docs campaign_run_valid_tap_reaches_the_member_data_seam` - Expected: **1 test PASSES already**. This is deliberate and not a defect: test (b) is the control half of the split — it guards the gate Step 3 introduces against over-rejection ("equity" must keep reaching the data seam once unknown taps refuse). Its RED counterpart is test (a). - -- [ ] **Step 3: GREEN — aura-research: vocabulary, fault, validation loop, TapKinds, hint** - - Seven edits in `crates/aura-research/src/lib.rs`. - - (1) Add `tap_vocabulary` directly below `emit_vocabulary`: - - old: - ```rust -/// The closed v1 emit vocabulary (data-level presentation outputs). -pub fn emit_vocabulary() -> &'static [&'static str] { - &["family_table", "selection_report"] -} - ``` - new: - ```rust -/// The closed v1 emit vocabulary (data-level presentation outputs). -pub fn emit_vocabulary() -> &'static [&'static str] { - &["family_table", "selection_report"] -} - -/// The wrap convention's persisted sink names — the closed tap vocabulary -/// (#201 decision 1). A genuinely new observable becomes a new entry or an -/// authored sink in the strategy blueprint (C22: the choice of sinks is part -/// of the experiment) — never an open node-path namespace in the document. -pub fn tap_vocabulary() -> &'static [&'static str] { - &["equity", "exposure", "r_equity", "net_r_equity"] -} - ``` - - (2) Add the `DocFault` variant (campaign side, after `UnknownEmitKind`): - - old: - ```rust - EmptyAxes { strategy: usize }, - EmptyAxis { strategy: usize, axis: String }, - UnknownEmitKind(String), - ProcessRefMustBeContentId, -} - ``` - new: - ```rust - EmptyAxes { strategy: usize }, - EmptyAxis { strategy: usize, axis: String }, - UnknownEmitKind(String), - UnknownTap { index: usize, tap: String }, - ProcessRefMustBeContentId, -} - ``` - - (3) Add the taps loop in `validate_campaign`, after the emit loop: - - old: - ```rust - for e in &doc.presentation.emit { - if !emit_vocabulary().contains(&e.as_str()) { - faults.push(DocFault::UnknownEmitKind(e.clone())); - } - } - faults -} - ``` - new: - ```rust - for e in &doc.presentation.emit { - if !emit_vocabulary().contains(&e.as_str()) { - faults.push(DocFault::UnknownEmitKind(e.clone())); - } - } - for (i, t) in doc.presentation.persist_taps.iter().enumerate() { - if !tap_vocabulary().contains(&t.as_str()) { - faults.push(DocFault::UnknownTap { index: i, tap: t.clone() }); - } - } - faults -} - ``` - - (4) Add the `SlotKind` variant (mirror `EmitKinds`): - - old: - ```rust - Axes, - EmitKinds, -} - ``` - new: - ```rust - Axes, - EmitKinds, - /// The closed tap vocabulary (`tap_vocabulary()` entries). - TapKinds, -} - ``` - - (5) Retype the `persist_taps` slot in `CAMPAIGN_SECTIONS` (`std::presentation`): - - old: - ```rust - SlotInfo { name: "persist_taps", kind: SlotKind::Strings, required: true }, - ``` - new: - ```rust - SlotInfo { name: "persist_taps", kind: SlotKind::TapKinds, required: true }, - ``` - - (6) Add the `slot_kind_label` arm: - - old: - ```rust - SlotKind::EmitKinds => "list of: family_table | selection_report", - } -} - ``` - new: - ```rust - SlotKind::EmitKinds => "list of: family_table | selection_report", - SlotKind::TapKinds => "list of: equity | exposure | r_equity | net_r_equity", - } -} - ``` - - (7) Name the vocabulary in the `open_slots_campaign` presentation hint: - - old: - ```rust - if v.get("presentation").is_none() { - slots.push(open("presentation", "required section: persist_taps + emit")); - } - ``` - new: - ```rust - if v.get("presentation").is_none() { - slots.push(open( - "presentation", - "required section: persist_taps (equity | exposure | r_equity | net_r_equity) + emit", - )); - } - ``` - - Run: `cargo test -p aura-research` - Expected: compiles, **all tests pass** — including the four Step-1 tests (`tap_vocabulary_is_the_wrap_conventions_four_sink_names`, `validate_campaign_refuses_unknown_taps_by_index`, `persist_taps_slot_advertises_the_tap_vocabulary`, `presentation_open_slot_hint_names_the_tap_vocabulary`) and the untouched `validate_campaign_accepts_the_fixture_and_reports_each_fault` (the fixture's "net_r_equity" tap is in-vocabulary). - NOTE: at this point `cargo build --workspace` FAILS with E0004 (non-exhaustive match): `doc_fault_prose` in `crates/aura-cli/src/research_docs.rs` does not yet cover `UnknownTap`. That is expected mid-task; Step 4 restores the workspace build. (`doc_fault_prose` is the only exhaustive `DocFault` consumer in the workspace — aura-campaign mentions `DocFault` only in a comment.) - -- [ ] **Step 4: GREEN — aura-cli prose twin (`doc_fault_prose` arm + unit test)** - - Three edits in `crates/aura-cli/src/research_docs.rs`. - - (1) Import `tap_vocabulary`: - - old: - ```rust -use aura_research::{ - campaign_to_json, campaign_vocabulary, describe_block, open_slots_campaign, - open_slots_process, parse_campaign, parse_process, process_to_json, process_vocabulary, - slot_kind_label, validate_campaign, validate_process, CampaignDoc, DocError, DocFault, - DocKind, ProcessDoc, StageBlock, -}; - ``` - new: - ```rust -use aura_research::{ - campaign_to_json, campaign_vocabulary, describe_block, open_slots_campaign, - open_slots_process, parse_campaign, parse_process, process_to_json, process_vocabulary, - slot_kind_label, tap_vocabulary, validate_campaign, validate_process, CampaignDoc, DocError, - DocFault, DocKind, ProcessDoc, StageBlock, -}; - ``` - - (2) Add the prose arm (vocabulary composed from `tap_vocabulary()` — one source, no third copy of the four names): - - old: - ```rust - DocFault::UnknownEmitKind(kind) => format!("presentation.emit: unknown kind \"{kind}\""), - DocFault::ProcessRefMustBeContentId => { - "process.ref: a process is referenced by content id in this version".into() - } - ``` - new: - ```rust - DocFault::UnknownEmitKind(kind) => format!("presentation.emit: unknown kind \"{kind}\""), - DocFault::UnknownTap { index, tap } => format!( - "presentation.persist_taps[{index}]: unknown tap \"{tap}\" (taps: {})", - tap_vocabulary().join(" | ") - ), - DocFault::ProcessRefMustBeContentId => { - "process.ref: a process is referenced by content id in this version".into() - } - ``` - - (3) Pin the prose in the file's `mod tests`, after the last existing test: - - old: - ```rust - #[test] - fn zero_walk_forward_length_prose_is_path_addressed_and_debug_free() { - let prose = doc_fault_prose(&DocFault::ZeroWalkForwardLength { stage: 2, field: "step_ms" }); - assert_eq!(prose, "pipeline[2]: walk_forward step_ms must be > 0"); - assert!(!prose.contains("ZeroWalkForwardLength"), "Debug leak: {prose}"); - } -} - ``` - new: - ```rust - #[test] - fn zero_walk_forward_length_prose_is_path_addressed_and_debug_free() { - let prose = doc_fault_prose(&DocFault::ZeroWalkForwardLength { stage: 2, field: "step_ms" }); - assert_eq!(prose, "pipeline[2]: walk_forward step_ms must be > 0"); - assert!(!prose.contains("ZeroWalkForwardLength"), "Debug leak: {prose}"); - } - - #[test] - /// #201 decision 1 (spec 0109): the unknown-tap refusal is path-addressed - /// (`presentation.persist_taps[i]`) and enumerates the closed vocabulary - /// inline — four names are small enough to print, so no introspection - /// pointer is needed (contrast the 17-name metric roster, which points at - /// `aura process introspect --metrics` instead). - fn unknown_tap_prose_is_path_addressed_and_enumerates_the_vocabulary() { - let prose = doc_fault_prose(&DocFault::UnknownTap { index: 0, tap: "bias".into() }); - assert_eq!( - prose, - "presentation.persist_taps[0]: unknown tap \"bias\" \ - (taps: equity | exposure | r_equity | net_r_equity)" - ); - assert!(!prose.contains("UnknownTap"), "Debug leak: {prose}"); - } -} - ``` - - Run: `cargo test -p aura-cli unknown_tap_prose` - Expected: compiles, `unknown_tap_prose_is_path_addressed_and_enumerates_the_vocabulary` passes (1 passed; the filter matches only this test). - -- [ ] **Step 5: Gates — both crates green, workspace builds** - - Run: `cargo test -p aura-research` - Expected: green, 0 failures. - - Run: `cargo test -p aura-cli --test research_docs campaign_run` - Expected: green, 0 failures — `campaign_run_refuses_unknown_tap_at_validate` now passes (the run refuses at the parse-valid gate with the enumerating prose, no deferral note, no Debug leak), `campaign_run_valid_tap_reaches_the_member_data_seam` still passes (the control), and every sibling `campaign_run_*` test is unchanged (`campaign_run_real_e2e_sweep_gate_walkforward` may self-skip on a data-less machine — that skip is green, not a failure). - - Run: `cargo build --workspace` - Expected: 0 errors (the Step-3 E0004 window is closed by the Step-4 arm). -### Task 2: trace_name widening + name composition + eligibility stamp (aura-registry + aura-campaign) - -**Context (cycle 0109, spec `docs/specs/0109-persist-taps.md`, #201 decision 5 + the spec's seam note):** `CampaignRunRecord` gains a sparse `trace_name: Option` — the TraceStore family name a campaign realization claims when its document requests `persist_taps`. The contract: the STORED `campaign_runs.jsonl` line AND the record `execute` RETURNS both carry the same `Some("{campaign8}-{run}")` iff `campaign.presentation.persist_taps` is non-empty, else `None`. Because the per-campaign `run` counter is assigned inside `Registry::append_campaign_run`, the seam is split: `aura-campaign::execute` stamps an eligibility **claim sentinel** (`Some(String::new())`) on the record it hands to the store; `append_campaign_run` replaces any `Some` with the derived name on the stored line (`None` stays `None`); `execute` then mirrors the same derivation onto the returned copy after `record.run = run`. The field is serde-default + skip-when-`None` (the C14/C23 widening convention), so pre-0109 stored lines keep parsing and re-serialized dumps of `None`-records stay byte-identical. - -**Files:** -- `crates/aura-registry/src/lib.rs` (tests: RED composition pin, pre-widening extension, literal threading) -- `crates/aura-registry/src/lineage.rs` (the field + the append-side composition) -- `crates/aura-registry/tests/campaign_run_store_e2e.rs` (literal threading) -- `crates/aura-campaign/tests/execute.rs` (RED stamp pin, `None` extension) -- `crates/aura-campaign/src/exec.rs` (the claim sentinel + the returned-copy mirror) - -- [ ] **Step 1: RED — registry pins for the name composition and the pre-0109 parse.** - - In `crates/aura-registry/src/lib.rs`, at the very end of the test module, extend the existing pre-widening test and append the new composition test. Replace this exact existing code (the file's final lines — the pre-widening test plus the module-closing brace): - - ```rust - /// A pre-0108 stored line — no `bootstrap`, no `generalizations` — still - /// parses (the C14/C23 serde-default widening convention): existing - /// `campaign_runs.jsonl` stores survive the record widening unchanged. - #[test] - fn campaign_run_line_without_new_fields_still_parses() { - let path = temp_family_dir("campaign_run_pre_widening"); - let store = path.with_file_name("campaign_runs.jsonl"); - let line = r#"{"campaign":"cafe","process":"beef","run":0,"seed":7,"cells":[{"strategy":"3f9c","instrument":"EURUSD","window_ms":[0,1000],"stages":[{"block":"std::gate","survivor_ordinals":[0,2]}]}]}"#; - fs::write(&store, format!("{line}\n")).expect("write pre-widening line"); - let reg = Registry::open(&path); - let loaded = reg.load_campaign_runs().expect("pre-widening line parses"); - assert_eq!(loaded.len(), 1); - assert_eq!(loaded[0].cells[0].stages[0].bootstrap, None); - assert!(loaded[0].generalizations.is_empty()); - } - } - ``` - - with: - - ```rust - /// A pre-0109 stored line — no `bootstrap`, no `generalizations`, no - /// `trace_name` — still parses (the C14/C23 serde-default widening - /// convention): existing `campaign_runs.jsonl` stores survive the record - /// widening unchanged. - #[test] - fn campaign_run_line_without_new_fields_still_parses() { - let path = temp_family_dir("campaign_run_pre_widening"); - let store = path.with_file_name("campaign_runs.jsonl"); - let line = r#"{"campaign":"cafe","process":"beef","run":0,"seed":7,"cells":[{"strategy":"3f9c","instrument":"EURUSD","window_ms":[0,1000],"stages":[{"block":"std::gate","survivor_ordinals":[0,2]}]}]}"#; - fs::write(&store, format!("{line}\n")).expect("write pre-widening line"); - let reg = Registry::open(&path); - let loaded = reg.load_campaign_runs().expect("pre-widening line parses"); - assert_eq!(loaded.len(), 1); - assert_eq!(loaded[0].cells[0].stages[0].bootstrap, None); - assert!(loaded[0].generalizations.is_empty()); - assert_eq!( - loaded[0].trace_name, None, - "pre-0109 line: an absent trace_name parses to None" - ); - } - - /// The 0109 name composition (#201 d5 / the spec's seam note): a `Some` - /// trace_name on the input record — the executor's claim sentinel, - /// content ignored — is replaced on the STORED line with the derived - /// `"{campaign8}-{run}"` (prefix from the record's own `campaign`, run - /// from the store's per-campaign counter); `None` stays `None`. The - /// executor guarantees a 64-hex campaign id, but the registry seam must - /// not panic on a shorter one — the safe-prefix case is pinned here. - #[test] - fn append_campaign_run_composes_trace_name_from_a_claim() { - let path = temp_family_dir("campaign_run_trace_name"); - let reg = Registry::open(&path); - let long = "aaaabbbbccccddddeeeeffff0000111122223333444455556666777788889999"; - - // claim sentinel (empty string) -> derived name, per-campaign counter - let mut claimed = campaign_run_record(long); - claimed.trace_name = Some(String::new()); - assert_eq!(reg.append_campaign_run(&claimed).expect("append claim 0"), 0); - assert_eq!(reg.append_campaign_run(&claimed).expect("append claim 1"), 1); - - // no claim -> None stays None - assert_eq!(reg.append_campaign_run(&campaign_run_record(long)).expect("append plain"), 2); - - // shorter-than-8-chars campaign id: composes from the whole id, no panic - let mut short = campaign_run_record("abc"); - short.trace_name = Some("sentinel content is ignored".to_string()); - assert_eq!(reg.append_campaign_run(&short).expect("append short-id claim"), 0); - - let stored = reg.load_campaign_runs().expect("load"); - assert_eq!( - stored.iter().map(|r| r.trace_name.as_deref()).collect::>(), - vec![Some("aaaabbbb-0"), Some("aaaabbbb-1"), None, Some("abc-0")], - "a claim composes {{campaign8}}-{{run}} on the stored line; None stays None", - ); - } - } - ``` - - (`campaign_run_record` is the existing helper at the top of the campaign-run test block; `temp_family_dir` and `fs` are already in scope in this module.) - - Run: `cargo test -p aura-registry append_campaign_run_composes_trace_name` - Expected outcome: **compilation fails** with `error[E0609]: no field \`trace_name\` on type ... CampaignRunRecord` (several occurrences); no tests run. This is the RED evidence — the widening does not exist yet. - -- [ ] **Step 2: GREEN — the field, the append-side composition, and literal threading.** - - Four files. All edits are old->new byte-exact replacements. - - **(a) `crates/aura-registry/src/lineage.rs` — the field.** The new field goes after `generalizations` (declaration order = serde order for the `Some` case). Replace: - - ```rust - /// Campaign-scope generalize annotations (cycle 0108), one per - /// (strategy, window) across instruments. Absent pre-0108 — the - /// serde-default widening keeps existing stored lines parseable. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub generalizations: Vec, - } - ``` - - with: - - ```rust - /// Campaign-scope generalize annotations (cycle 0108), one per - /// (strategy, window) across instruments. Absent pre-0108 — the - /// serde-default widening keeps existing stored lines parseable. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub generalizations: Vec, - /// The TraceStore family name this realization claims when the document - /// requests persist_taps — a pure derivation ("{campaign8}-{run}"): the - /// executor hands a claim sentinel (`Some`, any content) and - /// [`Registry::append_campaign_run`] composes the derived name onto the - /// stored line; the consumer persists the bytes (#201 d5). `None` when - /// the document requests no taps. Absent pre-0109 — the serde-default - /// widening keeps existing stored lines parseable. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub trace_name: Option, - } - ``` - - **(b) `crates/aura-registry/src/lineage.rs` — the composition in `append_campaign_run`.** Replace: - - ```rust - let mut file = fs::OpenOptions::new().create(true).append(true).open(&path)?; - let stored = CampaignRunRecord { run, ..record.clone() }; - let line = serde_json::to_string(&stored).expect("a finite CampaignRunRecord serializes"); - ``` - - with: - - ```rust - let mut file = fs::OpenOptions::new().create(true).append(true).open(&path)?; - // A `Some` trace_name (any content — the executor's claim sentinel, - // #201 d5) is replaced with the derived name "{campaign8}-{run}"; - // `None` stays `None`. The executor guarantees a 64-hex campaign id, - // but the registry seam must not panic on a shorter one (in-crate - // tests use short ids), so the prefix take is the safe `get(..8)`. - let trace_name = record.trace_name.as_ref().map(|_| { - let prefix = record.campaign.get(..8).unwrap_or(record.campaign.as_str()); - format!("{prefix}-{run}") - }); - let stored = CampaignRunRecord { run, trace_name, ..record.clone() }; - let line = serde_json::to_string(&stored).expect("a finite CampaignRunRecord serializes"); - ``` - - And extend the method's doc comment. Replace: - - ```rust - /// run. The input record's own `run` field is ignored. Reads the store once - /// to pick the run index (a read-before-write; single-process CLI - /// invocations do not race). - ``` - - with: - - ```rust - /// run. The input record's own `run` field is ignored; a `Some` - /// `trace_name` (the executor's claim sentinel, cycle 0109) is replaced - /// with the derived `"{campaign8}-{run}"` on the stored line, `None` - /// stays `None`. Reads the store once to pick the run index (a - /// read-before-write; single-process CLI invocations do not race). - ``` - - **(c) `crates/aura-registry/src/lib.rs` — thread `trace_name: None` through the three test literals.** - - Site 1 (the `campaign_run_record` helper). Replace: - - ```rust - run: 99, - seed: 7, - cells: vec![], - generalizations: vec![], - } - ``` - - with: - - ```rust - run: 99, - seed: 7, - cells: vec![], - generalizations: vec![], - trace_name: None, - } - ``` - - Site 2 (`campaign_run_record_roundtrips`). Replace: - - ```rust - ], - }], - generalizations: vec![], - }; - ``` - - with: - - ```rust - ], - }], - generalizations: vec![], - trace_name: None, - }; - ``` - - Site 3 (`campaign_run_record_roundtrips_with_annotator_fields`, where `generalizations` is a non-empty vec). Replace: - - ```rust - missing: vec!["US500".to_string()], - }], - }; - ``` - - with: - - ```rust - missing: vec!["US500".to_string()], - }], - trace_name: None, - }; - ``` - - **(d) `crates/aura-registry/tests/campaign_run_store_e2e.rs` — the `campaign_run` helper literal.** Replace: - - ```rust - }], - generalizations: vec![], - } - } - ``` - - with: - - ```rust - }], - generalizations: vec![], - trace_name: None, - } - } - ``` - - Run: `cargo test -p aura-registry` - Expected outcome: green — every target compiles and all tests pass (0 failed), including the new `append_campaign_run_composes_trace_name_from_a_claim` and the extended `campaign_run_line_without_new_fields_still_parses`. The existing whole-record `PartialEq` round-trip asserts stay green: their literals now carry `trace_name: None`, no claim is made, and the stored line keeps `None`. - -- [ ] **Step 3: RED — campaign pin for the eligibility stamp + returned/stored agreement.** - - In `crates/aura-campaign/tests/execute.rs`, two edits. - - **(a)** Pin the `None` side on the existing sweep-only test (this assert passes already — the `campaign()` helper builds `persist_taps: vec![]`; it pins that empty taps NEVER grow a name). In `execute_sweep_only_records_family_and_selection`, replace: - - ```rust - assert_eq!(runs[0].run, 0); - assert_eq!(runs[0].seed, 7); - assert_eq!(runs[0].cells.len(), 1); - ``` - - with: - - ```rust - assert_eq!(runs[0].run, 0); - assert_eq!(runs[0].seed, 7); - assert_eq!( - runs[0].trace_name, None, - "empty persist_taps claims no trace name (0109 contract)" - ); - assert_eq!(runs[0].cells.len(), 1); - ``` - - **(b)** Append the new test at the end of the file. Replace the file's final lines: - - ```rust - // the record round-trips through the store with BOTH annotations intact - let runs = reg.load_campaign_runs().expect("load campaign runs"); - assert_eq!(runs[0], out.record); - } - ``` - - with: - - ```rust - // the record round-trips through the store with BOTH annotations intact - let runs = reg.load_campaign_runs().expect("load campaign runs"); - assert_eq!(runs[0], out.record); - } - - /// The 0109 name contract (spec seam note / #201 d5): `execute` claims a - /// TraceStore family name iff the document requests persist_taps — the - /// RETURNED record and the STORED line both carry the same derived - /// `Some("{campaign8}-{run}")` (`append_campaign_run` composes it onto the - /// stored line from the claim sentinel; `execute` mirrors the same - /// derivation onto the returned copy after the counter is assigned). The - /// empty-taps `None` side is pinned on - /// `execute_sweep_only_records_family_and_selection`. - #[test] - fn execute_persist_taps_stamps_trace_name() { - let reg = temp_registry("persist_taps_trace_name"); - let mut doc = campaign(&["EURUSD"]); - doc.presentation.persist_taps = vec!["equity".to_string()]; - let proc_doc = process(vec![sweep_stage(false)]); - let out = execute(CAMPAIGN_ID, &doc, &proc_doc, &strategies(), &FakeRunner::clean(), ®) - .expect("persist_taps campaign executes"); - - // returned record: the derived name over the first run counter - let expected = format!("{}-0", &CAMPAIGN_ID[..8]); - assert_eq!( - out.record.trace_name.as_deref(), - Some(expected.as_str()), - "the returned record carries the derived trace name", - ); - - // the stored line agrees — in the field and as a whole record - let runs = reg.load_campaign_runs().expect("load campaign runs"); - assert_eq!(runs.len(), 1); - assert_eq!(runs[0].trace_name.as_deref(), Some(expected.as_str())); - assert_eq!(runs[0], out.record, "stored line and returned record are the same record"); - - // a second run of the same campaign derives the next counter's name - let second = execute(CAMPAIGN_ID, &doc, &proc_doc, &strategies(), &FakeRunner::clean(), ®) - .expect("second persist_taps run"); - let expected_1 = format!("{}-1", &CAMPAIGN_ID[..8]); - assert_eq!(second.record.trace_name.as_deref(), Some(expected_1.as_str())); - } - ``` - - (`CAMPAIGN_ID` is the file's existing 64-hex constant; its first 8 chars are `aaaabbbb`. `Presentation`'s fields are `pub`, so the `doc.presentation.persist_taps` mutation compiles.) - - Run: `cargo test -p aura-campaign execute_persist_taps_stamps_trace_name` - Expected outcome: compiles, and the one matched test **FAILS** (`0 passed; 1 failed`) on the first assert — left `None`, right `Some("aaaabbbb-0")` — because `execute` does not yet set the claim sentinel. This is the RED evidence. - -- [ ] **Step 4: GREEN — the claim sentinel + the returned-copy mirror in `execute`.** - - In `crates/aura-campaign/src/exec.rs`, replace the record finalization at the end of `execute` (note: `campaign_prefix` is already in scope — `let campaign_prefix = &campaign_id[..8];` earlier in the same function): - - ```rust - let mut record = CampaignRunRecord { - campaign: campaign_id.to_string(), - process: process_id, - run: 0, - seed: campaign.seed, - cells: cells_rec, - generalizations, - }; - let run = registry.append_campaign_run(&record).map_err(ExecFault::Registry)?; - record.run = run; - Ok(CampaignOutcome { record, run, cells: cells_out }) - ``` - - with: - - ```rust - let mut record = CampaignRunRecord { - campaign: campaign_id.to_string(), - process: process_id, - run: 0, - seed: campaign.seed, - cells: cells_rec, - generalizations, - // The claim sentinel (#201 d5): a `Some` (any content) tells - // `append_campaign_run` to compose the derived trace name onto the - // stored line; `None` means the document requests no taps. - trace_name: (!campaign.presentation.persist_taps.is_empty()).then(String::new), - }; - let run = registry.append_campaign_run(&record).map_err(ExecFault::Registry)?; - record.run = run; - // Mirror the store-side derivation onto the returned copy (the 0109 - // contract: stored line and returned record carry the same - // `Some("{campaign8}-{run}")` iff persist_taps is non-empty, else None). - record.trace_name = (!campaign.presentation.persist_taps.is_empty()) - .then(|| format!("{campaign_prefix}-{run}")); - Ok(CampaignOutcome { record, run, cells: cells_out }) - ``` - - Run: `cargo test -p aura-campaign` - Expected outcome: green, 0 failed across all targets — `execute_persist_taps_stamps_trace_name` now passes, and every existing whole-record `PartialEq` assert (`execute_sweep_only_records_family_and_selection`, `execute_is_deterministic_twice`, `execute_mc_after_gate_bootstraps_each_survivor`, `execute_generalize_across_two_instruments`, `execute_mc_and_generalize_compose_in_one_pipeline`) stays green: those campaigns have empty `persist_taps`, so both the stored and returned records carry `trace_name: None`. - -- [ ] **Step 5: Workspace gates — build + the aura-cli byte-identity pin.** - - No code changes. Run, in order: - - 1. `cargo build --workspace` - Expected outcome: builds cleanly (no other crate constructs a `CampaignRunRecord` literal — the only sites are the ones threaded in Steps 2 and 4). - - 2. `cargo test -p aura-cli --test research_docs campaign_runs_lists` - Expected outcome: `1 passed; 0 failed` — the matched test is the hand-seeded byte-identity pin `campaign_runs_lists_and_dumps_the_bare_stored_record`. It stays green without modification: its seeded store line omits `trace_name`, which parses to `None` via the serde default, and the dump path RE-serializes the parsed record (`serde_json::to_string` in `aura-cli/src/research_docs.rs`), where `skip_serializing_if = "Option::is_none"` omits the field again — so the dump remains byte-identical to the seeded line. -### Task 3: persist_campaign_traces — the consumer wiring (aura-cli) - -**Files:** `crates/aura-cli/src/campaign_run.rs`, `crates/aura-cli/tests/research_docs.rs` - -- [ ] **Step 1: RED — pin the deferral line's retirement on the Task-1(b) seam test.** - In `crates/aura-cli/tests/research_docs.rs`, locate the seam test Task 1 created as split (b) of the old `campaign_run_persist_taps_deferred_loudly`: the `#[test]` fn whose fixture writes a campaign via `campaign_doc_json(&bp_id, &proc_id, (1, 2), "\"equity\"", "")` (a VALID tap over the deterministic-refusal 1970 window) and asserts exit 1 with the member-data refusal (`no recorded geometry` / `no data for instrument`). - 1. If any assertion in that test mentions `persist_taps not yet honored` (tolerating or requiring the 0107 deferral line), delete that whole `assert!` statement. - 2. Append, as the LAST two statements of the function body (immediately before its closing `}`): - - ```rust - assert!( - !out.contains("persist_taps not yet honored"), - "the 0107 deferral line is retired; persist_taps is honored from 0109 on: {out}" - ); - assert!( - !out.contains("traces persisted:"), - "a member-data refusal aborts inside execute, before any tracing: {out}" - ); - ``` - - Run: `cargo test -p aura-cli --test research_docs campaign_run` - Expected: exactly ONE failure — the tightened seam test fails on the new `!out.contains("persist_taps not yet honored")` assertion (the deferral `eprintln!` still fires in `campaign_run.rs`); every other `campaign_run*` test passes or prints its data-less skip note. This is the RED pin for this task. - -- [ ] **Step 2: GREEN — wire trace persistence into `crates/aura-cli/src/campaign_run.rs`.** - Four edits, all in this file. - - **(a) Imports.** Replace this exact block (top of file): - - ```rust - use std::path::{Path, PathBuf}; - use std::sync::Arc; - - use aura_campaign::{CellSpec, ExecFault, MemberFault, MemberRunner}; - use aura_core::{Cell, ParamSpec, Scalar}; - use aura_engine::{blueprint_from_json, FamilySelection, RunReport}; - use aura_ingest::{instrument_geometry, unix_ms_to_epoch_ns, M1Field, M1FieldSource}; - use aura_registry::CampaignRunRecord; - use aura_research::{ - campaign_to_json, content_id_of, parse_campaign, parse_process, validate_campaign, - validate_process, DocRef, - }; - ``` - - with: - - ```rust - use std::path::{Path, PathBuf}; - use std::sync::{mpsc, Arc}; - - use aura_campaign::{CampaignOutcome, CellSpec, ExecFault, MemberFault, MemberRunner}; - use aura_core::{Cell, ParamSpec, Scalar, ScalarKind, Timestamp}; - use aura_engine::{ - blueprint_from_json, f64_field, summarize, summarize_r, ColumnarTrace, FamilySelection, - RunReport, - }; - use aura_ingest::{instrument_geometry, unix_ms_to_epoch_ns, M1Field, M1FieldSource}; - use aura_registry::{CampaignRunRecord, WriteKind}; - use aura_research::{ - campaign_to_json, content_id_of, parse_campaign, parse_process, validate_campaign, - validate_process, CampaignDoc, DocRef, - }; - ``` - - (The test module's own `use aura_core::ScalarKind;` stays as is — an explicit import shadows the `use super::*` glob without conflict.) - - **(b) Remove the deferral block.** Replace this exact block (inside `run_campaign`, just before the runner construction): - - ```rust - // Loud deferral (#198 decision 6), once per run — pinned ORDER: this - // prints after the document gates and BEFORE any member executes, so a - // data refusal inside execute cannot swallow it. - if !campaign.presentation.persist_taps.is_empty() { - eprintln!( - "aura: persist_taps not yet honored ({} tap(s) ignored)", - campaign.presentation.persist_taps.len() - ); - } - - let runner = CliMemberRunner { - ``` - - with: - - ```rust - let runner = CliMemberRunner { - ``` - - **(c) Call site + the new items.** The persist call lands AFTER the always-on final `campaign_run` stdout line — decided: stdout stays data-pure (the record line is the wire contract), the trace surface is a stderr concern, and the realization record is already stored by `execute`, so a trace failure exits 1 without un-recording it. Replace this exact block (the tail of `run_campaign`, including its closing brace): - - ```rust - println!( - "{}", - serde_json::to_string(&CampaignRunLine { campaign_run: &outcome.record }) - .expect("campaign run record serializes") - ); - Ok(()) - } - ``` - - with (the `#[cfg(test)]` module that follows stays untouched below the new items): - - ```rust - println!( - "{}", - serde_json::to_string(&CampaignRunLine { campaign_run: &outcome.record }) - .expect("campaign run record serializes") - ); - - // Trace persistence runs LAST, after the always-on record line (0109): - // stdout stays data-pure (the record line is the wire contract) and the - // trace surface is a stderr concern; the realization record is already - // stored by `execute`, so a trace failure exits 1 without un-recording it. - if let Some(trace_name) = &outcome.record.trace_name { - persist_campaign_traces( - trace_name, - &campaign.presentation.persist_taps, - &outcome, - &campaign, - &strategies, - &runner.server, - env, - )?; - } - Ok(()) - } - - /// Which drained wrap-convention channel a requested tap persists from on a - /// campaign trace re-run. The routing mirrors `persist_traces_r` (main.rs): - /// equity <- the SimBroker equity recorder (tx_eq), exposure <- the Bias - /// recorder (tx_ex), r_equity <- the cum_realized_r + unrealized_r recorder - /// (tx_req). `net_r_equity` has no channel here: the campaign runner wires - /// no cost leg (`wrap_r(.., cost: None)`), so a net curve is not produced - /// by this run's configuration. - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - enum TapChannel { - Equity, - Exposure, - REquity, - } - - /// Route one requested tap of the closed vocabulary to its drained channel; - /// `None` marks a tap this run cannot produce (the caller skips it loudly). - /// An unknown name cannot reach here (`validate_campaign` refuses it) and - /// maps to `None` all the same rather than guessing. - fn tap_channel(tap: &str) -> Option { - match tap { - "equity" => Some(TapChannel::Equity), - "exposure" => Some(TapChannel::Exposure), - "r_equity" => Some(TapChannel::REquity), - _ => None, - } - } - - /// The content-derived on-disk key of one campaign cell under - /// `traces//` — the `member_key` discipline (content, never a - /// runtime ordinal): strategy content-id prefix + instrument + - /// doc-positional window ordinal, sanitized to one portable path component. - fn campaign_cell_key(strategy: &str, instrument: &str, window_ordinal: usize) -> String { - let strategy8 = strategy.get(..8).unwrap_or(strategy); - crate::sanitize_component(&format!("{strategy8}-{instrument}-w{window_ordinal}")) - } - - /// Persist the requested `persist_taps` for every nominee cell under - /// `traces///` (0109, #201 d5): re-run each nominee - /// once in non-reduce trace mode over its own recorded `manifest.window`, - /// assert the re-run METRICS equal the recorded nominee metrics (the C1 - /// drift alarm — manifest fields are fresh-context and not compared), and - /// write the requested-AND-producible taps through the sweep verbs' - /// member-dir mechanism: a slash-joined `"{name}/{key}"` handed to - /// `TraceStore::write` (the `persist_traces_r(&format!("{name}/{key}"), ..)` - /// layout — the fn itself is not reused: it writes a fixed tap set and - /// process-exits on error). The written index manifest is the RECORDED - /// nominee's — the trace documents that run's provenance, not a re-derived - /// fresh-context one. Loud stderr per no-nominee cell and ONCE per - /// unproducible requested tap (producibility is run-configuration-level, - /// not per-cell); one summary line at the end. Every `Err` is a refusal - /// `campaign_cmd` renders as `aura: {msg}` + exit 1. - fn persist_campaign_traces( - trace_name: &str, - taps: &[String], - outcome: &CampaignOutcome, - campaign: &CampaignDoc, - strategies: &[(String, String)], - server: &Arc, - env: &Env, - ) -> Result<(), String> { - let store = env.trace_store(); - store - .ensure_name_free(trace_name, WriteKind::Family) - .map_err(|e| e.to_string())?; - - // Requested ∩ producible, in request order. When nothing producible was - // requested (net_r_equity only), no member dir is written and the - // summary honestly reports 0 tap(s). - let mut routed: Vec<(&str, TapChannel)> = Vec::new(); - for tap in taps { - match tap_channel(tap) { - Some(ch) => routed.push((tap.as_str(), ch)), - None => eprintln!( - "aura: tap \"{tap}\" is not produced by this run (needs a cost run); skipped" - ), - } - } - - // `outcome.cells` is index-aligned with `outcome.record.cells` by - // construction: `execute` pushes both in the same cell-loop iteration. - let mut persisted_cells = 0usize; - for (cell_rec, cell_out) in outcome.record.cells.iter().zip(&outcome.cells) { - let Some((_, nominee_report)) = &cell_out.nominee else { - eprintln!( - "aura: cell {}/{}/[{}, {}]: no nominee; no traces persisted", - cell_rec.strategy, cell_rec.instrument, cell_rec.window_ms.0, cell_rec.window_ms.1 - ); - continue; - }; - if routed.is_empty() { - continue; - } - - // The cell key is doc-derived: the window ordinal is the POSITION - // of the cell's window in campaign.data.windows (mirroring the - // family-name convention), never a loop counter re-derived here. - let window_ordinal = campaign - .data - .windows - .iter() - .position(|w| (w.from_ms, w.to_ms) == cell_rec.window_ms) - .ok_or_else(|| { - format!( - "cell window [{}, {}] is not one of campaign.data.windows", - cell_rec.window_ms.0, cell_rec.window_ms.1 - ) - })?; - let cell_key = - campaign_cell_key(&cell_rec.strategy, &cell_rec.instrument, window_ordinal); - - // The cell's blueprint: the run's own index-aligned resolution, by - // content id (exactly the bytes the executor's members ran). - let (_, blueprint_json) = strategies - .iter() - .find(|(id, _)| *id == cell_rec.strategy) - .ok_or_else(|| { - format!( - "cell strategy {} is not among the run's resolved strategies", - cell_rec.strategy - ) - })?; - - // Re-run the nominee, non-reduce: the SAME member the executor ran - // (same wrapped space, same params, same window, seed-free real - // data), mirroring `CliMemberRunner::run_member` with the reduce - // fold off so the per-cycle tap streams exist. The window is the - // nominee report's own `manifest.window` — already epoch-ns - // (`run_blueprint_member` stamped the post-seam bounds), so no - // second ms->ns crossing here. - let space = crate::blueprint_axis_probe(blueprint_json, env).param_space(); - let point = crate::point_from_params(&space, &nominee_report.manifest.params); - if instrument_geometry(server, &cell_rec.instrument).is_none() { - return Err(format!( - "no recorded geometry for symbol '{}' at {} — refusing to run a \ - real instrument with a guessed pip", - cell_rec.instrument, - env.data_path() - )); - } - let (from, to) = nominee_report.manifest.window; - let no_data = || { - format!( - "no data for instrument {} in the nominee window [{}, {}] (epoch-ns)", - cell_rec.instrument, from.0, to.0 - ) - }; - let source = M1FieldSource::open_window( - server, - &cell_rec.instrument, - Some(from), - Some(to), - M1Field::Close, - ) - .ok_or_else(no_data)?; - if aura_engine::Source::peek(&source).is_none() { - return Err(no_data()); - } - let signal = blueprint_from_json(blueprint_json, &|t| env.resolve(t)) - .expect("stored blueprint passed the referential gate; reload is infallible"); - let (tx_eq, rx_eq) = mpsc::channel(); - let (tx_ex, rx_ex) = mpsc::channel(); - let (tx_r, rx_r) = mpsc::channel(); - let (tx_req, rx_req) = mpsc::channel(); - let mut h = crate::wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, false, false, None) - .bootstrap_with_cells(&point) - .expect("the nominee's point re-bootstraps (it already ran this realization)"); - let sources: Vec> = vec![Box::new(source)]; - h.run(sources); - // Drain ALL FOUR channels (`run_signal_r` leaves req undrained; the - // trace path must not): eq/ex/req feed the taps, r feeds the metrics. - let eq_rows: Vec<(Timestamp, Vec)> = rx_eq.try_iter().collect(); - let ex_rows: Vec<(Timestamp, Vec)> = rx_ex.try_iter().collect(); - let r_rows: Vec<(Timestamp, Vec)> = rx_r.try_iter().collect(); - let req_rows: Vec<(Timestamp, Vec)> = rx_req.try_iter().collect(); - - // The C1 drift alarm: metrics equality against the recorded - // nominee. The reduce-mode fold shares its arithmetic with this - // non-reduce reduction (SeriesFold via `summarize`; GatedRecorder - // emits exactly the rows `summarize_r`'s ledger reads), so equality - // is bit-exact. - let mut rerun_metrics = summarize(&f64_field(&eq_rows, 0), &f64_field(&ex_rows, 0)); - rerun_metrics.r = Some(summarize_r(&r_rows, &[])); - if rerun_metrics != nominee_report.metrics { - return Err(format!( - "trace re-run diverged from the recorded nominee (C1 violation): cell \ - {cell_key} of trace {trace_name} does not reproduce its recorded \ - metrics; refusing to persist a silently-wrong trace" - )); - } - - let traces: Vec = routed - .iter() - .map(|&(tap, ch)| { - let rows: &[(Timestamp, Vec)] = match ch { - TapChannel::Equity => &eq_rows, - TapChannel::Exposure => &ex_rows, - TapChannel::REquity => &req_rows, - }; - ColumnarTrace::from_rows(tap, &[ScalarKind::F64], rows) - }) - .collect(); - store - .write(&format!("{trace_name}/{cell_key}"), &nominee_report.manifest, &traces) - .map_err(|e| e.to_string())?; - persisted_cells += 1; - } - - eprintln!( - "aura: traces persisted: {trace_name} ({} tap(s) x {persisted_cells} cell(s))", - routed.len() - ); - Ok(()) - } - ``` - - **(d) Hermetic unit tests.** In the same file's `#[cfg(test)] mod tests`, replace this exact block (the last test fn plus the module's closing brace): - - ```rust - assert!( - m.contains("open param \"threshold\"") && m.contains("(wrapped path: sma.threshold)"), - "raw-namespace prose expected: {m}" - ); - } - } - ``` - - with: - - ```rust - assert!( - m.contains("open param \"threshold\"") && m.contains("(wrapped path: sma.threshold)"), - "raw-namespace prose expected: {m}" - ); - } - - #[test] - /// 0109: the campaign cell key is content-derived (the `member_key` - /// discipline — never a runtime ordinal): strategy content-id prefix + - /// instrument + doc-positional window ordinal, one portable component. - fn campaign_cell_key_is_content_derived() { - let strategy = "bb34aa55".repeat(8); // a 64-hex content id - assert_eq!(campaign_cell_key(&strategy, "GER40", 0), "bb34aa55-GER40-w0"); - } - - #[test] - /// A non-portable instrument byte maps to `_` (the shared - /// `sanitize_component` charset) — one path component, never a nested - /// path or an invalid directory name. - fn campaign_cell_key_sanitizes_non_portable_bytes() { - let strategy = "0123abcd".repeat(8); - assert_eq!(campaign_cell_key(&strategy, "GER/40", 3), "0123abcd-GER_40-w3"); - } - - #[test] - /// The tap->channel routing mirrors `persist_traces_r` (equity<-eq, - /// exposure<-ex, r_equity<-req); `net_r_equity` is unproducible on the - /// campaign runner's cost-free wrap and routes to no channel. - fn tap_channel_routes_the_producible_vocabulary_only() { - assert_eq!(tap_channel("equity"), Some(TapChannel::Equity)); - assert_eq!(tap_channel("exposure"), Some(TapChannel::Exposure)); - assert_eq!(tap_channel("r_equity"), Some(TapChannel::REquity)); - assert_eq!(tap_channel("net_r_equity"), None); - } - } - ``` - - Run: `cargo build --workspace` - Expected: clean build, no warnings from the touched crate. - -- [ ] **Step 3: gates.** - Run: `cargo test -p aura-cli --bin aura campaign_cell_key` - Expected: 2 passed (`campaign_cell_key_is_content_derived`, `campaign_cell_key_sanitizes_non_portable_bytes`), 0 failed. - Run: `cargo test -p aura-cli --bin aura tap_channel` - Expected: 1 passed (`tap_channel_routes_the_producible_vocabulary_only`), 0 failed. - Run: `cargo test -p aura-cli --test research_docs campaign_run` - Expected: ALL `campaign_run*` tests pass (the Step-1 seam test is now GREEN: the deferral line is gone and the member-data refusal aborts inside `execute` before any tracing; on a data-less host the two real-data e2e tests print their skip note and pass). - -### Task 4: gated e2e extension - -**Files:** `crates/aura-cli/tests/research_docs.rs` - -- [ ] **Step 1: extend `campaign_run_real_e2e_sweep_gate_walkforward` to pin the full trace path.** - Three edits, all in `crates/aura-cli/tests/research_docs.rs`. - - **(a) Doc comment.** Replace this exact block: - - ```rust - /// Gated real-data e2e (the `cli_run.rs` skip idiom): a full - /// sweep -> gate -> walk_forward -> monte_carlo campaign over GER40 - /// Sept-2024 where the local archive is present — exit 0, emit-gated - /// family/selection lines, a final line parseable as JSON with top-level - /// `campaign_run` linking a sweep family id, a walk-forward family id, and a - /// pooled-OOS bootstrap on the terminal mc stage, and the - /// `campaign_runs.jsonl` sibling store written. Skips with a note elsewhere - /// so `cargo test --workspace` stays green on a data-less machine. - ``` - - with: - - ```rust - /// Gated real-data e2e (the `cli_run.rs` skip idiom): a full - /// sweep -> gate -> walk_forward -> monte_carlo campaign over GER40 - /// Sept-2024 where the local archive is present — exit 0, emit-gated - /// family/selection lines, a final line parseable as JSON with top-level - /// `campaign_run` linking a sweep family id, a walk-forward family id, and a - /// pooled-OOS bootstrap on the terminal mc stage, and the - /// `campaign_runs.jsonl` sibling store written. Since 0109 the campaign - /// also requests `persist_taps: ["equity", "r_equity"]`: the record carries - /// `trace_name`, the nominee's taps land under `traces///` - /// (the persist re-run equality-asserted against the recorded nominee — - /// C1), and `aura chart /` reads them back through the - /// shipped viewer. Skips with a note elsewhere so `cargo test --workspace` - /// stays green on a data-less machine. - ``` - - **(b) The persist_taps splice.** Replace this exact block (inside the test): - - ```rust - write_doc( - dir, - "wf.campaign.json", - &campaign_doc_json( - &bp_id, - &proc_id, - (1725148800000, 1727740799999), - "", - "\"family_table\", \"selection_report\"", - ), - ); - ``` - - with: - - ```rust - write_doc( - dir, - "wf.campaign.json", - &campaign_doc_json( - &bp_id, - &proc_id, - (1725148800000, 1727740799999), - "\"equity\", \"r_equity\"", - "\"family_table\", \"selection_report\"", - ), - ); - ``` - - **(c) The trace assertions.** Replace this exact block (the tail of the test, including its closing brace): - - ```rust - // The registry's new sibling store carries the realization record. - assert!( - runs_dir.join("campaign_runs.jsonl").is_file(), - "campaign_runs.jsonl written beside runs.jsonl" - ); - } - ``` - - with: - - ```rust - // The registry's new sibling store carries the realization record. - assert!( - runs_dir.join("campaign_runs.jsonl").is_file(), - "campaign_runs.jsonl written beside runs.jsonl" - ); - // 0109: the record claims the trace family it persisted under — - // "{campaign8}-{run}" (fresh store => run 0). - let trace_name = v["campaign_run"]["trace_name"] - .as_str() - .expect("persist_taps non-empty => the record carries trace_name"); - assert!( - trace_name.len() == 10 && trace_name.ends_with("-0"), - "trace_name is {{campaign8}}-{{run}}: {trace_name}" - ); - assert!( - trace_name[..8].chars().all(|c| c.is_ascii_hexdigit()), - "trace_name prefix is the campaign id's first 8 hex chars: {trace_name}" - ); - // The summary stderr line names the tap x cell arity (2 requested taps, - // one (strategy, instrument, window) cell). - assert!( - out.contains(&format!("aura: traces persisted: {trace_name} (2 tap(s) x 1 cell(s))")), - "the persist summary line: {out}" - ); - // The taps land under traces/// — the cell key is - // content-derived: strategy8-instrument-w. - let cell_key = format!("{}-GER40-w0", &bp_id[..8]); - let cell_dir = runs_dir.join("traces").join(trace_name).join(&cell_key); - assert!( - cell_dir.join("equity.json").is_file(), - "equity tap persisted at {}", - cell_dir.display() - ); - assert!( - cell_dir.join("r_equity.json").is_file(), - "r_equity tap persisted at {}", - cell_dir.display() - ); - assert!( - !cell_dir.join("exposure.json").exists(), - "unrequested taps are not persisted" - ); - // The shipped viewer reads the member back (the cli_run.rs chart - // idiom): exit 0 and an HTML page carrying both persisted tap series. - let (chart_out, chart_code) = - run_code_in(dir, &["chart", &format!("{trace_name}/{cell_key}")]); - assert_eq!(chart_code, Some(0), "aura chart reads the persisted member: {chart_out}"); - assert!( - chart_out.contains("\"equity\"") && chart_out.contains("\"r_equity\""), - "the chart page carries the two persisted taps: {chart_out}" - ); - } - ``` - - Run: `cargo test -p aura-cli --test research_docs campaign_run_real_e2e` - Expected: 2 passed, 0 failed. On a host with the local GER40 archive, `campaign_run_real_e2e_sweep_gate_walkforward` exercises the full trace path (record `trace_name`, persisted `equity.json` + `r_equity.json`, the C1 equality assert holding, chart read-back); on a data-less host both tests print `skip: no local GER40 data for the campaign e2e` and pass. - -- [ ] **Step 2: workspace gates.** - Run: `cargo test --workspace` - Expected: green — 0 failed across all crates (data-gated tests pass or skip per host). - Run: `cargo clippy --workspace --all-targets -- -D warnings` - Expected: clean, zero warnings. diff --git a/docs/specs/0109-persist-taps.md b/docs/specs/0109-persist-taps.md deleted file mode 100644 index 81acf80..0000000 --- a/docs/specs/0109-persist-taps.md +++ /dev/null @@ -1,222 +0,0 @@ -# persist_taps Wiring — Design Spec (cycle 0109) - -**Date:** 2026-07-04 -**Status:** Draft — /boss autonomous run; sign-off gate = grounding-check PASS -**Authors:** orchestrator + Claude -**Reference issue:** #201. All five design forks are decided on #201 -(fork-triage comment 2026-07-04; F1–F4 unanimous, F5 on a 4/5 majority with -the dissent recorded). One seam refinement made here and recorded below: the -F5 pointer field is stamped by the library as a pure name derivation, because -the run counter it needs is assigned inside `append_campaign_run`. - -## Goal - -`aura campaign run` honours `presentation.persist_taps`: after the pipeline -settles, each cell's **nominee** is re-run once in non-reduce trace mode -(bit-identical under C1 — the reproduce precedent, equality-asserted) and the -requested taps are persisted through the existing TraceStore under a -campaign-derived family name, chartable by the shipped web-from-disk contract -(`aura chart /`). The loud-deferral line dies; loud lines -remain only for what genuinely cannot persist (no nominee in a gate-truncated -cell; a vocabulary tap the run's configuration cannot produce). The tap -namespace becomes a **closed vocabulary** validated intrinsically. - -## Architecture - -- **aura-research** — `tap_vocabulary()` (the wrap convention's four sink - names) + `DocFault::UnknownTap` in `validate_campaign`; the persist_taps - slot hint names the vocabulary. Validation only — no content id moves; a - doc naming an unknown tap refuses at validate from this cycle on. -- **aura-registry** — `CampaignRunRecord.trace_name: Option` - (serde-default sparse; the C14/C23 widening convention). -- **aura-campaign** — ONE line of new semantics: `execute` stamps - `trace_name = Some("{campaign8}-{run}")` when `persist_taps` is non-empty - (a pure derivation from fields it already holds — the record declares which - store key this realization claims; writing bytes there stays the - consumer's). Everything else is byte-untouched; the `MemberRunner` seam - stays one-method (#201 decision 4). -- **aura-cli** — after `execute()` returns: if `trace_name` is `Some`, - `ensure_name_free(WriteKind::Family)` once, then per cell with a nominee: - re-run the nominee non-reduce (the `run_signal_r`-style channel drain over - the shipped wrap convention, windowed to the nominee report's own - `manifest.window`), assert the re-run report equals the recorded nominee - report (a C1 drift alarm — hard refusal on mismatch), and persist the - requested-AND-producible taps as `traces///.json` - (the `persist_traces_r` ColumnarTrace shapes). Loud stderr lines per - skipped cell (no nominee) and per unproducible requested tap - (`net_r_equity` without a cost run — the campaign runner wires none today). - -Determinism (C1): the re-run is the same member the executor already ran -(same params, same window, same seed conventions) — the equality assert turns -any divergence into a refusal instead of a silently-wrong trace. - -## Concrete code shapes - -### The user-facing program - -```json -"presentation": { "persist_taps": ["equity", "r_equity"], "emit": ["selection_report"] } -``` - -``` -$ aura campaign run campaign.json -…selection/record lines as today… -aura: traces persisted: bb34aa55-0 (2 tap(s) x 1 cell(s)) # stderr, once -$ aura chart bb34aa55-0/ # the shipped viewer, unchanged -$ aura campaign runs # record carries "trace_name":"bb34aa55-0" -``` - -A campaign naming an unknown tap refuses at validate: -`presentation.persist_taps[0]: unknown tap "bias" (taps: equity | exposure | r_equity | net_r_equity)`. -A gate-truncated cell: `aura: cell …: no nominee; no traces persisted`. A -requested-but-unproducible tap: -`aura: tap "net_r_equity" is not produced by this run (needs a cost run); skipped`. - -### aura-research - -```rust -/// The wrap convention's persisted sink names — the closed tap vocabulary -/// (#201 decision 1). A genuinely new observable becomes a new entry or an -/// authored sink in the strategy blueprint (C22: the choice of sinks is part -/// of the experiment) — never an open node-path namespace in the document. -pub fn tap_vocabulary() -> &'static [&'static str] { - &["equity", "exposure", "r_equity", "net_r_equity"] -} - -// DocFault gains: -UnknownTap { index: usize, tap: String }, -// validate_campaign: each presentation.persist_taps entry must be in -// tap_vocabulary() (mirror the UnknownEmitKind loop). -// open-slot / describe hint for persist_taps names the vocabulary -// ("list of: equity | exposure | r_equity | net_r_equity"). -``` - -### aura-registry - -```rust -pub struct CampaignRunRecord { - …existing… - /// The TraceStore family name this realization claims when the document - /// requests persist_taps — stamped by the executor as a pure derivation - /// ("{campaign8}-{run}"); the consumer persists the bytes (#201 d5). - /// None when the document requests no taps. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub trace_name: Option, -} -``` - -### aura-campaign (the one-line semantics + outcome plumbing) - -In `execute`, where the record is finalized after `append_campaign_run` -assigns `run`: - -```rust - record.run = run; - if !campaign.presentation.persist_taps.is_empty() { - record.trace_name = Some(format!("{campaign_prefix}-{run}")); - } -``` - -(The stored line is written by `append_campaign_run` BEFORE `run`/`trace_name` -are set on the returned copy — so the stamp must move INTO the store write: -`append_campaign_run` already overrides `run` on the stored line; the cleanest -honest shape is that `execute` computes `trace_name`'s ELIGIBILITY up front — -`record.trace_name = None` at build, and the store-side counter assignment -composes the name. Since the registry cannot know the eligibility rule, the -record handed to `append_campaign_run` carries a sentinel: `execute` sets -`trace_name = Some(String::new())` (empty = "claim a name") pre-append, and -`append_campaign_run` replaces a `Some` (any content) with the derived -`Some("{campaign-prefix8}-{run}")` on the stored line — the prefix from the -record's own `campaign` field — leaving `None` as `None`. `execute` then -mirrors the same derivation onto the returned copy. The planner pins the -exact seam; the CONTRACT is: stored line and returned record carry the same -`Some("{campaign8}-{run}")` iff `persist_taps` is non-empty, else `None`.) - -### aura-cli (the consumer) - -```rust -// campaign_run.rs, replacing the loud-deferral eprintln after execute(): -if let Some(trace_name) = &outcome.record.trace_name { - persist_campaign_traces(trace_name, &campaign.presentation.persist_taps, - &outcome, &strategies, env)?; // Err => exit-1 prose (C1 drift alarm included) -} - -// New fn (campaign_run.rs; the non-reduce member drain lives beside the -// runner's machinery in main.rs as a crate-root helper if cleaner): -// - env.trace_store().ensure_name_free(trace_name, WriteKind::Family) -// - per cell with nominee (params, report): -// cell_key = sanitize_component("{strategy8}-{instrument}-w{window_ordinal}") -// (content-derived, the member_key discipline — planner pins the exact -// composition; never a runtime ordinal) -// re-run: reload blueprint, wrap_r(sig, txs…, false, /*reduce=*/false, None), -// bind the nominee params (wrapped names — the manifest form), -// windowed sources over report.manifest.window (the ns bounds the -// member actually ran), drain eq/ex/r/req channels -// assert rerun_report == *report else Err("trace re-run diverged from the -// recorded nominee (C1 violation): …") -// persist requested ∩ producible taps via the TraceStore -// (equity/exposure/r_equity ColumnarTraces from the drained channels; -// net_r_equity only when a cost leg ran — today never: loud skip) -// - one summary stderr line: "aura: traces persisted: {name} ({t} tap(s) x {c} cell(s))" -``` - -## Components - -| Change | Crate | -|---|---| -| `tap_vocabulary`, `DocFault::UnknownTap`, validate + hints | aura-research | -| `CampaignRunRecord.trace_name` + the append-side name composition | aura-registry | -| the eligibility stamp + returned-copy mirror | aura-campaign | -| `persist_campaign_traces` (non-reduce nominee re-run + TraceStore write + prose), deferral line removed, `doc_fault_prose` arm | aura-cli | - -## Data flow - -``` -persist_taps non-empty ─▶ execute stamps eligibility ─▶ append_campaign_run - composes trace_name = "{campaign8}-{run}" onto the stored line + returned copy -CLI: trace_name Some ─▶ ensure_name_free ─▶ per nominee cell: - non-reduce re-run (window = nominee manifest.window) ─▶ equality assert (C1) - ─▶ TraceStore traces///.json ─▶ summary stderr line -web face: aura chart / — unchanged reader -``` - -## Error handling - -- Unknown tap: intrinsic `DocFault::UnknownTap`, path-addressed prose with the - vocabulary enumerated (validate AND the run's parse-valid gate). -- Name collision (`ensure_name_free`): the store's cross-kind refusal, exit 1. -- Re-run divergence: hard exit-1 refusal naming the C1 violation — never a - silently-wrong trace. -- No nominee / unproducible tap: loud stderr per case, run stays exit 0. - -## Testing strategy - -1. **aura-research:** vocabulary + `UnknownTap` fault (RED-first), hint lines. -2. **aura-registry:** `trace_name` widening round-trip; pre-0109 line parses; - append-side composition (sentinel `Some` → derived name on the stored - line AND the counter's run; `None` stays `None`). -3. **aura-campaign (fake runner):** `execute` returns `trace_name` - `Some("{campaign8}-0")` iff persist_taps non-empty (and the stored line - agrees — read back via `load_campaign_runs`). -4. **aura-cli seam:** unknown-tap validate refusal; the persist_taps - stderr surface flips from the deferral line to the new summary/skip lines - (the 0107 `campaign_run_persist_taps_deferred_loudly` test flips — - data-less hosts hit the member-data seam BEFORE tracing, so the flipped - test pins the validate-tier + prose, and the full trace path is pinned by - the gated e2e). -5. **Gated real-data e2e:** extend the shipped e2e campaign with - `persist_taps: ["equity", "r_equity"]`; assert exit 0, the record's - `trace_name`, the trace files exist under `traces///`, - and `aura chart /` (or the store's `read_family`) reads - them; skip where data is absent. - -## Acceptance criteria - -1. The worked program persists nominee traces chartable by the shipped - viewer; the record's `trace_name` navigates to them (no reader-side - convention knowledge needed — the F10 lesson). -2. `persist_taps: []` behaves exactly as today (`trace_name: None`, no lines). -3. Unknown taps refuse at validate; unproducible taps and gate-truncated - cells skip loudly; the deferral line is gone. -4. The re-run equality assert holds on the gated e2e (C1). -5. Suite green, clippy clean, doc build clean; pre-0109 stored records parse. diff --git a/docs/specs/fieldtest-annotator-stages.md b/docs/specs/fieldtest-annotator-stages.md deleted file mode 100644 index 522b5e7..0000000 --- a/docs/specs/fieldtest-annotator-stages.md +++ /dev/null @@ -1,133 +0,0 @@ -# Fieldtest — cycle 0108 (annotator stages, #200) — 2026-07-04 - -**Status:** Draft — awaiting orchestrator triage -**Author:** fieldtester (dispatched by fieldtest skill) - -## Scope -Cycle 0108 makes the two terminal annotator stages execute on top of the 0107 -campaign loop. The v2 pipeline shape is -`std::sweep [std::gate]* [std::walk_forward]? [std::monte_carlo]? [std::generalize]?`. -`std::monte_carlo` bootstraps the stage's incoming R-evidence with -position-dependent semantics (PooledOos over the walk_forward OOS series, else -PerSurvivor over each gate survivor's in-sample series); `std::generalize` runs -at campaign scope over per-cell nominees per (strategy, window), scoring the -cross-instrument worst-case R floor. Both are terminal (nothing flows out). The -realization records widen serde-default-sparse (`StageRealization.bootstrap`, -`CampaignRunRecord.generalizations`). A static preflight admits the shape and -refuses the eight off-shape/param/metric/instrument faults. - -## Examples -### fieldtests/cycle-0108-annotator-stages/c0108_campaign_full_pooled.json — full-suffix pooled campaign -- sweep(deflate) → gate → walk_forward → monte_carlo over GER40 2024-09. -- Fits scope: exercises the PooledOos mc path — the headline new surface. -- Outcome: built ✓, ran ✓ (exit 0), matched expected ✓ (`pooled_oos` bootstrap, - `n_trades:1843` = the two wf OOS windows 948+895 pooled). - -### fieldtests/cycle-0108-annotator-stages/c0108_campaign_mc_persurvivor.json — no-wf per-survivor campaign -- sweep → gate → monte_carlo over GER40 2024-09. -- Fits scope: exercises the PerSurvivor mc path (the other input shape). -- Outcome: built ✓, ran ✓ (exit 0), matched expected ✓ (`per_survivor` list of - `[ordinal, bootstrap]` over survivors [0,1,2]; survivor 1 flagged - `prob_le_zero:0.525`). - -### fieldtests/cycle-0108-annotator-stages/c0108_campaign_2instr_generalize.json — end-to-end generalize -- sweep → gate → wf → mc → generalize over GER40 + FRA40, 2024-09. -- Fits scope: exercises campaign-scope `std::generalize` (needs ≥2 instruments). -- Outcome: built ✓, ran ✓ (exit 0), matched expected ✓ (`generalizations` entry, - `worst_case:0.0437`, `sign_agreement:2`, nominees = each cell's last wf-OOS - winner; sparse widening present only on this line in the store). - -### fieldtests/cycle-0108-annotator-stages/c0108_probe_*.json — annotator boundary probes -- gate-after-mc, wf-after-mc, dup-mc, gen-not-last, zero-resamples, zero-blocklen, - non-R-generalize, single-instrument-generalize (+ their campaign runners). -- Fits scope: probes both refusal tiers (intrinsic vs executor preflight). -- Outcome: built ✓, ran ✓, matched expected ✓ (all refuse; exit 1; one intrinsic, - seven executor-tier — see F5). - -## Findings -### [working] F1 — full annotator suffix closes; pooled_oos over the wf OOS series -- `c0108_campaign_full_pooled.json`. mc records `pooled_oos` with `n_trades:1843` - provably = 948+895 (the two wf OOS windows pooled in roll order). Honest, - recorded, seeded. Recommended action: carry-on. - -### [working] F2 — no-wf campaign records one bootstrap per survivor; honesty seam bites -- `c0108_campaign_mc_persurvivor.json`. `per_survivor` over gate survivors - [0,1,2] each over the member's in-sample R series; survivor 1 passes the gate at - `expectancy_r:0.0019` but the bootstrap flags `prob_le_zero:0.525`. Carry-on. - -### [working] F3 — determinism is byte-identical modulo the run counter -- Three runs of `c0108_campaign_full_pooled.json`; every bootstrap percentile / - `prob_le_zero` / survivor set / selection reproduces. C1 intact. Carry-on. - -### [working] F4 — campaign-scope generalize closes end-to-end over two instruments -- `c0108_campaign_2instr_generalize.json`. `worst_case:0.0437`, - `sign_agreement:2`, nominees = last wf-OOS winner per cell, divergent winners - exposed via params; sparse `generalizations` key present only on this line. - Carry-on. - -### [working] F5 — the annotator tier boundary is honest and precise on both sides -- All `c0108_probe_*` runners. Intrinsic (`process validate`) catches only - gate-after-terminal; the executor preflight catches wf-after-mc, dup-mc, - gen-not-last, zero resamples/block_len, non-R metric, single-instrument - generalize — each with per-pair prose, exit 1. Carry-on. - -### [friction] F6 — `campaign validate` blesses executor-refused process shapes -- `c0108_probe_campaign_{wf_after_mc,dup_mc,gen_not_last}.json`: `campaign - validate` returns exit 0, only `campaign run` refuses. wf-after-mc is a pure - process-shape fault detectable without data. The three-tier model is invisible - from validate. Recommended action: plan (run the executor shape/param preflight - at validate time, or name the third tier in the validate output). - -### [doc-gap] F7 — pooled-vs-per-survivor duality invisible to the author surface -- `--block std::monte_carlo` says only "R-bootstrap over realised R". Nothing on - the CLI/glossary surface tells the author the output shape+semantics flip on - whether wf precedes mc (documented only in ledger C18 0108). Recommended - action: tighten docs (fold position-dependent semantics into `--block` / - glossary). - -### [doc-gap] F8 — generalize R-metric set unpredictable; refusal prose misleads -- Accepted: expectancy_r, net_expectancy_r, sqn, sqn_normalized. Refused: - total_pips + max_drawdown (both rankable), win_rate, and `max_r_drawdown` — - R-denominated yet refused as "not an R metric (… pip metrics do not)". - `--metrics` has no R/generalizable tag. Fixture: - `c0108_probe_generalize_maxrdrawdown.json`. Recommended action: tighten (add - applicability tag to `--metrics`; correct refusal prose for R-but-unranked - metrics). - -### [doc-gap] F9 — mc slot hint says "non-negative" but the executor refuses 0 -- `--block std::monte_carlo` labels resamples/block_len "non-negative integer - (u32)" (the type), but 0 is refused (`… must be > 0`). Fixture: - `c0108_probe_campaign_zero_resamples.json`. Recommended action: tighten the - slot hint to "positive integer". - -### [doc-gap] F10 — the recorded realization output schema is undocumented -- `prob_le_zero`, `per_survivor`, `pooled_oos`, `worst_case`, `sign_agreement`, - `e_r` percentiles appear in no public prose (only internal enum names in the - ledger). Stdout wraps as `{"campaign_run":…}` but the persisted JSONL is bare. - Recommended action: tighten docs (define the realization fields; a - glossary/introspect schema). - -### [friction] F11 — annotator realizations are write-only (no CLI read-back) -- `aura runs` exposes only families; no verb reads back a `campaign_run`. The - bootstrap/generalization records live only in run stdout + a gitignored JSONL, - one unbounded line per run. Recommended action: plan (an `aura campaign runs` / - `run ` read-back verb). - -## Recommendation summary -| finding | class | action | -|---|---|---| -| F1 pooled_oos over wf OOS | working | carry-on | -| F2 per_survivor + honesty seam | working | carry-on | -| F3 determinism byte-identical | working | carry-on | -| F4 campaign-scope generalize | working | carry-on | -| F5 tier boundary precise | working | carry-on | -| F6 validate blesses unrunnable shapes | friction | plan | -| F7 pooled-vs-per-survivor undiscoverable | doc-gap | tighten the design ledger / introspection | -| F8 generalize R-metric set + prose | doc-gap | tighten docs / prose | -| F9 "non-negative" hint vs >0 guard | doc-gap | tighten introspection hint | -| F10 realization output schema undocumented | doc-gap | tighten docs | -| F11 no realization read-back | friction | plan | - -**Verdict:** Yes — the annotator stages deliver honest, recorded, reproducible -inference on the 0107 loop. No bugs. Residue is discoverability at the author's -first contact with the two new stages (F6–F11).