1010
1111from pathlib import Path as FilePath
1212
13+ import frontmatter
1314from fastapi import APIRouter , Path , Query
15+ from loguru import logger
1416
15- from basic_memory .deps import EntityRepositoryV2ExternalDep , LinkResolverV2ExternalDep
17+ from basic_memory .deps import (
18+ EntityRepositoryV2ExternalDep ,
19+ FileServiceV2ExternalDep ,
20+ LinkResolverV2ExternalDep ,
21+ )
1622from basic_memory .models .knowledge import Entity
1723from basic_memory .schemas .schema import (
1824 ValidationReport ,
@@ -67,11 +73,41 @@ def _entity_to_note_data(entity: Entity) -> NoteData:
6773
6874
6975def _entity_frontmatter (entity : Entity ) -> dict :
70- """Build a frontmatter dict from an entity for schema resolution."""
71- frontmatter = dict (entity .entity_metadata ) if entity .entity_metadata else {}
76+ """Build a frontmatter dict from an entity's database metadata.
77+
78+ Used for the notes being validated — their type and schema ref are
79+ unlikely to change between syncs.
80+ """
81+ fm = dict (entity .entity_metadata ) if entity .entity_metadata else {}
7282 if entity .note_type :
73- frontmatter .setdefault ("type" , entity .note_type )
74- return frontmatter
83+ fm .setdefault ("type" , entity .note_type )
84+ return fm
85+
86+
87+ async def _schema_frontmatter_from_file (
88+ file_service : FileServiceV2ExternalDep ,
89+ entity : Entity ,
90+ ) -> dict :
91+ """Read a schema entity's frontmatter directly from its file.
92+
93+ Schema definitions (field declarations, validation mode) are the source
94+ of truth for validation. Reading from the file ensures schema-validate
95+ always uses the latest settings, even when the file watcher hasn't
96+ synced changes to entity_metadata in the database.
97+ """
98+ try :
99+ content = await file_service .read_file_content (entity .file_path )
100+ post = frontmatter .loads (content )
101+ return dict (post .metadata )
102+ except Exception :
103+ # Trigger: file is missing, unreadable, or has malformed frontmatter
104+ # Why: fall back to database metadata rather than failing validation entirely
105+ # Outcome: behaves like before this change — uses potentially stale data
106+ logger .warning (
107+ "Failed to read schema file, falling back to database metadata" ,
108+ file_path = entity .file_path ,
109+ )
110+ return _entity_frontmatter (entity )
75111
76112
77113# --- Validation ---
@@ -80,6 +116,7 @@ def _entity_frontmatter(entity: Entity) -> dict:
80116@router .post ("/schema/validate" , response_model = ValidationReport )
81117async def validate_schema (
82118 entity_repository : EntityRepositoryV2ExternalDep ,
119+ file_service : FileServiceV2ExternalDep ,
83120 link_resolver : LinkResolverV2ExternalDep ,
84121 project_id : str = Path (..., description = "Project external UUID" ),
85122 note_type : str | None = Query (None , description = "Note type to validate" ),
@@ -89,6 +126,10 @@ async def validate_schema(
89126
90127 Validates a specific note (by identifier) or all notes of a given type.
91128 Returns warnings/errors based on the schema's validation mode.
129+
130+ Schema definitions are read directly from their files to ensure the
131+ latest settings (validation mode, field declarations) are always used,
132+ even when file changes haven't been synced to the database yet.
92133 """
93134 results : list [NoteValidationResponse ] = []
94135
@@ -109,7 +150,7 @@ async def search_fn(query: str) -> list[dict]:
109150 query ,
110151 allow_reference_match = isinstance (schema_ref , str ) and query == schema_ref ,
111152 )
112- return [_entity_frontmatter ( e ) for e in entities ]
153+ return [await _schema_frontmatter_from_file ( file_service , e ) for e in entities ]
113154
114155 schema_def = await resolve_schema (frontmatter , search_fn )
115156 if schema_def :
@@ -145,7 +186,7 @@ async def search_fn(query: str) -> list[dict]:
145186 query ,
146187 allow_reference_match = isinstance (schema_ref , str ) and query == schema_ref ,
147188 )
148- return [_entity_frontmatter ( e ) for e in entities ]
189+ return [await _schema_frontmatter_from_file ( file_service , e ) for e in entities ]
149190
150191 schema_def = await resolve_schema (frontmatter , search_fn )
151192 if schema_def :
@@ -219,6 +260,7 @@ async def infer_schema_endpoint(
219260@router .get ("/schema/diff/{note_type}" , response_model = DriftReport )
220261async def diff_schema_endpoint (
221262 entity_repository : EntityRepositoryV2ExternalDep ,
263+ file_service : FileServiceV2ExternalDep ,
222264 note_type : str = Path (..., description = "Note type to check for drift" ),
223265 project_id : str = Path (..., description = "Project external UUID" ),
224266):
@@ -231,7 +273,7 @@ async def diff_schema_endpoint(
231273
232274 async def search_fn (query : str ) -> list [dict ]:
233275 entities = await _find_schema_entities (entity_repository , query )
234- return [_entity_frontmatter ( e ) for e in entities ]
276+ return [await _schema_frontmatter_from_file ( file_service , e ) for e in entities ]
235277
236278 # Resolve schema by note type
237279 schema_frontmatter = {"type" : note_type }
0 commit comments