Skip to content

A name is a fact, and the extractor reports the other names it reads - #670

Merged
WaylandYang merged 14 commits into
devfrom
feat/a-name-is-a-fact
Sep 13, 2026
Merged

WaylandYang merged 14 commits into
devfrom
feat/a-name-is-a-fact

Conversation

@WaylandYang

@WaylandYang WaylandYang commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Implements cut 1 of 0041. The record and the identity bench landed in #663; the status line now also carries the re-measurement below.

What changes

Names are facts. Migration 0055 creates a builtin known_as attribute per base, turns every entity's canonical name into a value fact on it, turns entities.aliases into name facts and drops the column. A name keeps its string in object_value with no object_id, so it never becomes a node and the canvas never draws it. The temporal engine only closes and conflicts functional or inverse-functional state relations, and known_as is neither.

For entities already merged when the migration runs, the name fact is placed on the survivor at the end of the merged_into chain and appended to moved_subject_facts of every active merge on the chain (and to invalidated_facts when the survivor already has that name), so reverting any link sends the name back exactly as a live merge would.

Resolution reads name facts. Same-type recall, cross-type drift, existing_by_name and the containment scan all read known_as facts, former names included. merge_entities and revert_merge lose their alias special cases: names move with the other facts.

The extractor reports names. The contract gets names: [{ref, name, quote}] and rule 1b (other names only, never a pronoun, a description, or something that belongs to the entity). The server keeps a name only if the name is in its quote and the quote is in the chunk, and drops it (name_claimed_by_another) if the same response or an earlier chunk of the document declares that name for a different entity. The name the model wrote for an entity gets the chunk as evidence when it appears there.

A shared name is sent to the adjudicator. A name another entity of a compatible type already has queues the pair (shared_name|…, adjudicating stage), never a merge. The adjudicator and the Review card see an entity's non-canonical names as an also known as: line; the canonical name is not listed, so two namesakes do not appear to share evidence.

Readers. Node degree, entity_detail facts, Review card fact lines and side degree exclude names. The entity detail API and the entity_facts tool (chat and MCP) return names separately. known_as is removed from the extraction prompt lists, the ontology bootstrap and the ontology vector index.

Measured

Identity bench from #663, DeepSeek-V3.2 + bge-m3, one run per order:

forward F1 reverse F1 pairs judged differently by the two orders
dev 0.43 0.54
this PR 0.68 0.68 0 of 210

A full name and its abbreviation now resolve to one entity in both arrival orders. Two cut-1 runs differed by 0.07 forward with no code change in between, so treat single-run numbers as approximate. Still split: a former name, a name in another script (Li Na, Haitan-2) — cut 2; the two 张伟 — cut 3. Not caught: a description the model reports as a name without declaring it as an entity ("海探1项目").

Verified

  • Migration on a copy of a bench database with four real same-name merges plus a hand-built two-link chain and a leftover alias; reverting B→C, A→B and a same-name merge through the API returned every name to the right entity.
  • cargo test for store, server and extract (491 passed), clippy with -D warnings, fmt.
  • New tests: a_name_is_a_fact (5) and the extractor's names parsing, contract text and claimed-name check.

Review fixes, 2026-09-13

A review before the release found defects outside the paths the tests covered. All are fixed on this branch, each with a test that fails on the previous head where one could be written.

1. A pair kept apart was queued again. pair_shared_name relied on create_review, which only de-duplicates pending pairs. Every later chunk repeating the alias opened a new card and another adjudicator call after someone had decided the two are different. It now skips pairs with a kept review. The Review page words shared_name instead of showing the code. Test: a_pair_kept_apart_is_not_queued_again.

2. known_as could be reached as an ordinary attribute. The ontology vector index still embedded it, so proposals and attribute adoption could map "former_name"-like values onto it through relation_type_id_by_key; the uniqueness panel suggested it for every entity with two names; and update_relation_type accepted functional on it, which would turn every second name into a conflict. Now it is not embedded, not found by key, cannot be edited (builtin_name_attribute), and is not a uniqueness candidate. Test: the_name_attribute_is_not_an_ordinary_attribute.

3. Names from a memory log skipped the nod (0018). Other facts from a memory document wait in pending_facts; the names loop wrote live name facts and pairings. A name read from a memory now becomes a pending known_as proposal; confirming it writes the name fact. No pairing happens at that point.

