Skip to content

Commit 59134af

Browse files
groksrcclaudephernandez
authored
fix: read schema definitions from file instead of stale database metadata (#635)
Signed-off-by: Drew Cain <groksrc@gmail.com> Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: phernandez <paul@basicmachines.co>
1 parent c7d97de commit 59134af

2 files changed

Lines changed: 334 additions & 8 deletions

File tree

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

Lines changed: 63 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,54 @@ 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+
metadata = dict(post.metadata)
102+
103+
# Trigger: file is mid-edit and missing required schema fields
104+
# Why: parse_schema_note() raises ValueError for missing entity/schema,
105+
# which would turn validation into a 500 response
106+
# Outcome: fall back to last-known-good database metadata
107+
if not metadata.get("entity") or not isinstance(metadata.get("schema"), dict):
108+
logger.warning(
109+
"Schema file has incomplete frontmatter, falling back to database metadata",
110+
file_path=entity.file_path,
111+
)
112+
return _entity_frontmatter(entity)
113+
114+
return metadata
115+
except Exception:
116+
# Trigger: file is missing, unreadable, or has malformed frontmatter
117+
# Why: fall back to database metadata rather than failing validation entirely
118+
# Outcome: behaves like before this change — uses potentially stale data
119+
logger.warning(
120+
"Failed to read schema file, falling back to database metadata",
121+
file_path=entity.file_path,
122+
)
123+
return _entity_frontmatter(entity)
75124

76125

77126
# --- Validation ---
@@ -80,6 +129,7 @@ def _entity_frontmatter(entity: Entity) -> dict:
80129
@router.post("/schema/validate", response_model=ValidationReport)
81130
async def validate_schema(
82131
entity_repository: EntityRepositoryV2ExternalDep,
132+
file_service: FileServiceV2ExternalDep,
83133
link_resolver: LinkResolverV2ExternalDep,
84134
project_id: str = Path(..., description="Project external UUID"),
85135
note_type: str | None = Query(None, description="Note type to validate"),
@@ -89,6 +139,10 @@ async def validate_schema(
89139
90140
Validates a specific note (by identifier) or all notes of a given type.
91141
Returns warnings/errors based on the schema's validation mode.
142+
143+
Schema definitions are read directly from their files to ensure the
144+
latest settings (validation mode, field declarations) are always used,
145+
even when file changes haven't been synced to the database yet.
92146
"""
93147
results: list[NoteValidationResponse] = []
94148

@@ -109,7 +163,7 @@ async def search_fn(query: str) -> list[dict]:
109163
query,
110164
allow_reference_match=isinstance(schema_ref, str) and query == schema_ref,
111165
)
112-
return [_entity_frontmatter(e) for e in entities]
166+
return [await _schema_frontmatter_from_file(file_service, e) for e in entities]
113167

114168
schema_def = await resolve_schema(frontmatter, search_fn)
115169
if schema_def:
@@ -145,7 +199,7 @@ async def search_fn(query: str) -> list[dict]:
145199
query,
146200
allow_reference_match=isinstance(schema_ref, str) and query == schema_ref,
147201
)
148-
return [_entity_frontmatter(e) for e in entities]
202+
return [await _schema_frontmatter_from_file(file_service, e) for e in entities]
149203

150204
schema_def = await resolve_schema(frontmatter, search_fn)
151205
if schema_def:
@@ -219,6 +273,7 @@ async def infer_schema_endpoint(
219273
@router.get("/schema/diff/{note_type}", response_model=DriftReport)
220274
async def diff_schema_endpoint(
221275
entity_repository: EntityRepositoryV2ExternalDep,
276+
file_service: FileServiceV2ExternalDep,
222277
note_type: str = Path(..., description="Note type to check for drift"),
223278
project_id: str = Path(..., description="Project external UUID"),
224279
):
@@ -231,7 +286,7 @@ async def diff_schema_endpoint(
231286

232287
async def search_fn(query: str) -> list[dict]:
233288
entities = await _find_schema_entities(entity_repository, query)
234-
return [_entity_frontmatter(e) for e in entities]
289+
return [await _schema_frontmatter_from_file(file_service, e) for e in entities]
235290

236291
# Resolve schema by note type
237292
schema_frontmatter = {"type": note_type}

0 commit comments

Comments
 (0)