feat(research,registry,campaign): tap vocabulary + trace_name record contract (0109 tasks 1-2)

Task 1: the tap namespace is a closed vocabulary — tap_vocabulary()
(the wrap convention's four sink names; escalation = new entry or an
authored blueprint sink, never an open node-path namespace) with
DocFault::UnknownTap in validate_campaign (index-addressed, prose
enumerating the vocabulary via the single source), SlotKind::TapKinds
on the persist_taps slot, open-slot hint naming the taps. The 0107
deferral seam test split into its two 0109 pins (unknown tap refuses
at validate; a valid tap passes validate and hits the member-data
seam) — the deferral eprintln itself survives until task 3.

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

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

refs #201
This commit is contained in:
2026-07-04 03:41:01 +02:00
parent ef7579f8fb
commit b8ef77b1c7
8 changed files with 390 additions and 27 deletions
+117 -10
View File
@@ -629,6 +629,25 @@ fn campaign_introspect_metrics_also_lists_the_vocabulary() {
);
}
/// 0109 task 1 (#201 decision 1): the closed tap vocabulary is discoverable
/// through the actual CLI binary, not just the `aura-research` library —
/// `campaign introspect --block std::presentation` composes
/// `describe_block` + `slot_kind_label` over the new `SlotKind::TapKinds`,
/// and this pins that composition reaches stdout with the exact four names,
/// so a future edit to `tap_vocabulary()` that forgets the CLI-visible label
/// (there is no other CLI test exercising `--block std::presentation`) fails
/// here instead of only in a library-level unit test.
#[test]
fn campaign_introspect_block_presentation_names_the_tap_vocabulary() {
let (out, code) = run_code(&["campaign", "introspect", "--block", "std::presentation"]);
assert_eq!(code, Some(0), "stdout/stderr: {out}");
assert!(
out.contains("list of: equity | exposure | r_equity | net_r_equity"),
"the block description must name the closed tap vocabulary: {out}"
);
assert!(out.contains("persist_taps"), "stdout/stderr: {out}");
}
/// Register must be a gate, not a passthrough: an invalid document is
/// refused with prose (exit 1) and — the property that matters for a
/// content-addressed store — no file is ever written under `runs/campaigns/`
@@ -900,6 +919,42 @@ fn campaign_runs_lists_and_dumps_the_bare_stored_record() {
);
}
/// 0109 task 2: `campaign runs <id>` dumps by deserializing the stored line
/// into `CampaignRunRecord` and re-serializing it (`research_docs.rs`'s
/// `campaign_runs` — "serde round-trip == the stored bytes"), so the
/// byte-identical contract `campaign_runs_lists_and_dumps_the_bare_stored_record`
/// pins above is only proven for a record where `trace_name` is absent
/// (pre-0109 shape). This is the same round-trip pinned with the new field
/// POPULATED: a stored line carrying `"trace_name":"deadbeef-3"` must dump
/// back byte-identical — the new field's declaration position (after
/// `generalizations`) and its `skip_serializing_if` must not perturb the
/// existing sparse-skip / declaration-order contract the read-back verb
/// depends on.
#[test]
fn campaign_runs_dump_round_trips_a_populated_trace_name() {
let _fixture = project_lock();
let dir = built_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
std::fs::create_dir_all(&runs_dir).expect("create runs dir");
let _cleanup = ScratchGuard(vec![ScratchPath::Dir(runs_dir.clone())]);
let campaign_id = "deadbeef".repeat(8);
let record_line = format!(
r#"{{"campaign":"{campaign_id}","process":"beef","run":3,"seed":42,"cells":[{{"strategy":"3f9c","instrument":"GER40","window_ms":[1725148800000,1727740799999],"stages":[{{"block":"std::sweep","family_id":"sweep-0"}}]}}],"trace_name":"deadbeef-3"}}"#
);
std::fs::write(runs_dir.join("campaign_runs.jsonl"), format!("{record_line}\n"))
.expect("seed the campaign-run store");
let (dump_out, dump_code) = run_code_in(dir, &["campaign", "runs", &campaign_id]);
assert_eq!(dump_code, Some(0), "dump exits 0: {dump_out}");
assert_eq!(
dump_out.trim(),
record_line,
"a populated trace_name round-trips byte-identical through the read-back verb: {dump_out}"
);
}
/// The v2 boundary, order side: `[sweep, monte_carlo, walk_forward]` is
/// intrinsically valid — `process register` ACCEPTS it (asserted inside
/// `register_process_doc`; the intrinsic-tier ground is the shipped
@@ -1203,14 +1258,62 @@ fn campaign_run_refuses_zero_resamples_monte_carlo_before_any_member_runs() {
assert!(!out.contains("ZeroBootstrapParam"), "Debug leak: {out}");
}
/// 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.
/// 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_persist_taps_deferred_loudly() {
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");
@@ -1225,18 +1328,22 @@ fn campaign_run_persist_taps_deferred_loudly() {
write_doc(
dir,
"taps.campaign.json",
&campaign_doc_json(&bp_id, &proc_id, (1, 2), "\"r_record\"", ""),
&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("aura: persist_taps not yet honored (1 tap(s) ignored)"),
"the tap note must precede the member-data refusal: {out}"
!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}"
);
}
/// Gated real-data e2e (the `cli_run.rs` skip idiom): a full