affaan-m/ECC · error
invalid canonical health evidence snapshot
Error message
invalid canonical health evidence snapshot
What it means
RecordedEvidence::verify checks that the evidence snapshot is canonical: candidate_id must be exactly 64 lowercase hex characters (plus other structural checks). This error means the health evidence snapshot has a malformed candidate id or otherwise fails canonical-form validation, so it cannot be trusted as a canonical health record.
Solutions
- Use the id returned by HarnessCandidate (e.g. id_for_v2()/id_for_v1()) verbatim; never retype or transform it.
- Lowercase the id and re-check it is 64 hex chars before persisting evidence: id.to_lowercase() with a length/charset check.
- Regenerate the evidence snapshot from the recorded evaluator flow so the id is derived from the candidate, not supplied manually.
- Fix the export/import step that altered the id (e.g. case-folding, truncation, or line-wrapping).
Example fix
// before
let evidence = RecordedEvidence { candidate_id: params.candidate_id.to_uppercase(), .. };
// after
let candidate_id = params.candidate_id.trim().to_lowercase();
assert_eq!(candidate_id.len(), 64);
assert!(candidate_id.bytes().all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()));
let evidence = RecordedEvidence { candidate_id, .. }; Defensive patterns
Strategy: type-guard
Validate before calling
fn is_canonical_candidate_id(id: &str) -> bool {
id.len() == 64
&& id.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
} Type guard
fn narrow_candidate_id(raw: &str) -> Option<CandidateId> {
let s = raw.trim().to_lowercase();
(s.len() == 64 && s.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)))
.then(|| CandidateId(s))
} Try / catch
match evidence.verify() {
Ok(()) => proceed(),
Err(e) if e.to_string().contains("invalid canonical health evidence") => bail!("corrupt candidate id in evidence; regenerate snapshot"),
Err(e) => return Err(e),
} Prevention
- Copy candidate ids programmatically from API return values, never by retyping.
- Lowercase hex ids before persisting; reject ids that fail a 64-hex-char check at ingestion.
- Avoid shell pipelines (tr, cut, word wrap) that transform ids in logs or exports.
When it happens
Trigger: Loading or deserializing a RecordedEvidence whose candidate_id is shorter/longer than 64 chars, contains non-hex characters (uppercase A-F, 'g'-'z', punctuation), or is empty; then calling verify (directly or via canonical_json / evaluate_promote_and_health_check).
Common situations: Truncating or reformatting the candidate id (e.g. uppercasing through a shell pipeline); copying an id from logs that wrapped lines; constructing evidence by hand in a test with a placeholder id like "test-candidate"; exporting data through a system that altered the id's case.
Understand the failure class
Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.
Related errors
- Invalid : expected globally unique id
- Invalid managed hook entry at
- invalid plan-canvas session key
- memory id must match mem_<lowercase-id> and cannot contain…
- placement id must be unique and nonempty
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/3c20b814080ad804.
Report an issue: GitHub.
Appendix: source
Thrown at ecc2/src/harness_eval.rs:348
pub fn canonical_json(&self) -> Result<String> {
self.verify()?;
Ok(serde_json::to_string(self)?)
}
pub fn digest(&self) -> Result<String> {
Ok(sha256_hex(self.canonical_json()?.as_bytes()))
}
pub fn verify(&self) -> Result<()> {
if self.schema_version != 1
|| self.evaluator != "recorded-v1"
|| self.candidate_id.len() != 64
|| !self
.candidate_id
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
{
bail!("invalid canonical health evidence snapshot");
}
Ok(())
}
}
pub struct RecordedEvaluator {
name: String,
scores: BTreeMap<(String, u64), f64>,
health_ok: bool,
health_candidate: Option<String>,
calls: Vec<(String, u64)>,
}
impl RecordedEvaluator {
#[cfg(test)]
pub fn new(scores: BTreeMap<(String, u64), f64>, health_ok: bool) -> Self {
Self {
name: "recorded-v1".into(),View on GitHub (pinned to 8321021c54)