Problem
Several places in the ingestion path classify EDN triples by substring-testing the whole rendered triple string, rather than parsing out the attribute position:
":contains" in t / ":contains" not in t — the EAVT-collision split, used in _forward_apply, _reverse_apply, _re_date_structural_facts and _ingest_close.
":introduced-by" not in t — filtering structural triples before re-dating, in _forward_reconcile_provisional and _reverse_apply.
":modified-in" not in t — same shape, in _reverse_apply.
A triple is a string like [:module/auth :contains :function/auth-py-login]. The test matches anywhere in it, including inside a string value. Any fact whose value contains one of these literals — a :description quoting code, a :subject from a commit message mentioning :contains, a file path — is silently misrouted.
Why it matters
Misrouting is not a crash, it is silent data corruption in either direction:
- A triple wrongly classified as
:contains gets split into its own _transact. Harmless.
- A triple wrongly classified as not
:contains gets batched — and minigraf's EAVT pending index omits value bytes, so facts sharing (entity, attribute, valid_from) in one transact collapse to the last. This is exactly the failure that produced sub-phase 2b1, where five of six containment edges on an ordinary file were lost permanently.
- A
:introduced-by-containing value wrongly filtered out of the structural set means an entity's facts are not re-dated when its introduction moves earlier, leaving a valid-time window where the entity has lineage but no type, name or file.
Not currently exploitable with the triple shapes this codebase emits — _edn_escape handles quoting, and no current attribute value has been observed to contain these literals. But the guard is incidental, not designed, and the failure mode is silent.
Suggested fix
Classify on the attribute position rather than the whole string. Either:
- Carry triples as structured
(entity, attribute, value) tuples through the build phase and render to EDN only at the transact boundary — the more invasive but correct fix; or
- Match on the attribute slot specifically, e.g. a compiled
^\[\S+\s+:contains\s anchored pattern, as a low-risk stopgap.
Option 1 also removes the repeated re-parsing of triple strings that several of these call sites do.
Context
Surfaced during the #222 phase 2d whole-branch review (#230). Not a #222 defect — it predates the multi-stream work and applies to the forward-only path equally.
Problem
Several places in the ingestion path classify EDN triples by substring-testing the whole rendered triple string, rather than parsing out the attribute position:
":contains" in t/":contains" not in t— the EAVT-collision split, used in_forward_apply,_reverse_apply,_re_date_structural_factsand_ingest_close.":introduced-by" not in t— filtering structural triples before re-dating, in_forward_reconcile_provisionaland_reverse_apply.":modified-in" not in t— same shape, in_reverse_apply.A triple is a string like
[:module/auth :contains :function/auth-py-login]. The test matches anywhere in it, including inside a string value. Any fact whose value contains one of these literals — a:descriptionquoting code, a:subjectfrom a commit message mentioning:contains, a file path — is silently misrouted.Why it matters
Misrouting is not a crash, it is silent data corruption in either direction:
:containsgets split into its own_transact. Harmless.:containsgets batched — and minigraf's EAVT pending index omits value bytes, so facts sharing(entity, attribute, valid_from)in one transact collapse to the last. This is exactly the failure that produced sub-phase 2b1, where five of six containment edges on an ordinary file were lost permanently.:introduced-by-containing value wrongly filtered out of the structural set means an entity's facts are not re-dated when its introduction moves earlier, leaving a valid-time window where the entity has lineage but no type, name or file.Not currently exploitable with the triple shapes this codebase emits —
_edn_escapehandles quoting, and no current attribute value has been observed to contain these literals. But the guard is incidental, not designed, and the failure mode is silent.Suggested fix
Classify on the attribute position rather than the whole string. Either:
(entity, attribute, value)tuples through the build phase and render to EDN only at the transact boundary — the more invasive but correct fix; or^\[\S+\s+:contains\sanchored pattern, as a low-risk stopgap.Option 1 also removes the repeated re-parsing of triple strings that several of these call sites do.
Context
Surfaced during the #222 phase 2d whole-branch review (#230). Not a #222 defect — it predates the multi-stream work and applies to the forward-only path equally.