# 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.