feat(registry): index-first identity resolution with self-repairing sidecar

Replace the per-call O(store) scan in find_blueprint_by_identity with a
persistent identity-id -> content-id sidecar index
(blueprint_identity_index.jsonl, sibling of runs.jsonl). Lookups consult
the index first and VERIFY every hit by loading that one blueprint under
the current roster and recomputing its identity id -- the index is a
cache, never an oracle, so results stay scan-identical under roster
drift, store surgery, or index corruption (up to the same-identity-twin
choice, unspecified in both paths and documented on the fn). Any miss or
failed verification runs the old scan as a full-store repair pass: no
early return, best-effort append of every absent-or-different mapping,
last line wins so later repairs heal stale lines. No write-path, engine,
or caller changes; a pre-index store backfills itself on the first miss;
a read-only store keeps scanning as before.

Decided against put-time index maintenance: it would need a roster-free
doc-level identity function whose equivalence to the loaded-composite
path no green test ratifies (the composite path canonicalizes
structurally on load); the repair path is the load-bearing mechanism
either way. Decision log: the issue's comments.

Verification: 7 new aura-registry tests (backfill, verified-hit-no-walk
via a poison-dir probe, one-pass legacy backfill, stale-line healing,
unloadable-under-roster preservation, garbage tolerance) plus a CLI e2e
(campaign validate resolving an identity ref cold then warm); workspace
suite 1346 passed / 0 failed; clippy -D warnings clean; doc build clean.

