audit: cycle 0109 tidy (drift resolved by ledger/glossary lift + tap-channel cross-pin)

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
This commit is contained in:
2026-07-04 04:22:41 +02:00
parent 46a85d3ea4
commit 179c2f8bf0
6 changed files with 38 additions and 1824 deletions
+11
View File
@@ -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<TapChannel> {
// 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),
+24 -2
View File
@@ -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
+3 -3
View File
@@ -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 <file|content id>`; 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 <file|content id>`; 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:** —
File diff suppressed because it is too large Load Diff
-222
View File
@@ -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; F1F4 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 <name>/<cell-key>`). 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<String>`
(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/<name>/<cell-key>/<tap>.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/<cell-key> # the shipped viewer, unchanged
$ aura campaign runs <campaign-id> # 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<String>,
}
```
### 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/<name>/<cell-key>/<tap>.json ─▶ summary stderr line
web face: aura chart <name>/<cell-key> — 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/<name>/<cell-key>/`,
and `aura chart <name>/<cell-key>` (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.
-133
View File
@@ -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 <id>` 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 (F6F11).