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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user