4. The governance agent was handed bare names as quotes. quotes_of keeps one quote per chunk ordered by confidence, and name facts (confidence 1.0, quote = the name) won. Name evidence whose quote is only the name is left out, and other facts come first within a chunk. An alias with a sentence (简称海探1) is still offered. Test: a_bare_name_is_not_a_quote_and_a_deleted_document_keeps_the_name.

5. Name lookups scanned every base's facts. The correlated EXISTS never filtered the fact row's base, so facts_value_text_idx could not be used. has_name_in is now an uncorrelated IN over the base with the index predicate. On a synthetic base (200k entities, 45k merges) the same-type recall query went from 393 ms to 46 ms; the name part is an index lookup (0.5 ms, was 198 ms), and what remains is the scan over entities caused by the OR, which predates this PR.

6. The migration was quadratic. Survivor lookup, moved_subject_facts and duplicate detection ran a correlated subquery per row over unindexed temp tables. They are now one pass each (DISTINCT ON, grouped arrays joined once, row_number()). On the synthetic base: 707 s before, 30 s now, same results (202,800 name facts, 15,000 duplicates invalidated, 90,000 moved and 30,000 invalidated ids recorded on merges). A user-defined non-builtin known_as relation now gives up its key first instead of silently receiving every name.

Also:

  • Deleting a document no longer invalidates an entity's canonical name fact when that document was its only evidence.
  • The changes feed (chat and MCP changes) no longer lists "X known as X" for every new entity; new aliases still appear.
  • Type-resolution profiles don't count names as roles, quotes or facts.
  • scripts/bench/govern.mjs and ask.mjs no longer read entities.aliases.
  • A declaration that only carries names is not pruned as an orphan by the shape checks.

Checked again

  • cargo test --no-fail-fast for store, server and extract on a migrated scratch database with UTOPIA_TEST_REQUIRE_DB=1: 521 passed, 0 failed, three times. The three new store tests fail on the previous head. Clippy -D warnings, fmt, web style guard, tsc, Vitest (51) clean.
  • Migration on a copy of a real bench base (4,590 entities, 394 merged, 394 active merges, 157 aliases), through the server's own migrator: 153–213 ms. Every surviving entity's live names equal its old canonical name plus aliases (0 missing, 0 extra, 0 duplicates); every merged entity's name is in its own merge's moved_subject_facts; every invalidated duplicate is in a merge's invalidated_facts. Reverting all 394 merges newest-first left every entity with exactly its own name.
  • Memory log end to end (MCP remember on a running server): "海洋探测器1号(简称海探1)…" produced a pending known_as = 海探1 and no live name fact; confirming it created the fact with its evidence, and an entity search for 海探1 then found 海洋探测器1号.
  • Identity bench, DeepSeek-V3 (not V3.2) + bge-m3 through SiliconFlow, because that is the model this environment has:
forward F1 reverse F1
dev 0.462 0.400 (2 of 21 anchors unresolved)
this PR before the review fixes 0.611 0.529
this PR, two runs 0.611, 0.611 0.438, 0.438

Forward is identical across all runs. The reverse difference is one adjudicator ruling on the two 李娜 (merged at 0.95 in one run, kept apart at 0.90 in the other two); the inputs of that pair do not pass through anything changed here. On this model the adjudicator keeps 海洋探测器1号 and 海探1 apart (0.95) in the reverse order even though the full-name entity lists 海探1 — before and after the fixes — so the reverse-order gain reported with V3.2 above does not reproduce on V3. That is the adjudicator's judgement, which is what cut 3 ("evidence decides") addresses.

🤖 Generated with Claude Code

WaylandYang and others added 9 commits September 13, 2026 18:51
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
@WaylandYang
WaylandYang force-pushed the docs/a-name-is-a-claim-about-an-entity branch from 7799424 to 668f2e4 Compare September 13, 2026 10:51
Signed-off-by: WaylandYang <wayland0916@gmail.com>

# Conflicts:
#	docs/decisions/README.md
Base automatically changed from docs/a-name-is-a-claim-about-an-entity to dev September 13, 2026 13:51
WaylandYang and others added 2 commits September 13, 2026 22:13
…te stays builtin, a memory waits for a nod

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
WaylandYang and others added 2 commits September 13, 2026 22:43
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant