{"record":{"id":"88a2b467a0d12843","repo":"affaan-m/ECC","slug":"legacy-candidate-id-collision-with-different-immut","errorCode":null,"errorMessage":"legacy candidate id collision with different immutable content","messagePattern":"legacy candidate id collision with different immutable content","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"ecc2/src/session/store.rs","lineNumber":5376,"sourceCode":"                [&legacy_id],\n                |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?)),\n            )\n            .optional()?;\n        let expected = (\n            candidate.canonical_config.clone(),\n            trace_json.clone(),\n            evidence_json.clone(),\n        );\n        if let Some(stored) = legacy {\n            let legacy_candidate = CandidateSpec {\n                id: legacy_id.clone(),\n                canonical_config: stored.0,\n                trace_refs: serde_json::from_str(&stored.1)?,\n                evidence_refs: serde_json::from_str(&stored.2)?,\n            };\n            legacy_candidate.verify_persisted_id(&legacy_id)?;\n            if legacy_candidate.id_for_v2()? != candidate.id {\n                anyhow::bail!(\"legacy candidate id collision with different immutable content\");\n            }\n            let tx = self.conn.unchecked_transaction()?;\n            Self::register_harness_alias(&tx, &candidate.id, &legacy_id)?;\n            tx.commit()?;\n            return Ok(());\n        }\n        self.conn.execute(\n            \"INSERT INTO harness_candidates (id, canonical_config_json, trace_refs_json, evidence_refs_json, created_at)\n             VALUES (?1, ?2, ?3, ?4, ?5) ON CONFLICT(id) DO NOTHING\",\n            rusqlite::params![candidate.id, candidate.canonical_config, trace_json, evidence_json, chrono::Utc::now().to_rfc3339()],\n        )?;\n        let stored: (String, String, String) = self.conn.query_row(\n            \"SELECT canonical_config_json, trace_refs_json, evidence_refs_json FROM harness_candidates WHERE id = ?1\",\n            [&candidate.id],\n            |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),\n        )?;\n        if stored != expected {\n            anyhow::bail!(\"candidate id collision with different immutable content\");","sourceCodeStart":5358,"sourceCodeEnd":5394,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/ecc2/src/session/store.rs#L5358-L5394","documentation":"When registering a harness candidate, the store checks whether an entry already exists under the candidate's legacy (v1) id. If a stored legacy candidate's derived v2 id differs from the new candidate's v2 id while both map to the same legacy id, the same legacy identifier refers to two different sets of immutable content, which is treated as an unrecoverable identity collision and the registration aborts.","triggerScenarios":"Calling the candidate-registration (persist) path with a candidate whose canonical_config/trace_refs/evidence_refs produce a legacy_id already stored in harness_candidates, but whose id_for_v2() differs from the stored legacy candidate's derived v2 id.","commonSituations":"Re-registering a candidate after editing its canonical config or evidence refs while keeping the same legacy id; restoring an old database where a legacy row was written with different content; a migration/replay tool reusing legacy ids across divergent candidate definitions.","solutions":["Change the candidate's immutable content (canonical_config, trace_refs, or evidence_refs) so it matches what is stored under the legacy id, or use a new legacy id.","Query the harness_candidates table by legacy id and compare the stored canonical_config_json/trace_refs_json/evidence_refs_json with the candidate you are submitting to find the exact field that diverged.","If the stored legacy row is stale or from a different dataset, remove it deliberately and re-register (understanding this invalidates the legacy identity).","Check that the same candidate is not being registered twice from two code paths with slightly different canonical serialization."],"exampleFix":"// before: candidate edited after first registration, legacy id unchanged\nlet candidate = CandidateSpec { id: derived_v2_id, canonical_config: edited_config, .. };\nstore.persist_candidate(&candidate)?; // legacy id collision\n\n// after: recompute ids from current content, or restore original content\nlet candidate = CandidateSpec::from_config(original_config)?; // id matches stored legacy row\nstore.persist_candidate(&candidate)?;","handlingStrategy":"validation","validationCode":"fn ensure_legacy_consistent(store: &Store, candidate: &CandidateSpec) -> anyhow::Result<()> {\n    let legacy_id = candidate.legacy_id();\n    if let Some(stored) = store.lookup_candidate(&legacy_id)? {\n        let stored_v2 = CandidateSpec::from_stored(&legacy_id, &stored)?.id_for_v2()?;\n        anyhow::ensure!(stored_v2 == candidate.id, \"legacy id maps to different content; regenerate ids or restore original content\");\n    }\n    Ok(())\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Treat canonical_config, trace_refs, and evidence_refs as immutable once a legacy id is issued","Derive ids from content every time instead of hardcoding or caching them","Diff candidate content against stored rows in a preflight check before persisting"],"tags":["database","content-addressing","collision","rust"],"backgroundTag":"internal-invariant-violation","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}