DR-140: Entity naming has one key-space, produced by resolution rather than by deterministic canonicalisation
DR-140 — Entity naming has one key-space, produced by resolution rather than by deterministic canonicalisation
Section titled “DR-140 — Entity naming has one key-space, produced by resolution rather than by deterministic canonicalisation”Context
Section titled “Context”The platform carried two deliberately divergent canonicalisers, a 14-row entity_aliases
table mirrored as constants in both TypeScript and Python, and a Stage-5 pass that mutated
entity_mentions.canonical_name in place. S545 measured what each contributed.
Of 17 real pairs the cocoindex pair resolver judged to be the same thing —
entity_pair_resolutions being the only surviving archaeology of the real extractor —
canonicalise_entity_name would have collapsed 0, and
canonicalise_for_relationship together with the entire alias map 2. The resolver
alone caught the remaining 15, including gdpr ≡ general data protection regulation and
ico (information commissioner's office) ≡ information commissioner. No regex reaches
those. The deterministic layer also created divergence: _TRAILING_PERIOD_RE strips a
trailing period on the relationship side and not the mention side, and
get_entity_summary — the only join between the two tables — matches by raw string
equality, so the row is silently unjoinable.
Separately, Stage-5 UPDATEs and DELETEs rows the cocoindex engine has already
declared, on a table whose natural unique key contains canonical_name. Mutating a
unique-key component in place guarantees collisions, and
_select_prior_op_key_holders, the cross-op widened DELETE with its curation
re-assertion, the bl-225 collapse block and the Step-6 UPDATE loop all exist to absorb
them.
Decision
Section titled “Decision”Resolution happens before declaration, and produces the single key-space.
- Stage-5’s post-hoc mutation is replaced by a two-phase
use_mount()flow: phase one declares documents and collects names; phase two resolves, then declares mentions keyed on the resolved canonical. This is the shape both cocoindex knowledge-graph examples use, for the reason they state — concepts are shared across documents, so no single file’s component can own them. entity_relationshipsendpoints derive from resolved mentions rather than being canonicalised independently from raw strings, and an endpoint corresponding to no extracted mention is refused — the admission logic DR-135 already applies to mentions.- Deterministic canonicalisation is reduced to what a stable primary key needs
(
strip → NFKD → lower). It is no longer the mechanism that decides two names are one thing;resolve_entitiesis.
DR-105’s curated pin is untouched and is the correct pattern:
ExistingCanonicalPolicy.PINNED with is_existing_canonical is cocoindex’s own
human-override hook, and flow.py’s pin carry-forward re-declares the curated value so
engine state and human intent are the same state.
Alternatives considered
Section titled “Alternatives considered”- Fix the alias map’s key-space instead. Rejected on measurement: the map’s ceiling is
2 of 17 with a perfect key, and a correctly-keyed map still cannot express the 15 the
resolver catches.
ontology/18-entity-aliases.mdindependently ratifies the table asprovenance_model: client— “empty by default” — so the 14 shippedprovenance='core'rows were never sanctioned. - Keep the ordering and stop mutating the key (a separate
resolved_canonicalcolumn, or a synthetic unique key). Cheaper, and it does make collisions impossible — but it leaves the engine’s tracking record disagreeing with the database, which is the condition the collision machinery was built to survive rather than the collisions themselves. - Retire the relationship graph entirely. Genuinely open at the time of the ruling — its consumers are thin. Rejected because “which certifications does this organisation hold” is a real PSQ question and retiring the graph makes it structurally unanswerable.
Consequences
Section titled “Consequences”- The
entity_aliasescore seed,_BASELINE_ALIASESin both languages, the ISO branch ofcanonicalise_entity_name, andcanonicalise_for_relationshipall lose their stated requirement. Their retirement is scoped as its own task and awaits the owner’s go — this decision settles the mechanism, not the deletion schedule. canonicalise_for_relationship’s parity contract has had one side sincelib/ai/classify.tswas deleted; the 44-row parity fixture measures the implementation against a frozen transcript of a function nothing calls.- Inv-7 is a separate live bug and this decision does not fix it. The reading that a
memo-skipped component declares nothing and has its children reconciled away was
reproduced false on the pin: a byte-identical re-stage memo-hits and the declared
children survive; only source-item deletion cascades.
items_processedcannot diagnose it because its counter sits inside the memoised body. - Platform staging has drifted from prod and from
seed.sqlon one alias row, undeclared and untimestamped — the table has noupdated_at. Repair is in scope of the retirement task. - Per DR-139, the pre-130 invariants that shaped this layer (id-53’s Inv-4/Inv-10, id-81’s roster invariants, id-101’s R1) are evidence of intent at that time and do not survive this ruling on their own authority.