closes #191
This commit is contained in:
2026-07-17 14:13:48 +02:00
parent bfb8648925
commit 88a1c28954
2 changed files with 454 additions and 6 deletions
+158
View File
@@ -416,6 +416,164 @@ fn campaign_validate_in_project_reports_referential_tier_end_to_end() {
// drop, at the end of this scope — including on a mid-test panic.
}
/// Property (#191, index-first identity lookup at the CLI seam): a strategy
/// referenced by `identity_id` (never previously exercised end to end — every
/// prior campaign e2e fixture used `content_id`) resolves through `campaign
/// validate`'s referential tier exactly like a `content_id` ref. The FIRST
/// resolution has no sidecar index yet, so `Registry::find_blueprint_by_identity`
/// falls back to the store scan and backfills `blueprint_identity_index.jsonl`
/// as a repair pass; the SECOND resolution goes through that now-present index
/// (a verified hit). Both calls must answer identically — this is the E2E
/// manifestation of "index-first lookup with verify-on-hit and repair-walk":
/// if the backfilled index or its verify-on-hit read disagreed with the scan,
/// only the second call would regress.
#[test]
fn campaign_validate_resolves_identity_ref_via_index_first_lookup_then_index_hit() {
let (dir, _fixture) = fresh_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("seed.process.json")),
ScratchPath::File(dir.join("identity-ok.campaign.json")),
]);
// Seed one real, content-addressed blueprint (mirrors the content_id
// sibling test above).
let closed_bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let (sweep_out, sweep_code) = run_code_in(
&dir,
&[
"sweep",
&closed_bp,
"--axis",
"sma_signal.fast.length=2,4",
"--axis",
"sma_signal.slow.length=8,16",
"--name",
"campaign-identity-seed",
],
);
assert_eq!(sweep_code, Some(0), "seed sweep failed: {sweep_out}");
let bp_id = std::fs::read_dir(runs_dir.join("blueprints"))
.expect("blueprints dir")
.next()
.expect("one stored blueprint")
.expect("dir entry")
.path()
.file_stem()
.expect("stem")
.to_string_lossy()
.into_owned();
let bp_path = runs_dir.join("blueprints").join(format!("{bp_id}.json"));
// The identity id of the SAME stored bytes — `--content-id FILE
// --identity-id` combined, over the store's own file so the computed
// identity id is guaranteed to match what the registry recomputes on a
// lookup (no risk of drift from re-deriving it off a differently-bound
// copy of the blueprint).
let (id_out, id_code) =
run_code_in(&dir, &["graph", "introspect", "--content-id", bp_path.to_str().expect("utf8 path"), "--identity-id"]);
assert_eq!(id_code, Some(0), "stdout/stderr: {id_out}");
let mut id_lines = id_out.lines();
let printed_content_id = id_lines.next().expect("content id line");
assert_eq!(printed_content_id, bp_id, "content id of the stored file matches its store key");
let identity_id = id_lines.next().expect("identity id line").to_string();
let exec_process = PROCESS_DOC
.replacen(
"\"select\": \"plateau:worst\", \"deflate\": true",
"\"select\": \"argmax\", \"deflate\": false",
1,
)
.replacen("\"overfit_probability\", \"cmp\": \"lt\", \"value\": 0.1", "\"win_rate\", \"cmp\": \"lt\", \"value\": 1.1", 1);
write_doc(&dir, "seed.process.json", &exec_process);
let (proc_out, proc_code) = run_code_in(&dir, &["process", "register", "seed.process.json"]);
assert_eq!(proc_code, Some(0), "seed process register failed: {proc_out}");
let proc_id = proc_out
.lines()
.find(|l| l.starts_with("registered process "))
.expect("register line")
.trim_start_matches("registered process ")
.split(' ')
.next()
.expect("id")
.to_string();
let campaign = format!(
r#"{{
"format_version": 1,
"kind": "campaign",
"name": "identity-ref-check",
"data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": 1, "to_ms": 2 }} ] }},
"strategies": [ {{ "ref": {{ "identity_id": "{identity}" }},
"axes": {{ "fast.length": {{ "kind": "I64", "values": [2] }},
"slow.length": {{ "kind": "I64", "values": [6] }} }} }} ],
"process": {{ "ref": {{ "content_id": "{proc}" }} }},
"seed": 1,
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
}}"#,
identity = identity_id,
proc = proc_id
);
write_doc(&dir, "identity-ok.campaign.json", &campaign);
let sidecar = runs_dir.join("blueprint_identity_index.jsonl");
assert!(!sidecar.exists(), "no index sidecar before the first identity lookup");
// First call: no index -> scan fallback -> repair-walk backfills the sidecar.
let (out1, code1) = run_code_in(&dir, &["campaign", "validate", "identity-ok.campaign.json"]);
assert_eq!(code1, Some(0), "stdout/stderr: {out1}");
assert!(out1.contains(
"campaign document valid (referential): all references resolve, axes are in the param space"
));
assert!(sidecar.is_file(), "the scan fallback must backfill the sidecar index");
// Second call: the sidecar now exists -> a verified index hit. Same answer.
let (out2, code2) = run_code_in(&dir, &["campaign", "validate", "identity-ok.campaign.json"]);
assert_eq!(code2, Some(0), "stdout/stderr: {out2}");
assert_eq!(out1, out2, "index-hit resolution must answer identically to the scan fallback");
}
/// Property (#191, negative twin): an `identity_id` ref that matches no
/// stored blueprint is reported through the SAME `ref_fault_prose`
/// `IdentityUnmatched` mapping as `content_id`'s `StrategyNotFound` — pinned
/// as a unit-tested Debug-free string (`ref_fault_prose_is_debug_free`) but
/// never before driven through the actual `campaign validate` CLI seam,
/// so a wiring break between the fault type and its CLI dispatch would not
/// have failed any existing test.
#[test]
fn campaign_validate_reports_unresolved_identity_ref_end_to_end() {
// Referential tier only runs inside a project (`campaign_validate_outside_project_skips_referential_tier`);
// a plain `temp_cwd` would silently pass at the intrinsic tier instead.
let (dir, _fixture) = fresh_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
ScratchPath::Dir(runs_dir),
ScratchPath::File(dir.join("identity-miss.campaign.json")),
]);
let campaign = r#"{
"format_version": 1,
"kind": "campaign",
"name": "identity-ref-miss",
"data": { "instruments": ["GER40"], "windows": [ { "from_ms": 1, "to_ms": 2 } ] },
"strategies": [ { "ref": { "identity_id": "0000000000000000000000000000000000000000000000000000000000000000" },
"axes": { "fast": { "kind": "I64", "values": [8] } } } ],
"process": { "ref": { "content_id": "4e2d" } },
"seed": 1,
"presentation": { "persist_taps": [], "emit": ["family_table"] }
}"#;
write_doc(&dir, "identity-miss.campaign.json", campaign);
let (out, code) = run_code_in(&dir, &["campaign", "validate", "identity-miss.campaign.json"]);
assert_eq!(code, Some(1), "stdout/stderr: {out}");
assert!(out.contains("campaign references do not resolve:"), "stdout/stderr: {out}");
assert!(
out.contains("matches no stored blueprint"),
"the IdentityUnmatched fault prose must appear: {out}"
);
}
const CAMPAIGN_DOC: &str = r#"{
"format_version": 1,
"kind": "campaign",
+296 -6
View File
@@ -49,8 +49,10 @@ pub struct Registry {
/// read-the-counter-then-write critical section (`append_family`,
/// `append_campaign_run` in `lineage.rs`), so concurrent writers through
/// one handle also see a consistent per-name run counter. Store-agnostic:
/// it guards all three JSONL append paths — the flat runs store
/// ([`Registry::append`]), `families.jsonl`, and `campaign_runs.jsonl`
/// it guards all four JSONL append paths — the flat runs store
/// ([`Registry::append`]), `families.jsonl`, `campaign_runs.jsonl`, and the
/// blueprint identity index (`blueprint_identity_index.jsonl`,
/// `append_identity_index`)
/// (they never contend on the same file, but share this one lock).
/// In-process only; cross-process locking stays out of contract.
append_write_lock: std::sync::Mutex<()>,
@@ -240,6 +242,17 @@ pub enum RefFault {
BindingRoleUnknown { role: String, roles: Vec<String> },
}
/// One identity-index mapping: the identity id (debug-symbol-blind hash) of
/// a stored blueprint → the content id (byte-exact hash) it lives at. One
/// JSON line per mapping in the `blueprint_identity_index.jsonl` sidecar
/// (#191); duplicate identity ids resolve last-line-wins at read time, so a
/// later repair append heals an earlier stale line.
#[derive(serde::Serialize, serde::Deserialize)]
struct IdentityIndexLine {
identity_id: String,
content_id: String,
}
impl Registry {
/// Referential tier: resolve the campaign's references against the
/// project's stores and check each axis (name AND declared kind) against
@@ -350,28 +363,128 @@ impl Registry {
Ok(faults)
}
/// Scan the blueprint store for a blueprint whose identity id matches —
/// The identity-index sidecar path — the `families.jsonl` discipline: a
/// fixed-name sibling of the runs store, so isolation stays
/// per-directory (#191).
fn identity_index_path(&self) -> PathBuf {
self.path.with_file_name("blueprint_identity_index.jsonl")
}
/// Append one identity→content mapping as a single JSON line, creating
/// the file (and its parent directory) if absent. Serialized by the
/// internal `append_write_lock` (#276) like every other JSONL append
/// path. Callers treat the result as best-effort: the index is a cache,
/// and a cache write must never degrade a correct read.
fn append_identity_index(&self, identity_id: &str, content_id: &str) -> Result<(), RegistryError> {
let _guard = self.append_write_lock.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let path = self.identity_index_path();
if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
fs::create_dir_all(parent)?;
}
let line = serde_json::to_string(&IdentityIndexLine {
identity_id: identity_id.to_string(),
content_id: content_id.to_string(),
})
.expect("two plain strings serialize");
let mut file = fs::OpenOptions::new().create(true).append(true).open(&path)?;
writeln!(file, "{line}")?;
Ok(())
}
/// Read the index into a LAST-line-wins identity→content map. The index
/// is a pure cache with the scan as its truth path, so reads never fail
/// the lookup: a missing or unreadable file is an empty map, and a line
/// that does not parse (torn write, manual edit) is skipped.
fn load_identity_index(&self) -> std::collections::BTreeMap<String, String> {
let Ok(text) = fs::read_to_string(self.identity_index_path()) else {
return std::collections::BTreeMap::new();
};
let mut map = std::collections::BTreeMap::new();
for raw in text.lines() {
if let Ok(line) = serde_json::from_str::<IdentityIndexLine>(raw) {
map.insert(line.identity_id, line.content_id);
}
}
map
}
/// Resolve an identity id to a stored blueprint's canonical JSON —
/// public so a campaign run resolves the same identity refs the
/// referential tier validates.
///
/// Index-first (#191): the persistent `blueprint_identity_index.jsonl`
/// sidecar is consulted and every hit is VERIFIED by loading that one
/// blueprint under the current `resolve` roster and recomputing its
/// identity id — the index is a cache, never an oracle, so a verified hit
/// is always a *valid* match (same identity id) under roster drift, store
/// surgery, or index corruption. Any miss or failed verification falls
/// back to the scan, which doubles as a full-store repair pass: it walks
/// every entry (no early return), recomputes each loadable entry's
/// identity id exactly as before, and best-effort appends every mapping
/// the index lacks or has wrong. A pre-index store therefore backfills
/// itself on the first miss; a read-only store simply keeps scanning as
/// it always has.
///
/// One corner is *not* byte-identical to the scan: two stored blueprints
/// that differ only in fields excluded from the identity hash (e.g. debug
/// names) share one `identity_id` but have distinct `content_id`s. The
/// scan always answers with the first `read_dir` match; a verified index
/// hit answers with whichever of those content ids the index currently
/// maps the identity to, which need not be the first match. Both are
/// valid blueprints for that identity, so the answer is correct either
/// way, just not guaranteed to pick the same one as an uncached scan.
pub fn find_blueprint_by_identity(
&self,
identity_id: &str,
resolve: &dyn Fn(&str) -> Option<PrimitiveBuilder>,
) -> Result<Option<String>, RegistryError> {
let index = self.load_identity_index();
if let Some(content_id) = index.get(identity_id)
&& let Some(json) = self.get_blueprint(content_id)?
&& let Ok(composite) = blueprint_from_json(&json, resolve)
&& let Ok(identity_json) = blueprint_identity_json(&composite)
&& aura_research::content_id_of(&identity_json) == identity_id
{
return Ok(Some(json));
}
// Miss or failed verification: today's scan, as a repair pass. The
// answer comes from the walk (first `read_dir` match, as before),
// never from the repaired index.
let entries = match fs::read_dir(self.blueprints_dir()) {
Ok(e) => e,
Err(_) => return Ok(None), // no store yet -> nothing matches
};
let mut found = None;
for entry in entries {
let entry = entry?;
let json = fs::read_to_string(entry.path())?;
let Ok(composite) = blueprint_from_json(&json, resolve) else { continue };
let Ok(identity_json) = blueprint_identity_json(&composite) else { continue };
if aura_research::content_id_of(&identity_json) == identity_id {
return Ok(Some(json));
let entry_identity = aura_research::content_id_of(&identity_json);
// The filename stem is the content id — `blueprint_path`'s
// write-one/read-one mapping, read back.
let Some(content_id) = entry.path().file_stem().and_then(|s| s.to_str().map(String::from))
else {
continue;
};
if index.get(&entry_identity) != Some(&content_id) {
// absent-or-different only, so a walk over an already-correct
// index appends nothing — *for the common one-content-per-
// identity case*. In the duplicate-identity/distinct-content
// corner (two stored blueprints sharing an identity id, e.g.
// differing only in debug names), `index` is this call's
// snapshot and is never updated mid-walk, so every entry
// whose content id differs from that stale snapshot appends
// a line, one per full-scan lookup, even though each write is
// individually a valid mapping. Best-effort regardless — a
// cache write must never degrade a correct read.
let _ = self.append_identity_index(&entry_identity, &content_id);
}
if found.is_none() && entry_identity == identity_id {
found = Some(json);
}
}
Ok(None)
Ok(found)
}
}
@@ -1781,6 +1894,183 @@ mod tests {
assert_eq!(reg.validate_campaign_refs(&by_identity, &resolve).expect("io ok"), Vec::new());
}
/// Property (#191): the identity-index sidecar is a last-line-wins cache
/// over identity id → content id — a missing file reads as empty (no
/// error, since the scan is the truth path), a later append for the same
/// identity id heals an earlier stale line, and a garbage line (torn
/// concurrent write, manual edit) is skipped rather than failing the read.
#[test]
fn identity_index_reads_last_line_wins_and_skips_garbage() {
let reg = Registry::open(temp_family_dir("identity_index_read"));
// missing file: empty map, no error — the index is a cache and the
// scan is the truth path.
assert!(reg.load_identity_index().is_empty());
reg.append_identity_index("id-a", "content-1").expect("append");
reg.append_identity_index("id-b", "content-2").expect("append");
// a stale line for id-a is healed by a later append: last line wins.
reg.append_identity_index("id-a", "content-3").expect("append");
// garbage lines (torn concurrent write, manual edit) are skipped.
let text = fs::read_to_string(reg.identity_index_path()).expect("index exists");
fs::write(reg.identity_index_path(), format!("{text}not json\n")).expect("garbage line");
let map = reg.load_identity_index();
assert_eq!(map.get("id-a").map(String::as_str), Some("content-3"));
assert_eq!(map.get("id-b").map(String::as_str), Some("content-2"));
assert_eq!(map.len(), 2);
}
/// Store a composite as a canonical blueprint and return its
/// (blueprint_json, content_id, identity_id) — the seeding pattern of
/// `referential_tier_resolves_refs_and_checks_axes`, factored for the
/// identity-index tests.
fn seed_blueprint(reg: &Registry, composite: &aura_engine::Composite) -> (String, String, String) {
let blueprint_json = aura_engine::blueprint_to_json(composite).expect("serializes");
let content_id = aura_research::content_id_of(&blueprint_json);
reg.put_blueprint(&content_id, &blueprint_json).expect("seed blueprint");
let identity_json = blueprint_identity_json(composite).expect("identity form");
let identity_id = aura_research::content_id_of(&identity_json);
(blueprint_json, content_id, identity_id)
}
/// The referential-tier fixture: a one-node composite over the zero-arg
/// vocabulary node `Bias`, loadable via `std_vocabulary`.
fn bias_fixture() -> aura_engine::Composite {
aura_engine::Composite::new(
"fixture",
vec![aura_std::Bias::builder().named("b").into()],
vec![],
vec![],
vec![],
)
}
/// A second, identity-distinct fixture: same shape over the zero-arg
/// vocabulary node `Delay`. Distinct node types are identity-bearing
/// (only debug names are identity-blind), so the two fixtures give the
/// store two entries with different identity ids.
fn delay_fixture() -> aura_engine::Composite {
aura_engine::Composite::new(
"fixture2",
vec![aura_std::Delay::builder().named("d").into()],
vec![],
vec![],
vec![],
)
}
#[test]
fn identity_index_lookup_backfills_and_resolves() {
let reg = Registry::open(temp_family_dir("identity_backfill"));
let resolve = |t: &str| aura_std::std_vocabulary(t);
let (blueprint_json, content_id, identity_id) = seed_blueprint(&reg, &bias_fixture());
// no index yet: the lookup answers via the scan fallback...
assert!(!reg.identity_index_path().exists());
let found = reg.find_blueprint_by_identity(&identity_id, &resolve).expect("io ok");
assert_eq!(found.as_deref(), Some(blueprint_json.as_str()));
// ...and repairs the index in the same pass.
assert_eq!(reg.load_identity_index().get(&identity_id), Some(&content_id));
// the second lookup resolves identically.
let again = reg.find_blueprint_by_identity(&identity_id, &resolve).expect("io ok");
assert_eq!(again.as_deref(), Some(blueprint_json.as_str()));
}
#[test]
fn identity_index_verified_hit_answers_without_a_store_walk() {
let reg = Registry::open(temp_family_dir("identity_hit_no_walk"));
let resolve = |t: &str| aura_std::std_vocabulary(t);
let (blueprint_json, _content_id, identity_id) = seed_blueprint(&reg, &bias_fixture());
// backfill the index, then poison the store: a DIRECTORY named like
// a blueprint makes any scan's read_to_string error, so only a
// walk-free hit can succeed past it.
reg.find_blueprint_by_identity(&identity_id, &resolve).expect("backfill");
fs::create_dir_all(reg.blueprints_dir().join("poison.json")).expect("plant poison dir");
// verified hit: no walk, the poison is never touched.
let found = reg.find_blueprint_by_identity(&identity_id, &resolve).expect("hit path");
assert_eq!(found.as_deref(), Some(blueprint_json.as_str()));
// an unknown identity must scan — and the poison proves the scan
// would have bitten: the lookup errors instead of answering.
assert!(reg.find_blueprint_by_identity("0000", &resolve).is_err());
}
#[test]
fn identity_index_legacy_store_backfills_in_one_pass() {
let reg = Registry::open(temp_family_dir("identity_legacy_backfill"));
let resolve = |t: &str| aura_std::std_vocabulary(t);
let (_json_a, content_a, identity_a) = seed_blueprint(&reg, &bias_fixture());
let (_json_b, content_b, identity_b) = seed_blueprint(&reg, &delay_fixture());
assert_ne!(identity_a, identity_b, "fixtures must be identity-distinct");
assert!(!reg.identity_index_path().exists());
// one miss-lookup (an absent identity) walks the store once and
// returns None — only the scan can prove absence...
assert_eq!(reg.find_blueprint_by_identity("0000", &resolve).expect("io ok"), None);
// ...and that single walk backfills a mapping for EVERY loadable
// entry, not just the queried one.
let map = reg.load_identity_index();
assert_eq!(map.get(&identity_a), Some(&content_a));
assert_eq!(map.get(&identity_b), Some(&content_b));
assert_eq!(map.len(), 2);
}
#[test]
fn identity_index_stale_lines_degrade_to_scan_and_heal() {
let reg = Registry::open(temp_family_dir("identity_stale_heals"));
let resolve = |t: &str| aura_std::std_vocabulary(t);
let (json_a, content_a, identity_a) = seed_blueprint(&reg, &bias_fixture());
let (_json_b, content_b, _identity_b) = seed_blueprint(&reg, &delay_fixture());
// stale shape 1: identity → an absent content id. Verify-on-hit
// cannot load it, the scan answers, the repair appends the correct
// pair behind the stale line — last line wins.
reg.append_identity_index(&identity_a, "feedface").expect("stale line");
let found = reg.find_blueprint_by_identity(&identity_a, &resolve).expect("io ok");
assert_eq!(found.as_deref(), Some(json_a.as_str()));
assert_eq!(reg.load_identity_index().get(&identity_a), Some(&content_a));
// stale shape 2: identity → an EXISTING content id whose recomputed
// identity differs. Verify-on-hit refuses, the scan answers, the
// repair heals again.
reg.append_identity_index(&identity_a, &content_b).expect("stale line");
let found = reg.find_blueprint_by_identity(&identity_a, &resolve).expect("io ok");
assert_eq!(found.as_deref(), Some(json_a.as_str()));
assert_eq!(reg.load_identity_index().get(&identity_a), Some(&content_a));
// healed for good: the hit path now clears a scan-poisoned store.
fs::create_dir_all(reg.blueprints_dir().join("poison.json")).expect("plant poison dir");
let again = reg.find_blueprint_by_identity(&identity_a, &resolve).expect("hit path");
assert_eq!(again.as_deref(), Some(json_a.as_str()));
}
#[test]
fn identity_index_unloadable_under_roster_stays_unmatched() {
let reg = Registry::open(temp_family_dir("identity_unloadable"));
let resolve = |t: &str| aura_std::std_vocabulary(t);
let (_json, _content_id, identity_id) = seed_blueprint(&reg, &bias_fixture());
reg.find_blueprint_by_identity(&identity_id, &resolve).expect("backfill");
// under a roster that cannot load the blueprint the verified hit
// fails and the scan skips the entry — `IdentityUnmatched` semantics
// preserved exactly (a preservation pin: green before AND after the
// lookup rework).
let empty = |_t: &str| None::<PrimitiveBuilder>;
assert_eq!(reg.find_blueprint_by_identity(&identity_id, &empty).expect("io ok"), None);
}
#[test]
fn identity_index_garbage_line_does_not_break_resolution() {
let reg = Registry::open(temp_family_dir("identity_garbage_line"));
let resolve = |t: &str| aura_std::std_vocabulary(t);
let (blueprint_json, _content_id, identity_id) = seed_blueprint(&reg, &bias_fixture());
// a wholly-garbage index file must not break resolution (a
// preservation pin — the scan is the truth path).
fs::write(reg.identity_index_path(), "not json at all\n").expect("garbage index");
let found = reg.find_blueprint_by_identity(&identity_id, &resolve).expect("io ok");
assert_eq!(found.as_deref(), Some(blueprint_json.as_str()));
}
/// Property (#246): a campaign axis naming a BOUND param (not an open
/// one) is checked exactly like an open-param axis — a kind-correct axis
/// over the bound path is fault-free, and a kind-mismatched one over that