audit: trace-identity cycle close — C29 honoured, code twins back in lockstep
The drift review found the run handle keying on the blueprint's *content* id, which hashes `doc` fields and every other C23 debug symbol. C29's Id treatment forbids a description influencing an identity, and the repository already carried the right projection unused. The digest now substitutes `blueprint_identity_json`'s debug-symbol-blind projection (#171) for `topology_hash` in the hashed value. `manifest.topology_hash` itself is untouched, so #343's reference semantics and the committed record-line fingerprint both stand. That projection blanks more than descriptions — the render name and node, role, output, tap and gang names — while param openness stays identity-bearing. This is correct rather than incidental: names are non-load-bearing debug symbols (C8/C23), so blueprints differing only in them compute bit-identically and one address is the right answer. The reach is now stated in C22 and in the digest's own doc, together with the one observable consequence: two structurally identical blueprints whose declared tap names differ share a directory while writing differently named tap files, since the write path never prunes (#352). Two code twins the cycle left stale are corrected. `name_gate`'s doc comment still carried the C23 clause this cycle superseded and moved to its sidecar, and the authoring guide claimed a same-identity re-run replaces the directory's contents — it does not, for the same non-pruning reason. Bench is report-only and all five fingerprints are unchanged. `campaign_sweep` reports a peak-RSS NOTICE (+12.8%); the campaign leg is untouched by this cycle and its fingerprint is stable, so nothing is ratified against it here. Routed rather than fixed: #353 — naming authority for one flat namespace is spelled in three crates and the single-run leg has no claim path — filed into the recorded-stream-store milestone, where the container consolidates anyway; a data-identity seam recorded on #320 (a run's identity carries the window, never the data's content, so a corrected archive is a silent overwrite until that store supplies a recording identity); and a second instance of the non-pruning class on #352. The cycle's spec and plan are removed — git-ignored working files, read by the drift review before deletion. refs #311
This commit is contained in:
@@ -19,7 +19,7 @@ use aura_core::Cell;
|
||||
#[cfg(test)]
|
||||
use aura_composites::StopRule;
|
||||
use aura_engine::{
|
||||
blueprint_from_json, blueprint_to_json, f64_field, join_on_ts,
|
||||
blueprint_from_json, blueprint_identity_json, blueprint_to_json, f64_field, join_on_ts,
|
||||
JoinedRow,
|
||||
SelectionMode,
|
||||
};
|
||||
@@ -1447,13 +1447,21 @@ fn exec_blueprint_leg(path: &str, overrides: &[String], tap: &[String], env: &au
|
||||
// `&cell.strategy_id`, the stored base document's id, as `topo` even
|
||||
// for a reopened member). Computed here, over the still-unreopened
|
||||
// `signal`, before `reopen_all` consumes it, and threaded into
|
||||
// `run_signal_r`'s own `topo` parameter so every consumer inside that
|
||||
// `run_signal_r`'s own `refs` parameter so every consumer inside that
|
||||
// function — the record line AND the trace-store persistence
|
||||
// (`bound.finish`, `aura-registry::trace_store`) — sees the base
|
||||
// document's hash from the start; no post-hoc manifest mutation
|
||||
// after the run, which would miss the already-persisted `index.json`.
|
||||
// `base_identity` is the sibling #171 identity projection, computed
|
||||
// over the SAME un-reopened `signal` for the same reason (2026-07-27,
|
||||
// C29): the reopened variant would show the overridden param as OPEN
|
||||
// rather than bound, which would split a no-op `--override` from the
|
||||
// un-overridden run it is identical to (`run_identity_digest`'s doc
|
||||
// comment).
|
||||
let base_topo =
|
||||
content_id(&blueprint_to_json(&signal).expect("a buildable signal serializes"));
|
||||
let base_identity =
|
||||
content_id(&blueprint_identity_json(&signal).expect("a buildable signal serializes"));
|
||||
let signal = reopen_all(signal, &paths);
|
||||
// An out-of-domain override value fails a node's own domain check at
|
||||
// bootstrap (e.g. `Sma::new`'s `assert!`, or a negative length
|
||||
@@ -1466,7 +1474,10 @@ fn exec_blueprint_leg(path: &str, overrides: &[String], tap: &[String], env: &au
|
||||
// refusal (exit 1, C14 partition) instead of an uncaught exit 101 —
|
||||
// the input was well-formed argv; the value is what the node refuses.
|
||||
let outcome = match aura_campaign::catch_member_panic(|| {
|
||||
run_signal_r(signal, ¶ms, RunData::Synthetic, 0, env, tap_plan, Some(&base_topo))
|
||||
run_signal_r(
|
||||
signal, ¶ms, RunData::Synthetic, 0, env, tap_plan,
|
||||
Some((&base_topo, &base_identity)),
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|msg| {
|
||||
eprintln!("aura: {}", panic_refusal_prose(&msg));
|
||||
|
||||
@@ -721,6 +721,69 @@ same effective params must mint the same handle: base {base_out} / overridden {o
|
||||
);
|
||||
}
|
||||
|
||||
/// C29 audit fix (2026-07-27): a blueprint's composite-level `doc` field is a
|
||||
/// CONTENT-id key (`blueprint_to_json` serializes it, so it changes
|
||||
/// `topology_hash`) but an identity-BLIND debug symbol
|
||||
/// (`blueprint_identity_json`/`strip_debug_symbols` blanks it) — C29's own Id
|
||||
/// treatment: "description fields … are blanked for the identity id". Two
|
||||
/// blueprints differing ONLY in that field are, by that rule, the SAME run and
|
||||
/// must land in the SAME trace directory, even though their `topology_hash`es
|
||||
/// genuinely differ (proving the two documents are not byte-identical, so this
|
||||
/// is not a vacuous pass).
|
||||
#[test]
|
||||
fn exec_blueprint_description_only_edit_lands_in_the_same_trace_directory() {
|
||||
let cwd = temp_cwd("doc-only-same-dir");
|
||||
let bare_path = cwd.join("bare.json");
|
||||
std::fs::write(&bare_path, tap_blueprint_json()).expect("write bare blueprint");
|
||||
let mut with_doc: serde_json::Value =
|
||||
serde_json::from_str(&tap_blueprint_json()).expect("parse tapped blueprint");
|
||||
with_doc["blueprint"]["doc"] = serde_json::json!("an authored one-line rationale");
|
||||
let doc_path = cwd.join("with_doc.json");
|
||||
std::fs::write(&doc_path, serde_json::to_string(&with_doc).expect("re-serialize doc'd blueprint"))
|
||||
.expect("write doc'd blueprint");
|
||||
|
||||
let (base_out, base_code) =
|
||||
run_code_in(&cwd, &["exec", bare_path.to_str().expect("utf-8 path")]);
|
||||
assert_eq!(base_code, Some(0), "stdout/stderr: {base_out}");
|
||||
let base_line = base_out.lines().find(|l| l.starts_with('{')).expect("record line");
|
||||
let base_v: serde_json::Value = serde_json::from_str(base_line).expect("record parses");
|
||||
let base_handle =
|
||||
base_v["trace_name"].as_str().expect("a recording run prints its trace_name").to_string();
|
||||
let base_topo =
|
||||
base_v["manifest"]["topology_hash"].as_str().expect("base topology_hash").to_string();
|
||||
|
||||
let (doc_out, doc_code) =
|
||||
run_code_in(&cwd, &["exec", doc_path.to_str().expect("utf-8 path")]);
|
||||
assert_eq!(doc_code, Some(0), "stdout/stderr: {doc_out}");
|
||||
let doc_line = doc_out.lines().find(|l| l.starts_with('{')).expect("record line");
|
||||
let doc_v: serde_json::Value = serde_json::from_str(doc_line).expect("record parses");
|
||||
let doc_handle =
|
||||
doc_v["trace_name"].as_str().expect("a recording run prints its trace_name").to_string();
|
||||
let doc_topo =
|
||||
doc_v["manifest"]["topology_hash"].as_str().expect("doc'd topology_hash").to_string();
|
||||
|
||||
assert_ne!(
|
||||
base_topo, doc_topo,
|
||||
"a doc edit DOES change the content id (C24/C29) — the two documents are not \
|
||||
byte-identical: {base_out} / {doc_out}"
|
||||
);
|
||||
assert_eq!(
|
||||
base_handle, doc_handle,
|
||||
"a description-only edit must not mint a new trace directory (C29's Id treatment): \
|
||||
base {base_out} / with-doc {doc_out}"
|
||||
);
|
||||
|
||||
let entries: Vec<_> = std::fs::read_dir(cwd.join("runs/traces"))
|
||||
.expect("read traces dir")
|
||||
.map(|e| e.expect("dir entry").file_name())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
entries.len(),
|
||||
1,
|
||||
"one identity, one directory — found {entries:?} under runs/traces"
|
||||
);
|
||||
}
|
||||
|
||||
/// A malformed override token (no `NODE.PARAM=VALUE` shape) refuses as usage.
|
||||
#[test]
|
||||
fn exec_override_malformed_token_refuses_with_usage() {
|
||||
|
||||
Reference in New Issue
Block a user