Skip to content

Commit f7d2f35

Browse files
groksrcclaude
andcommitted
fix: read schema definitions from file instead of stale database metadata
schema-validate and schema-diff now read schema note frontmatter directly from the file via file_service, instead of relying on entity_metadata in the database. This ensures validation always uses the latest schema settings (validation mode, field declarations) even when the file watcher hasn't synced changes to the database. Falls back to database metadata if the file can't be read. Fixes #634 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Drew Cain <groksrc@gmail.com>
1 parent c4c9f84 commit f7d2f35

1 file changed

Lines changed: 50 additions & 8 deletions

File tree

src/basic_memory/api/v2/routers/schema_router.py

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,15 @@
1010

1111
from pathlib import Path as FilePath
1212

13+
import frontmatter
1314
from 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+
)
1622
from basic_memory.models.knowledge import Entity
1723
from basic_memory.schemas.schema import (
1824
ValidationReport,
@@ -67,11 +73,41 @@ def _entity_to_note_data(entity: Entity) -> NoteData:
6773

6874

6975
def _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)
81117
async 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)
220261
async 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

Comments
 (0)