Skip to content

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”

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.

Resolution happens before declaration, and produces the single key-space.

  1. 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.
  2. entity_relationships endpoints 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.
  3. 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_entities is.

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.

  • 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.md independently ratifies the table as provenance_model: client — “empty by default” — so the 14 shipped provenance='core' rows were never sanctioned.
  • Keep the ordering and stop mutating the key (a separate resolved_canonical column, 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.
  • The entity_aliases core seed, _BASELINE_ALIASES in both languages, the ISO branch of canonicalise_entity_name, and canonicalise_for_relationship all 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 since lib/ai/classify.ts was 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_processed cannot diagnose it because its counter sits inside the memoised body.
  • Platform staging has drifted from prod and from seed.sql on one alias row, undeclared and untimestamped — the table has no updated_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.