feat(entities): add sol call entities merge primitive
Lifts the deep-merge algorithm from apps/speakers/bootstrap.py:merge_names
into think/entities/merge.py as a slug-keyed primitive merge_entity(...),
exposed via sol call entities merge SOURCE_SLUG TARGET_SLUG. Default-safe
(--no-commit); --commit required to mutate. Reduces sol call speakers
merge-names to a name-resolving shim that preserves its existing contract
including the any-principal rejection.
Policy deltas vs. the original merge_names:
- source-principal + target-not: transfers principal to target
- both-principal: error, no mutation
- --no-keep-source-as-aka: skips adding source display name to target.aka
while still merging source's own aka list
- rejects the merge if any other entity's aka list references the source
slug or display name, listing every offending entity id
Lifts _dedupe_akas / _dedupe_emails / _dedupe_observations from
think/merge.py:_merge_entities into think/entities/merge.py so merge_journals
and merge_entity share one implementation.
Extracts voiceprint I/O (load_existing_voiceprint_keys,
save_voiceprints_batch, save_voiceprints_safely, normalize_embedding,
load_entity_voiceprints_file) into think/entities/voiceprints.py so the
entity merge primitive does not reach up into apps/speakers.
Audit-log line appended to journal/logs/entity-merges.jsonl on successful
commit only; dry-run prints a plan JSON with would_* counts and zero disk
mutation.
Updates the L2 write-owner table in docs/coding-standards.md.