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() {
|
||||
|
||||
@@ -198,8 +198,10 @@ pub struct GangMember {
|
||||
/// Deterministic shape gate for authored composite render names (#331) —
|
||||
/// shape only, never content judgement (C29 discipline): non-empty, no path
|
||||
/// separator (`/` or `\`), and not the special segments `.` or `..`. The rule
|
||||
/// exists because the name keys a trace directory on disk unsanitized
|
||||
/// (`traces/<name>/`, `crates/aura-registry/src/trace_store.rs`). Applied at
|
||||
/// exists because the name has an operational run-time role: it prefixes a
|
||||
/// run's identity-keyed trace directory on disk unsanitized
|
||||
/// (`traces/<name>-<id8>/`, #311; `crates/aura-registry/src/trace_store.rs`),
|
||||
/// not the whole directory component as before #311. Applied at
|
||||
/// both data-borne birth routes for a name (the skeptic's two-seam finding):
|
||||
/// the `Op::Name` op intake (`construction.rs`) and the CLI's
|
||||
/// blueprint-envelope root-name intake — never at store read-back (C29:
|
||||
|
||||
@@ -101,7 +101,33 @@ pub struct RunnerError {
|
||||
/// serde_json's map is sorted, so re-serialising a parsed value yields
|
||||
/// deterministic bytes — the same technique aura-bench uses for its record-line
|
||||
/// fingerprint (`crates/aura-bench/src/surfaces/fixed_cost.rs`).
|
||||
pub fn run_identity_digest(manifest: &aura_engine::RunManifest) -> String {
|
||||
///
|
||||
/// **`topology_hash` is not hashed as-is — `identity_hash` stands in for it
|
||||
/// (2026-07-27 audit fix, C29).** `manifest.topology_hash` is a **content** id
|
||||
/// (`content_id_of(blueprint_to_json(..))`, #158/C24): the canonical bytes it
|
||||
/// hashes include a blueprint's `doc` field — an authored one-line rationale,
|
||||
/// C29 — and every other C23 debug symbol (render name, instance names, bound
|
||||
/// param/role/output/tap/gang names). A description-only (or name-only) edit
|
||||
/// to an otherwise bit-identical blueprint therefore changes `topology_hash`,
|
||||
/// which would mint a fresh trace directory for what is, by C29's own Id
|
||||
/// treatment ("description fields … are blanked for the identity id"), the
|
||||
/// SAME run — a description influencing an identity id is exactly what C29
|
||||
/// forbids. The caller passes `identity_hash` — the #171
|
||||
/// `blueprint_identity_json` projection (debug-symbol-blind by construction)
|
||||
/// — and this function substitutes it for whatever `manifest.topology_hash`
|
||||
/// carries in the hashed value; `manifest.topology_hash` itself is untouched
|
||||
/// (it keeps its own #343 reference-semantics content id — the reproduction
|
||||
/// store still keys on it byte-exact). **The blanking is wholesale, not
|
||||
/// description-only:** `identity_hash` is blind to the render name and every
|
||||
/// node/role/output/tap/gang name too, because none of these carry run
|
||||
/// semantics (a bound param's *openness* stays identity-bearing) — two
|
||||
/// blueprints identical in every load-bearing respect but differing only in
|
||||
/// such names correctly compute the same `identity_hash`, one directory, not
|
||||
/// two. Both mint sites
|
||||
/// (`aura-runner::member::run_signal_r`, `aura-runner::measure::run_measurement`)
|
||||
/// compute `identity_hash` from the SAME blueprint reference they already hash
|
||||
/// for `topology_hash`, so the two can never drift.
|
||||
pub fn run_identity_digest(manifest: &aura_engine::RunManifest, identity_hash: &str) -> String {
|
||||
use aura_engine::Scalar;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
@@ -124,6 +150,10 @@ pub fn run_identity_digest(manifest: &aura_engine::RunManifest) -> String {
|
||||
"params".to_string(),
|
||||
serde_json::to_value(&merged).expect("the merged param vec serializes"),
|
||||
);
|
||||
obj.insert(
|
||||
"topology_hash".to_string(),
|
||||
serde_json::Value::String(identity_hash.to_string()),
|
||||
);
|
||||
}
|
||||
let canonical = serde_json::to_string(&v).expect("re-serializing a parsed value cannot fail");
|
||||
let hex = format!("{:x}", Sha256::digest(canonical.as_bytes()));
|
||||
@@ -135,6 +165,15 @@ mod tests {
|
||||
use super::run_identity_digest;
|
||||
use aura_engine::{ProjectProvenance, RunManifest, Scalar, Timestamp};
|
||||
|
||||
/// A fixed `identity_hash` stand-in used by every case below that does not
|
||||
/// itself vary the blueprint's identity projection — held equal across both
|
||||
/// calls being compared so the case tests exactly the field it names, not an
|
||||
/// incidental `identity_hash` difference. Shaped like a real SHA-256 hex
|
||||
/// digest (64 lowercase hex chars) but not a real hash of anything.
|
||||
fn identity_hash() -> String {
|
||||
"beef".repeat(16)
|
||||
}
|
||||
|
||||
/// A manifest with every field populated the way a real single run stamps
|
||||
/// them — the base each case below perturbs by exactly one field.
|
||||
fn manifest() -> RunManifest {
|
||||
@@ -158,7 +197,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn run_identity_digest_is_eight_lowercase_hex() {
|
||||
let d = run_identity_digest(&manifest());
|
||||
let d = run_identity_digest(&manifest(), &identity_hash());
|
||||
assert_eq!(d.len(), 8, "the handle suffix is 8 hex chars: {d}");
|
||||
assert!(
|
||||
d.bytes().all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()),
|
||||
@@ -172,8 +211,8 @@ mod tests {
|
||||
let mut b = manifest();
|
||||
b.commit = "0123456789abcdef".to_string();
|
||||
assert_eq!(
|
||||
run_identity_digest(&a),
|
||||
run_identity_digest(&b),
|
||||
run_identity_digest(&a, &identity_hash()),
|
||||
run_identity_digest(&b, &identity_hash()),
|
||||
"manifest.commit is the binary's build provenance, not the run's identity"
|
||||
);
|
||||
}
|
||||
@@ -185,8 +224,8 @@ mod tests {
|
||||
b.project.as_mut().expect("base manifest carries provenance").commit =
|
||||
Some("deadbeef-dirty".to_string());
|
||||
assert_eq!(
|
||||
run_identity_digest(&a),
|
||||
run_identity_digest(&b),
|
||||
run_identity_digest(&a, &identity_hash()),
|
||||
run_identity_digest(&b, &identity_hash()),
|
||||
"project.commit is re-derived per invocation; editing the worktree is not a new run"
|
||||
);
|
||||
}
|
||||
@@ -200,8 +239,8 @@ mod tests {
|
||||
let mut b = manifest();
|
||||
b.project.as_mut().expect("base manifest carries provenance").commit = None;
|
||||
assert_eq!(
|
||||
run_identity_digest(&a),
|
||||
run_identity_digest(&b),
|
||||
run_identity_digest(&a, &identity_hash()),
|
||||
run_identity_digest(&b, &identity_hash()),
|
||||
"a derivable HEAD and an underivable one are the same run"
|
||||
);
|
||||
}
|
||||
@@ -212,8 +251,8 @@ mod tests {
|
||||
let mut b = manifest();
|
||||
b.params = vec![("fast.length".to_string(), Scalar::I64(3))];
|
||||
assert_ne!(
|
||||
run_identity_digest(&a),
|
||||
run_identity_digest(&b),
|
||||
run_identity_digest(&a, &identity_hash()),
|
||||
run_identity_digest(&b, &identity_hash()),
|
||||
"params are identity-bearing — this is the headline case"
|
||||
);
|
||||
}
|
||||
@@ -233,8 +272,8 @@ mod tests {
|
||||
("bias.scale".to_string(), Scalar::F64(0.5)),
|
||||
];
|
||||
assert_eq!(
|
||||
run_identity_digest(&a),
|
||||
run_identity_digest(&b),
|
||||
run_identity_digest(&a, &identity_hash()),
|
||||
run_identity_digest(&b, &identity_hash()),
|
||||
"same effective params, different params/defaults partition — same run"
|
||||
);
|
||||
}
|
||||
@@ -248,17 +287,40 @@ mod tests {
|
||||
b.project.as_mut().expect("base manifest carries provenance").dylib_sha256 =
|
||||
Some("ff00".to_string());
|
||||
assert_ne!(
|
||||
run_identity_digest(&a),
|
||||
run_identity_digest(&b),
|
||||
run_identity_digest(&a, &identity_hash()),
|
||||
run_identity_digest(&b, &identity_hash()),
|
||||
"dylib_sha256 is exactly what C13 hot-reload varies"
|
||||
);
|
||||
}
|
||||
|
||||
/// C29 audit fix (2026-07-27): `topology_hash` is a CONTENT id — it hashes
|
||||
/// a blueprint's `doc` field (and every other C23 debug symbol) along with
|
||||
/// its structure — so a description-only edit changes `manifest.
|
||||
/// topology_hash` even though the run is otherwise bit-identical. The
|
||||
/// digest must not tell these two manifests apart: it hashes the caller's
|
||||
/// `identity_hash`, not `manifest.topology_hash`, so two manifests whose
|
||||
/// `topology_hash` fields differ (as a real doc-only edit would produce)
|
||||
/// digest the same as long as the SAME `identity_hash` is supplied — which
|
||||
/// is exactly what `blueprint_identity_json` (doc-blind) computes for both.
|
||||
#[test]
|
||||
fn run_identity_digest_ignores_topology_hash_content_id_when_identity_hash_agrees() {
|
||||
let a = manifest(); // topology_hash: "0f1e2d3c"
|
||||
let mut b = manifest();
|
||||
b.topology_hash = Some("ffffffff".to_string()); // as if a doc-only edit changed the content id
|
||||
assert_eq!(
|
||||
run_identity_digest(&a, &identity_hash()),
|
||||
run_identity_digest(&b, &identity_hash()),
|
||||
"a description-only edit changes topology_hash (a content id, C29) but not \
|
||||
the blueprint's identity projection — the digest must not tell these apart"
|
||||
);
|
||||
}
|
||||
|
||||
/// The absolute property every other case in this module only tests
|
||||
/// relationally: ONE fully-populated manifest (every field set, including
|
||||
/// a `project` with `namespace`, `dylib_sha256` AND `commit` all present)
|
||||
/// yields this ONE specific 8-hex value — stably, across builds and
|
||||
/// processes, since the whole trace store's addressing rests on it.
|
||||
/// plus one fixed `identity_hash` yields this ONE specific 8-hex value —
|
||||
/// stably, across builds and processes, since the whole trace store's
|
||||
/// addressing rests on it.
|
||||
///
|
||||
/// If this literal ever needs to change, that change is NOT a casual
|
||||
/// update: it means `run_identity_digest`'s canonicalisation shifted
|
||||
@@ -285,8 +347,8 @@ mod tests {
|
||||
}),
|
||||
};
|
||||
assert_eq!(
|
||||
run_identity_digest(&m),
|
||||
"3d080714",
|
||||
run_identity_digest(&m, &"c0ffee00".repeat(8)),
|
||||
"8cfcc5fc",
|
||||
"the digest for this fixed manifest must be stable across builds and processes"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,10 +90,15 @@ pub fn run_measurement(
|
||||
// topology_hash's own two-line body, inlined (mirrors member::run_signal_r):
|
||||
// `content_id_of` over the canonical (#164) blueprint JSON — the CLI shell's
|
||||
// `topology_hash` helper is the same primitive, kept single-sourced at
|
||||
// `aura_research`.
|
||||
// `aura_research`. `identity_hash` is the sibling #171 projection over the
|
||||
// SAME `signal`, feeding `run_identity_digest` in place of `topology_hash`
|
||||
// (2026-07-27, C29) — see that function's doc comment.
|
||||
let topo = aura_research::content_id_of(
|
||||
&aura_engine::blueprint_to_json(&signal).expect("a buildable signal serializes"),
|
||||
); // before signal is consumed
|
||||
let identity_hash = aura_research::content_id_of(
|
||||
&aura_engine::blueprint_identity_json(&signal).expect("a buildable signal serializes"),
|
||||
); // before signal is consumed
|
||||
// #311 (mirrors run_signal_r): the render name is the readable prefix; the
|
||||
// directory is minted from the manifest below.
|
||||
let render_name = signal.name().to_string();
|
||||
@@ -131,7 +136,8 @@ pub fn run_measurement(
|
||||
manifest.defaults = defaults;
|
||||
manifest.topology_hash = Some(topo);
|
||||
manifest.project = env.provenance();
|
||||
let run_name = format!("{}-{}", render_name, crate::run_identity_digest(&manifest));
|
||||
let run_name =
|
||||
format!("{}-{}", render_name, crate::run_identity_digest(&manifest, &identity_hash));
|
||||
|
||||
// Bind each declared tap per the plan's subscription (mirrors
|
||||
// run_signal_r — the shared bind_tap_plan/BoundTaps pair IS the mirror).
|
||||
|
||||
@@ -540,19 +540,24 @@ fn compile_error_prose(e: &CompileError, names: &[String], params: &[Scalar]) ->
|
||||
/// The single construction+run path shared by the `aura run <blueprint.json>` CLI
|
||||
/// arm and its bit-identical test.
|
||||
///
|
||||
/// `topo`: `None` computes `topology_hash` inline from `signal` as always
|
||||
/// (every existing caller); `Some(hash)` overrides it with the caller's own
|
||||
/// **reference-semantics** hash (#343, revised) — the exec blueprint leg's
|
||||
/// `--override` branch passes the loaded base document's content id, computed
|
||||
/// BEFORE `reopen_all`, so the manifest reaching every consumer inside this
|
||||
/// function (the record line AND the trace-store persistence below) carries
|
||||
/// the base document's id, never the reopened topology's own, exactly as the
|
||||
/// campaign leg's `run_blueprint_member` has always taken its `topo` as a
|
||||
/// caller-supplied reference (`runner.rs`'s `&cell.strategy_id`).
|
||||
/// `refs`: `None` computes both `topology_hash` (the content id) and the run
|
||||
/// identity's `identity_hash` (the #171 `blueprint_identity_json` projection —
|
||||
/// debug-symbol-blind, including to a C29 description) inline from `signal`,
|
||||
/// as always (every existing caller). `Some((topo, identity))` overrides BOTH
|
||||
/// with the caller's own **reference-semantics** hashes (#343, revised): the
|
||||
/// exec blueprint leg's `--override` branch computes them from the loaded base
|
||||
/// document BEFORE `reopen_all`, so every consumer inside this function (the
|
||||
/// record line, the trace-store persistence below, AND the #311 handle) sees
|
||||
/// the base document's hashes, never the reopened topology's own — computing
|
||||
/// `identity_hash` from the REOPENED signal instead would show the overridden
|
||||
/// param as open rather than bound, which would split a no-op `--override`
|
||||
/// (one that reopens a param only to rebind it to its own default) from the
|
||||
/// un-overridden run it is identical to. Mirrors the campaign leg's own
|
||||
/// precedent for `topo` alone (`runner.rs`'s `&cell.strategy_id`).
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn run_signal_r(
|
||||
signal: Composite, params: &[Scalar], data: RunData, seed: u64, env: &Env,
|
||||
plan: TapPlan, topo: Option<&str>,
|
||||
plan: TapPlan, refs: Option<(&str, &str)>,
|
||||
) -> Result<crate::RunOutcome<RunReport>, RunnerError> {
|
||||
// #297 fieldtest finding B1 (milestone-safe-to-embed): mirror the CLI's
|
||||
// own `exec_blueprint_leg` exposes-neither guard (`aura-cli/src/main.rs`,
|
||||
@@ -574,13 +579,20 @@ pub fn run_signal_r(
|
||||
// topology_hash's own two-line body, inlined: `content_id_of` over the
|
||||
// canonical (#164) blueprint JSON — the CLI shell's `topology_hash`
|
||||
// helper is the same primitive, kept single-sourced at `aura_research`.
|
||||
// Skipped when the caller already supplies a reference-semantics hash
|
||||
// (`topo: Some(..)`) — no need to hash a signal whose own topology_hash
|
||||
// will be overridden anyway.
|
||||
let topo: String = match topo {
|
||||
Some(t) => t.to_string(),
|
||||
None => aura_research::content_id_of(
|
||||
&aura_engine::blueprint_to_json(&signal).expect("a buildable signal serializes"),
|
||||
// `identity_hash` is the sibling #171 projection, over the SAME `signal`
|
||||
// reference, feeding `run_identity_digest` in place of `topology_hash`
|
||||
// (2026-07-27, C29). Both skipped when the caller already supplies
|
||||
// reference-semantics hashes (`refs: Some(..)`) — no need to hash a
|
||||
// signal whose own topology_hash/identity_hash will be overridden anyway.
|
||||
let (topo, identity_hash): (String, String) = match refs {
|
||||
Some((t, i)) => (t.to_string(), i.to_string()),
|
||||
None => (
|
||||
aura_research::content_id_of(
|
||||
&aura_engine::blueprint_to_json(&signal).expect("a buildable signal serializes"),
|
||||
),
|
||||
aura_research::content_id_of(
|
||||
&aura_engine::blueprint_identity_json(&signal).expect("a buildable signal serializes"),
|
||||
),
|
||||
), // before signal is consumed
|
||||
};
|
||||
// #311: the render name is only the readable PREFIX of the run's directory
|
||||
@@ -632,7 +644,8 @@ pub fn run_signal_r(
|
||||
manifest.broker = r_sma_broker_label(pip_size);
|
||||
manifest.topology_hash = Some(topo);
|
||||
manifest.project = env.provenance();
|
||||
let run_name = format!("{}-{}", render_name, crate::run_identity_digest(&manifest));
|
||||
let run_name =
|
||||
format!("{}-{}", render_name, crate::run_identity_digest(&manifest, &identity_hash));
|
||||
// Bind each declared tap per the plan's subscription, before bootstrap
|
||||
// (#283): typed refusals for bad plans, record consumers hold their
|
||||
// streaming writer in-graph, folds accumulate O(1), live closures run
|
||||
|
||||
@@ -257,10 +257,14 @@ overwrote an earlier one's trace — is **resolved** (#311), and no longer
|
||||
depends on naming at all: a run's trace directory is keyed by the run's own
|
||||
identity (`traces/<name>-<id8>/`, a digest over its manifest), so two runs
|
||||
that differ in any identity-bearing input — a different topology, an
|
||||
`--override` value, the data window, the seed — land in two directories,
|
||||
while two runs that differ in nothing land in one and the later simply
|
||||
rewrites what the earlier wrote. The render name survives as the directory's
|
||||
readable prefix, and `aura chart <name>` lists the runs recorded under it.
|
||||
`--override` value, the data window, the seed — land in two directories, and
|
||||
two runs that differ in nothing land in the same one. That directory's
|
||||
*contents* are not wholesale-replaced on the second write, though: the
|
||||
declared tap *plan* (which of a blueprint's taps this invocation subscribes)
|
||||
is not itself identity-bearing, so a same-identity pair that persists a
|
||||
different tap subset can strand an earlier tap file beside the later run's
|
||||
own — tracked as #352. The render name survives as the directory's readable
|
||||
prefix, and `aura chart <name>` lists the runs recorded under it.
|
||||
|
||||
Value forms are the typed-tag representations used everywhere in this
|
||||
family of artifacts:
|
||||
|
||||
@@ -295,3 +295,10 @@ aggregate** over those members, not a persisted family-level record."
|
||||
#319 sugar retirement, 2026-07-25):** "`aura campaign run <file|content-id>`
|
||||
executes a campaign (a file is register-then-run sugar; the content id is
|
||||
canonical): a zero-fault referential gate, then the process pipeline."
|
||||
|
||||
**Current-state "Single-run trace identity (#311)" clause (superseded
|
||||
2026-07-27 — the audit that closed the #311 cycle found `topology_hash`
|
||||
itself, a C24 content id, feeding the digest, in violation of C29's Id
|
||||
treatment):** "What remains — the merged params, window, seed, broker,
|
||||
instrument, `topology_hash`, and the project's `namespace`/`dylib_sha256` (the
|
||||
C13 hot-reload discriminator) — is identity-bearing."
|
||||
|
||||
@@ -263,9 +263,18 @@ partition between them records *how* a bound value was supplied (varied vs. held
|
||||
at its default, #246) rather than *what* the run's effective parameterisation is —
|
||||
the same provenance-not-identity distinction, applied to a different pair of keys;
|
||||
the merge is lossless since the two vectors are disjoint by construction. What
|
||||
remains — the merged params, window, seed, broker, instrument, `topology_hash`,
|
||||
and the project's `namespace`/`dylib_sha256` (the C13 hot-reload discriminator) —
|
||||
is identity-bearing. The two provenance keys are removed rather than blanked
|
||||
remains — the merged params, window, seed, broker, instrument, and the
|
||||
project's `namespace`/`dylib_sha256` (the C13 hot-reload discriminator) — is
|
||||
identity-bearing, with one substitution (2026-07-27, C29): `topology_hash` is
|
||||
a **content** id (`content_id_of(blueprint_to_json(..))`, #158/C24) that also
|
||||
hashes a blueprint's `doc` field and every other C23 debug symbol, so a
|
||||
description-only edit would otherwise mint a fresh directory for a
|
||||
bit-identical run — forbidden by C29's own Id treatment ("description fields
|
||||
… are blanked for the identity id"). The digest hashes
|
||||
`blueprint_identity_json`'s debug-symbol-blind projection (#171) in
|
||||
`topology_hash`'s place instead; `manifest.topology_hash` itself is
|
||||
untouched, keeping its own #343 reference-semantics content id. The two
|
||||
provenance keys are removed rather than blanked
|
||||
because `ProjectProvenance.commit` skips serializing when it is `None`: blanking
|
||||
would canonicalise an absent key to `null` and split one identity in two.
|
||||
`aura-runner::run_identity_digest` is the single implementation; both single-run
|
||||
|
||||
@@ -146,6 +146,12 @@ run's own name, on both shapes: …"
|
||||
"file I/O is `aura-registry::TraceStore`, persisting each run under
|
||||
`runs/traces/<name>/` beside the run registry's `runs.jsonl`"
|
||||
|
||||
**Current-state "Single-run trace identity (#311)" clause (superseded
|
||||
2026-07-27 — the audit that closed the #311 cycle found `topology_hash`
|
||||
itself, a C24 content id, feeding the digest, in violation of C29's Id
|
||||
treatment):** "Everything else the manifest carries is identity-bearing,
|
||||
`project.dylib_sha256` included (it is what C13 hot-reload varies)."
|
||||
|
||||
**Current-state "Single run" + "Families" + "Newcomer" clauses (superseded by
|
||||
the #319 sugar retirement, 2026-07-25):** "**Single run.** `aura run
|
||||
<blueprint.json>` persists every tap the blueprint declares to the trace
|
||||
|
||||
@@ -92,7 +92,22 @@ different pair: `params`/`defaults` record *how* a bound value was supplied
|
||||
(varied vs. held at its default, #246), not *what* the run's effective
|
||||
parameterisation is, and the two vectors are disjoint by construction, so the
|
||||
merge is lossless. Everything else the manifest carries is identity-bearing,
|
||||
`project.dylib_sha256` included (it is what C13 hot-reload varies). The
|
||||
`project.dylib_sha256` included (it is what C13 hot-reload varies) —
|
||||
**except** `topology_hash` (2026-07-27, C29): it is a **content** id
|
||||
(`content_id_of(blueprint_to_json(..))`, #158/C24) that also hashes a
|
||||
blueprint's `doc` field and every other C23 debug symbol, so a
|
||||
description-only edit would otherwise mint a fresh directory for a
|
||||
bit-identical run, which C29's own Id treatment forbids. The digest hashes
|
||||
`blueprint_identity_json`'s debug-symbol-blind projection (#171) in
|
||||
`topology_hash`'s place instead; `manifest.topology_hash` itself is
|
||||
untouched. That projection blanks every C23 debug symbol, not descriptions
|
||||
alone — the render name, and node, role, output, tap and gang names — while
|
||||
param openness stays identity-bearing; two blueprints differing only in
|
||||
such names compute bit-identically, so one address is the correct answer.
|
||||
One observable consequence: two structurally identical blueprints whose
|
||||
declared *tap names* differ share a directory while writing differently
|
||||
named tap files, and the later run's `index.json` lists only its own, since
|
||||
the write path never prunes (tracked as #352, unchanged here). The
|
||||
manifest is assembled *before* the tap bind and the same value is reused for
|
||||
the record, so the handle can never name a directory whose identity the
|
||||
stored record does not describe. Consequences: two runs of one blueprint
|
||||
|
||||
@@ -60,6 +60,11 @@ content id and are blanked for the identity id, exactly as `Composite.doc`
|
||||
([C24](c24-blueprint-data.md)), so node docs move no id. Documents have no
|
||||
identity projection — their pair is: absent `description` is byte-identical
|
||||
to the field-less form (existing content ids stable), present participates.
|
||||
A single `aura exec` run's #311 trace-directory handle honours this same
|
||||
treatment (2026-07-27 audit fix): its digest hashes the blueprint's identity
|
||||
projection, not `manifest.topology_hash` (a content id), so a
|
||||
description-only blueprint edit mints no fresh directory for a bit-identical
|
||||
run (`aura-runner::run_identity_digest`, [C18](c18-registry.md)).
|
||||
|
||||
**Forbids.** The engine evaluating description text — the gate is
|
||||
deterministic string shape, never content judgement (no freetext logic
|
||||
|
||||
Reference in New Issue
Block a user