1010
1111from fastapi import APIRouter , Path , Query
1212
13- from basic_memory .deps import (
14- SearchServiceV2ExternalDep ,
15- EntityRepositoryV2ExternalDep ,
16- )
13+ from basic_memory .deps import EntityRepositoryV2ExternalDep
1714from basic_memory .models .knowledge import Entity
1815from basic_memory .schemas .schema import (
1916 ValidationReport ,
2421 FieldFrequencyResponse ,
2522 DriftFieldResponse ,
2623)
27- from basic_memory .schemas .search import SearchQuery
2824from basic_memory .schema .resolver import resolve_schema
2925from basic_memory .schema .validator import validate_note
3026from basic_memory .schema .inference import infer_schema , NoteData , ObservationData , RelationData
@@ -81,7 +77,6 @@ def _entity_frontmatter(entity: Entity) -> dict:
8177@router .post ("/schema/validate" , response_model = ValidationReport )
8278async def validate_schema (
8379 entity_repository : EntityRepositoryV2ExternalDep ,
84- search_service : SearchServiceV2ExternalDep ,
8580 project_id : str = Path (..., description = "Project external UUID" ),
8681 entity_type : str | None = Query (None , description = "Entity type to validate" ),
8782 identifier : str | None = Query (None , description = "Specific note identifier" ),
@@ -93,18 +88,12 @@ async def validate_schema(
9388 """
9489 results : list [NoteValidationResponse ] = []
9590
96- async def search_fn (query : str ) -> list :
97- # Search for schema notes, then load full entity_metadata from the entity table.
98- # The search index only stores minimal metadata (e.g., {"entity_type": "schema"}),
99- # but parse_schema_note needs the full frontmatter with entity/schema/version keys.
100- results = await search_service .search (SearchQuery (text = query , types = ["schema" ]), limit = 5 )
101- frontmatters = []
102- for row in results :
103- if row .permalink :
104- entity = await entity_repository .get_by_permalink (row .permalink )
105- if entity :
106- frontmatters .append (_entity_frontmatter (entity ))
107- return frontmatters
91+ async def search_fn (query : str ) -> list [dict ]:
92+ # Trigger: resolve_schema passes the entity type name as query
93+ # Why: direct metadata match avoids fragile text search that fails when
94+ # schema title doesn't match entity type (e.g. "Strict Person" vs "strictperson")
95+ entities = await _find_schema_entities (entity_repository , query )
96+ return [_entity_frontmatter (e ) for e in entities ]
10897
10998 # --- Single note validation ---
11099 if identifier :
@@ -205,7 +194,6 @@ async def infer_schema_endpoint(
205194@router .get ("/schema/diff/{entity_type}" , response_model = DriftReport )
206195async def diff_schema_endpoint (
207196 entity_repository : EntityRepositoryV2ExternalDep ,
208- search_service : SearchServiceV2ExternalDep ,
209197 entity_type : str = Path (..., description = "Entity type to check for drift" ),
210198 project_id : str = Path (..., description = "Project external UUID" ),
211199):
@@ -216,25 +204,16 @@ async def diff_schema_endpoint(
216204 fields, and cardinality changes.
217205 """
218206
219- async def search_fn (query : str ) -> list :
220- # Search for schema notes, then load full entity_metadata from the entity table.
221- # The search index only stores minimal metadata (e.g., {"entity_type": "schema"}),
222- # but parse_schema_note needs the full frontmatter with entity/schema/version keys.
223- results = await search_service .search (SearchQuery (text = query , types = ["schema" ]), limit = 5 )
224- frontmatters = []
225- for row in results :
226- if row .permalink :
227- entity = await entity_repository .get_by_permalink (row .permalink )
228- if entity :
229- frontmatters .append (_entity_frontmatter (entity ))
230- return frontmatters
207+ async def search_fn (query : str ) -> list [dict ]:
208+ entities = await _find_schema_entities (entity_repository , query )
209+ return [_entity_frontmatter (e ) for e in entities ]
231210
232211 # Resolve schema by entity type
233212 schema_frontmatter = {"type" : entity_type }
234213 schema_def = await resolve_schema (schema_frontmatter , search_fn )
235214
236215 if not schema_def :
237- return DriftReport (entity_type = entity_type )
216+ return DriftReport (entity_type = entity_type , schema_found = False )
238217
239218 # Collect all notes of this type
240219 entities = await _find_by_entity_type (entity_repository , entity_type )
@@ -281,6 +260,26 @@ async def _find_by_entity_type(
281260 return list (result .scalars ().all ())
282261
283262
263+ async def _find_schema_entities (
264+ entity_repository : EntityRepositoryV2ExternalDep ,
265+ target_entity_type : str ,
266+ ) -> list [Entity ]:
267+ """Find schema entities whose entity_metadata['entity'] matches the target type.
268+
269+ Queries all schema-type entities, then filters by metadata in Python.
270+ JSON column filtering syntax varies across SQLite/Postgres; an in-memory
271+ filter on the small set of schema notes is simple and portable.
272+ """
273+ query = entity_repository .select ().where (Entity .entity_type == "schema" )
274+ result = await entity_repository .execute_query (query )
275+ entities = list (result .scalars ().all ())
276+ return [
277+ e
278+ for e in entities
279+ if e .entity_metadata and e .entity_metadata .get ("entity" ) == target_entity_type
280+ ]
281+
282+
284283def _to_note_validation_response (result ) -> NoteValidationResponse :
285284 """Convert a core ValidationResult to a Pydantic response model."""
286285 return NoteValidationResponse (
0 commit comments