From da12a6374c8fc81452ebca43fa75b03f53e7ed9a Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Fri, 17 Apr 2026 18:44:09 -0600 Subject: [PATCH] refactor(entities): split get_or_create_journal_entity into load + create MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete `get_or_create_journal_entity` and replace with explicit `create_journal_entity` alongside the existing reader `load_journal_entity`. Every caller now composes the two at the call site so writes are visible in the caller's own source: load_journal_entity(id) or create_journal_entity(id, ...) This is the L2 foundation of the entity-write-ownership refactor. Rewritten call sites: - think/importers/shared.py (seed_entities) - apps/speakers/bootstrap.py (bootstrap_voiceprints) - apps/speakers/discovery.py (identify_cluster — reuses existing load) - apps/speakers/owner.py (confirm_owner_candidate) - apps/entities/call.py (attach_entity) - think/entities/saving.py (_save_entities_attached) --- apps/entities/call.py | 4 ++-- apps/speakers/bootstrap.py | 4 ++-- apps/speakers/discovery.py | 4 ++-- apps/speakers/owner.py | 8 +++++--- think/entities/__init__.py | 4 ++-- think/entities/journal.py | 28 ++++++++++++---------------- think/entities/saving.py | 12 ++++++++---- think/importers/shared.py | 5 +++-- 8 files changed, 36 insertions(+), 33 deletions(-) diff --git a/apps/entities/call.py b/apps/entities/call.py index 1e4717032..525c775c5 100644 --- a/apps/entities/call.py +++ b/apps/entities/call.py @@ -15,7 +15,7 @@ import typer from think.entities.core import entity_slug, is_valid_entity_type from think.entities.journal import ( clear_journal_entity_cache, - get_or_create_journal_entity, + create_journal_entity, load_journal_entity, save_journal_entity, ) @@ -282,7 +282,7 @@ def attach_entity( entity_id = entity_slug(name) # Create journal entity (identity record) if it doesn't exist - get_or_create_journal_entity( + load_journal_entity(entity_id) or create_journal_entity( entity_id=entity_id, name=name, entity_type=type_, diff --git a/apps/speakers/bootstrap.py b/apps/speakers/bootstrap.py index ed94c8c03..d3533d923 100644 --- a/apps/speakers/bootstrap.py +++ b/apps/speakers/bootstrap.py @@ -34,8 +34,8 @@ import numpy as np from apps.speakers.owner import load_owner_centroid from think.entities import entity_slug, find_matching_entity, is_name_variant_match from think.entities.journal import ( + create_journal_entity, ensure_journal_entity_memory, - get_or_create_journal_entity, load_all_journal_entities, load_journal_entity, save_journal_entity, @@ -247,7 +247,7 @@ def bootstrap_voiceprints(dry_run: bool = False) -> dict[str, Any]: # Create a new entity for this speaker entity_id = entity_slug(speaker_name) if not dry_run: - entity = get_or_create_journal_entity( + entity = load_journal_entity(entity_id) or create_journal_entity( entity_id=entity_id, name=speaker_name, entity_type="Person", diff --git a/apps/speakers/discovery.py b/apps/speakers/discovery.py index 79fdb30e8..d86c95af2 100644 --- a/apps/speakers/discovery.py +++ b/apps/speakers/discovery.py @@ -343,7 +343,7 @@ def identify_cluster( from apps.speakers.routes import _load_speaker_corrections from think.entities import entity_slug, find_matching_entity from think.entities.journal import ( - get_or_create_journal_entity, + create_journal_entity, load_all_journal_entities, load_journal_entity, ) @@ -370,7 +370,7 @@ def identify_cluster( entity_id = entity_slug(name) existing = load_journal_entity(entity_id) entity_created = existing is None - entity = get_or_create_journal_entity( + entity = existing or create_journal_entity( entity_id=entity_id, name=name, entity_type="Person", diff --git a/apps/speakers/owner.py b/apps/speakers/owner.py index 24e4761e3..2c701e961 100644 --- a/apps/speakers/owner.py +++ b/apps/speakers/owner.py @@ -376,8 +376,9 @@ def confirm_owner_candidate() -> dict[str, Any]: from think.entities import entity_slug from think.entities.core import get_identity_names from think.entities.journal import ( + create_journal_entity, ensure_journal_entity_memory, - get_or_create_journal_entity, + load_journal_entity, ) candidate_path = _owner_candidate_path() @@ -400,8 +401,9 @@ def confirm_owner_candidate() -> dict[str, Any]: if not identity_names: return {"error": "No principal entity found"} principal_name = identity_names[0] - principal = get_or_create_journal_entity( - entity_id=entity_slug(principal_name), + principal_id = entity_slug(principal_name) + principal = load_journal_entity(principal_id) or create_journal_entity( + entity_id=principal_id, name=principal_name, entity_type="Person", ) diff --git a/think/entities/__init__.py b/think/entities/__init__.py index b4f145b93..2832a17d9 100644 --- a/think/entities/__init__.py +++ b/think/entities/__init__.py @@ -49,10 +49,10 @@ from think.entities.formatting import format_entities, format_observations # Journal-level entity management from think.entities.journal import ( block_journal_entity, + create_journal_entity, delete_journal_entity, ensure_journal_entity_memory, get_journal_principal, - get_or_create_journal_entity, has_journal_principal, journal_entity_memory_path, journal_entity_path, @@ -124,10 +124,10 @@ __all__ = [ "is_valid_entity_type", # Journal "block_journal_entity", + "create_journal_entity", "delete_journal_entity", "ensure_journal_entity_memory", "get_journal_principal", - "get_or_create_journal_entity", "has_journal_principal", "journal_entity_memory_path", "journal_entity_path", diff --git a/think/entities/journal.py b/think/entities/journal.py index 76efcf837..78a43e252 100644 --- a/think/entities/journal.py +++ b/think/entities/journal.py @@ -186,7 +186,7 @@ def _should_be_principal(name: str, aka: list[str] | None) -> bool: return False -def get_or_create_journal_entity( +def create_journal_entity( entity_id: str, name: str, entity_type: str, @@ -195,26 +195,24 @@ def get_or_create_journal_entity( *, skip_principal: bool = False, ) -> EntityDict: - """Get existing journal entity or create new one. + """Create and persist a new journal-level entity. - If entity exists, returns it unchanged (does not update fields). - If entity doesn't exist, creates it with provided values. + Caller must guarantee the entity does not already exist. Compose with + `load_journal_entity` at the call site: + `load_journal_entity(id) or create_journal_entity(id, ...)`. Args: entity_id: Entity ID (slug) - name: Entity name - entity_type: Entity type (e.g., "Person", "Company") - aka: Optional list of aliases - skip_principal: If True, don't flag as principal even if matches identity + name: Display name + entity_type: Entity type (e.g. "Person", "Organization") + aka: Optional list of alternate names + emails: Optional list of email addresses (lowercased on save) + skip_principal: If True, do not auto-flag as principal even when the + name/aka match identity and no principal exists yet. Returns: - The existing or newly created entity dict + The newly-created and persisted entity dict. """ - existing = load_journal_entity(entity_id) - if existing: - return existing - - # Create new entity entity: EntityDict = { "id": entity_id, "name": name, @@ -226,8 +224,6 @@ def get_or_create_journal_entity( if emails: entity["emails"] = [e.lower() for e in emails] - # Check if this should be the principal - # Only flag if: matches identity, no existing principal, and not skipped if ( not skip_principal and _should_be_principal(name, aka) diff --git a/think/entities/saving.py b/think/entities/saving.py index 69947e5bc..ba3e3a679 100644 --- a/think/entities/saving.py +++ b/think/entities/saving.py @@ -14,7 +14,11 @@ import random import time from think.entities.core import EntityDict, atomic_write, entity_slug -from think.entities.journal import get_or_create_journal_entity, save_journal_entity +from think.entities.journal import ( + create_journal_entity, + load_journal_entity, + save_journal_entity, +) from think.entities.loading import ( clear_entity_loading_cache, detected_entities_path, @@ -90,9 +94,9 @@ def _save_entities_attached(facet: str, entities: list[EntityDict]) -> None: aka = entity.get("aka") is_detached = entity.get("detached", False) - # Ensure journal entity exists (creates if needed, preserves if exists) - # Skip principal flagging for detached entities - journal_entity = get_or_create_journal_entity( + # Load existing journal entity, or create one. Skip principal + # flagging for detached entities. + journal_entity = load_journal_entity(entity_id) or create_journal_entity( entity_id=entity_id, name=name, entity_type=entity_type, diff --git a/think/importers/shared.py b/think/importers/shared.py index ad8604444..ad68d1383 100644 --- a/think/importers/shared.py +++ b/think/importers/shared.py @@ -667,8 +667,9 @@ def seed_entities( """ from think.entities.core import entity_slug from think.entities.journal import ( - get_or_create_journal_entity, + create_journal_entity, load_all_journal_entities, + load_journal_entity, save_journal_entity, ) from think.entities.matching import find_entity_by_email, find_matching_entity @@ -711,7 +712,7 @@ def seed_entities( # Create new entity eid = entity_slug(name) emails = [email.lower()] if email else None - new_entity = get_or_create_journal_entity( + new_entity = load_journal_entity(eid) or create_journal_entity( entity_id=eid, name=name, entity_type=entity_type, -- 2.51.2