Summary
file_validation_errors() (forensics/ingest.py:1154-1180) has two defects in how it detects multiple engines/accounts per file. The first defect means the "one engine per file" invariant — which drives structural quarantine of mixed-identity files (#126/#146/#157) — is silently not enforced for v2 uploads, which is the current primary schema version (see the v2 forensics readme, #137).
Defect 1 (primary) — v2 engine multiplicity check is a no-op
The engine-multiplicity set is built by gating on is_hex(engine_id, exact_len=32):
engine_ids = sorted({
line.normalized.get("engine_id")
for line in parsed_lines
if is_hex(line.normalized.get("engine_id"), exact_len=32) # ingest.py:1160
})
if len(engine_ids) > 1:
errors.append("audit log contains multiple engine_ids; ...")
But for marmot-forensics-audit/v2, engine_id is validated (normalize_event, forensics/ingest.py:472-476) as merely a non-empty string ≤64 chars — not hex. So any v2 (non-hex) engine_id is filtered out of the set, the set is empty, and len(...) > 1 never fires.
Scenario: a v2 upload whose lines carry two distinct non-hex engine_ids (e.g. "engine-alpha", "engine-beta") passes as STATUS_VALID. Two engines' events are merged under one file identity — body_account_identity() backfills from whichever engine's source_context appears first, and dedup/grouping/projections treat them as one file. The invariant the schema is supposed to guarantee is violated, and the file is never structurally quarantined. (Account multiplicity is unaffected: account_ref is always validated as 32-hex regardless of version.)
Fix: branch on schema version — for v2, count distinct non-empty engine_id strings directly instead of gating on is_hex(..., exact_len=32).
Defect 2 (secondary, lower confidence) — multiplicity computed over invalid lines
Both sets iterate all parsed_lines, including invalid ones (forensics/ingest.py:1156-1174), unlike file_metadata() which uses valid_lines. A single malformed line whose engine_id/account_ref still normalizes to a well-formed value that differs from the file's real identity flips the whole file to the "multiple engine_ids/account_refs" state and structurally quarantines it — suppressing every otherwise-valid event in the file, contrary to the "preserve line-level evidence" guardrail. This is a judgment call (a file that genuinely names two identities arguably should be flagged), but computing the sets over valid_lines would align it with file_metadata and avoid one corrupt line poisoning the file.
Summary
file_validation_errors()(forensics/ingest.py:1154-1180) has two defects in how it detects multiple engines/accounts per file. The first defect means the "one engine per file" invariant — which drives structural quarantine of mixed-identity files (#126/#146/#157) — is silently not enforced for v2 uploads, which is the current primary schema version (see the v2 forensics readme, #137).Defect 1 (primary) — v2 engine multiplicity check is a no-op
The engine-multiplicity set is built by gating on
is_hex(engine_id, exact_len=32):But for
marmot-forensics-audit/v2,engine_idis validated (normalize_event,forensics/ingest.py:472-476) as merely a non-empty string ≤64 chars — not hex. So any v2 (non-hex)engine_idis filtered out of the set, the set is empty, andlen(...) > 1never fires.Scenario: a v2 upload whose lines carry two distinct non-hex
engine_ids (e.g."engine-alpha","engine-beta") passes asSTATUS_VALID. Two engines' events are merged under one file identity —body_account_identity()backfills from whichever engine'ssource_contextappears first, and dedup/grouping/projections treat them as one file. The invariant the schema is supposed to guarantee is violated, and the file is never structurally quarantined. (Account multiplicity is unaffected:account_refis always validated as 32-hex regardless of version.)Fix: branch on schema version — for v2, count distinct non-empty
engine_idstrings directly instead of gating onis_hex(..., exact_len=32).Defect 2 (secondary, lower confidence) — multiplicity computed over invalid lines
Both sets iterate all
parsed_lines, including invalid ones (forensics/ingest.py:1156-1174), unlikefile_metadata()which usesvalid_lines. A single malformed line whoseengine_id/account_refstill normalizes to a well-formed value that differs from the file's real identity flips the whole file to the "multiple engine_ids/account_refs" state and structurally quarantines it — suppressing every otherwise-valid event in the file, contrary to the "preserve line-level evidence" guardrail. This is a judgment call (a file that genuinely names two identities arguably should be flagged), but computing the sets overvalid_lineswould align it withfile_metadataand avoid one corrupt line poisoning the file.