chore: rename entity_type to note_type (#600)

Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Paul Hernandez
2026-02-22 20:28:57 -06:00
committed by GitHub
parent e1cccba72d
commit c44291830c
88 changed files with 901 additions and 598 deletions
@@ -0,0 +1,167 @@
"""Rename entity_type column to note_type
Revision ID: j3d4e5f6g7h8
Revises: i2c3d4e5f6g7
Create Date: 2026-02-22 12:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
from sqlalchemy import text
# revision identifiers, used by Alembic.
revision: str = "j3d4e5f6g7h8"
down_revision: Union[str, None] = "i2c3d4e5f6g7"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def table_exists(connection, table_name: str) -> bool:
"""Check if a table exists (idempotent migration support)."""
if connection.dialect.name == "postgresql":
result = connection.execute(
text(
"SELECT 1 FROM information_schema.tables "
"WHERE table_name = :table_name"
),
{"table_name": table_name},
)
return result.fetchone() is not None
# SQLite
result = connection.execute(
text("SELECT 1 FROM sqlite_master WHERE type='table' AND name = :table_name"),
{"table_name": table_name},
)
return result.fetchone() is not None
def index_exists(connection, index_name: str) -> bool:
"""Check if an index exists (idempotent migration support)."""
if connection.dialect.name == "postgresql":
result = connection.execute(
text("SELECT 1 FROM pg_indexes WHERE indexname = :index_name"),
{"index_name": index_name},
)
return result.fetchone() is not None
# SQLite
result = connection.execute(
text("SELECT 1 FROM sqlite_master WHERE type='index' AND name = :index_name"),
{"index_name": index_name},
)
return result.fetchone() is not None
def column_exists(connection, table: str, column: str) -> bool:
"""Check if a column exists in a table (idempotent migration support)."""
if connection.dialect.name == "postgresql":
result = connection.execute(
text(
"SELECT 1 FROM information_schema.columns "
"WHERE table_name = :table AND column_name = :column"
),
{"table": table, "column": column},
)
return result.fetchone() is not None
# SQLite
result = connection.execute(text(f"PRAGMA table_info({table})"))
columns = [row[1] for row in result]
return column in columns
def upgrade() -> None:
"""Rename entity_type → note_type on the entity table."""
connection = op.get_bind()
dialect = connection.dialect.name
# Skip if already migrated (idempotent)
if column_exists(connection, "entity", "note_type"):
return
if dialect == "postgresql":
# Postgres supports direct column rename
op.execute("ALTER TABLE entity RENAME COLUMN entity_type TO note_type")
# Recreate the index with new name
op.execute("DROP INDEX IF EXISTS ix_entity_type")
op.execute("CREATE INDEX ix_note_type ON entity (note_type)")
else:
# SQLite 3.25.0+ supports ALTER TABLE RENAME COLUMN directly.
# Avoids batch_alter_table which fails on tables with generated columns
# (duplicate column name error when recreating the table).
op.execute("ALTER TABLE entity RENAME COLUMN entity_type TO note_type")
# Recreate the index with new name
if index_exists(connection, "ix_entity_type"):
op.drop_index("ix_entity_type", table_name="entity")
op.create_index("ix_note_type", "entity", ["note_type"])
# Update search index metadata: rename entity_type → note_type in JSON
# This updates the stored metadata so search results use the new field name
# Guard: search_index may not exist on a fresh DB (created by an earlier migration)
if not table_exists(connection, "search_index"):
return
if dialect == "postgresql":
op.execute(
text("""
UPDATE search_index
SET metadata = metadata - 'entity_type' || jsonb_build_object('note_type', metadata->'entity_type')
WHERE metadata ? 'entity_type'
""")
)
else:
op.execute(
text("""
UPDATE search_index
SET metadata = json_set(
json_remove(metadata, '$.entity_type'),
'$.note_type',
json_extract(metadata, '$.entity_type')
)
WHERE json_extract(metadata, '$.entity_type') IS NOT NULL
""")
)
def downgrade() -> None:
"""Rename note_type → entity_type on the entity table."""
connection = op.get_bind()
dialect = connection.dialect.name
if dialect == "postgresql":
op.execute("ALTER TABLE entity RENAME COLUMN note_type TO entity_type")
op.execute("DROP INDEX IF EXISTS ix_note_type")
op.execute("CREATE INDEX ix_entity_type ON entity (entity_type)")
else:
op.execute("ALTER TABLE entity RENAME COLUMN note_type TO entity_type")
if index_exists(connection, "ix_note_type"):
op.drop_index("ix_note_type", table_name="entity")
op.create_index("ix_entity_type", "entity", ["entity_type"])
# Revert search index metadata
if not table_exists(connection, "search_index"):
return
if dialect == "postgresql":
op.execute(
text("""
UPDATE search_index
SET metadata = metadata - 'note_type' || jsonb_build_object('entity_type', metadata->'note_type')
WHERE metadata ? 'note_type'
""")
)
else:
op.execute(
text("""
UPDATE search_index
SET metadata = json_set(
json_remove(metadata, '$.note_type'),
'$.entity_type',
json_extract(metadata, '$.note_type')
)
WHERE json_extract(metadata, '$.note_type') IS NOT NULL
""")
)
@@ -201,7 +201,7 @@ async def create_entity(
Created entity with generated external_id (UUID) and file content
"""
logger.info(
"API v2 request", endpoint="create_entity", entity_type=data.entity_type, title=data.title
"API v2 request", endpoint="create_entity", note_type=data.note_type, title=data.title
)
if fast:
@@ -145,14 +145,14 @@ async def create_resource(
# Determine file details
file_name = PathLib(data.file_path).name
content_type = file_service.content_type(data.file_path)
entity_type = "canvas" if data.file_path.endswith(".canvas") else "file"
note_type = "canvas" if data.file_path.endswith(".canvas") else "file"
# Create a new entity model
# Explicitly set external_id to ensure NOT NULL constraint is satisfied (fixes #512)
entity = EntityModel(
external_id=str(uuid.uuid4()),
title=file_name,
entity_type=entity_type,
note_type=note_type,
content_type=content_type,
file_path=data.file_path,
checksum=checksum,
@@ -253,14 +253,14 @@ async def update_resource(
# Determine file details
file_name = PathLib(target_file_path).name
content_type = file_service.content_type(target_file_path)
entity_type = "canvas" if target_file_path.endswith(".canvas") else "file"
note_type = "canvas" if target_file_path.endswith(".canvas") else "file"
# Update entity using internal ID
updated_entity = await entity_repository.update(
entity.id,
{
"title": file_name,
"entity_type": entity_type,
"note_type": note_type,
"content_type": content_type,
"file_path": target_file_path,
"checksum": checksum,
@@ -51,7 +51,7 @@ def _entity_relations(entity: Entity) -> list[RelationData]:
RelationData(
relation_type=rel.relation_type,
target_name=rel.to_name,
target_entity_type=rel.to_entity.entity_type if rel.to_entity else None,
target_note_type=rel.to_entity.note_type if rel.to_entity else None,
)
for rel in entity.outgoing_relations
]
@@ -69,8 +69,8 @@ def _entity_to_note_data(entity: Entity) -> NoteData:
def _entity_frontmatter(entity: Entity) -> dict:
"""Build a frontmatter dict from an entity for schema resolution."""
frontmatter = dict(entity.entity_metadata) if entity.entity_metadata else {}
if entity.entity_type:
frontmatter.setdefault("type", entity.entity_type)
if entity.note_type:
frontmatter.setdefault("type", entity.note_type)
return frontmatter
@@ -81,7 +81,7 @@ def _entity_frontmatter(entity: Entity) -> dict:
async def validate_schema(
entity_repository: EntityRepositoryV2ExternalDep,
project_id: str = Path(..., description="Project external UUID"),
entity_type: str | None = Query(None, description="Entity type to validate"),
note_type: str | None = Query(None, description="Note type to validate"),
identifier: str | None = Query(None, description="Specific note identifier"),
):
"""Validate notes against their resolved schemas.
@@ -95,7 +95,7 @@ async def validate_schema(
if identifier:
entity = await entity_repository.get_by_permalink(identifier)
if not entity:
return ValidationReport(entity_type=entity_type, total_notes=0, results=[])
return ValidationReport(note_type=note_type, total_notes=0, results=[])
frontmatter = _entity_frontmatter(entity)
schema_ref = frontmatter.get("schema")
@@ -120,7 +120,7 @@ async def validate_schema(
results.append(_to_note_validation_response(result))
return ValidationReport(
entity_type=entity_type or entity.entity_type,
note_type=note_type or entity.note_type,
total_notes=1,
valid_count=1 if (results and results[0].passed) else 0,
warning_count=sum(len(r.warnings) for r in results),
@@ -128,8 +128,8 @@ async def validate_schema(
results=results,
)
# --- Batch validation by entity type ---
entities = await _find_by_entity_type(entity_repository, entity_type) if entity_type else []
# --- Batch validation by note type ---
entities = await _find_by_note_type(entity_repository, note_type) if note_type else []
for entity in entities:
frontmatter = _entity_frontmatter(entity)
@@ -156,7 +156,7 @@ async def validate_schema(
valid = sum(1 for r in results if r.passed)
return ValidationReport(
entity_type=entity_type,
note_type=note_type,
total_notes=len(results),
total_entities=len(entities),
valid_count=valid,
@@ -173,7 +173,7 @@ async def validate_schema(
async def infer_schema_endpoint(
entity_repository: EntityRepositoryV2ExternalDep,
project_id: str = Path(..., description="Project external UUID"),
entity_type: str = Query(..., description="Entity type to analyze"),
note_type: str = Query(..., description="Note type to analyze"),
threshold: float = Query(0.25, description="Minimum frequency for optional fields"),
):
"""Infer a schema from existing notes of a given type.
@@ -181,13 +181,13 @@ async def infer_schema_endpoint(
Examines observation categories and relation types across all notes
of the given type. Returns frequency analysis and suggested Picoschema.
"""
entities = await _find_by_entity_type(entity_repository, entity_type)
entities = await _find_by_note_type(entity_repository, note_type)
notes_data = [_entity_to_note_data(entity) for entity in entities]
result = infer_schema(entity_type, notes_data, optional_threshold=threshold)
result = infer_schema(note_type, notes_data, optional_threshold=threshold)
return InferenceReport(
entity_type=result.entity_type,
note_type=result.note_type,
notes_analyzed=result.notes_analyzed,
field_frequencies=[
FieldFrequencyResponse(
@@ -212,10 +212,10 @@ async def infer_schema_endpoint(
# --- Drift Detection ---
@router.get("/schema/diff/{entity_type}", response_model=DriftReport)
@router.get("/schema/diff/{note_type}", response_model=DriftReport)
async def diff_schema_endpoint(
entity_repository: EntityRepositoryV2ExternalDep,
entity_type: str = Path(..., description="Entity type to check for drift"),
note_type: str = Path(..., description="Note type to check for drift"),
project_id: str = Path(..., description="Project external UUID"),
):
"""Show drift between a schema definition and actual note usage.
@@ -229,21 +229,21 @@ async def diff_schema_endpoint(
entities = await _find_schema_entities(entity_repository, query)
return [_entity_frontmatter(e) for e in entities]
# Resolve schema by entity type
schema_frontmatter = {"type": entity_type}
# Resolve schema by note type
schema_frontmatter = {"type": note_type}
schema_def = await resolve_schema(schema_frontmatter, search_fn)
if not schema_def:
return DriftReport(entity_type=entity_type, schema_found=False)
return DriftReport(note_type=note_type, schema_found=False)
# Collect all notes of this type
entities = await _find_by_entity_type(entity_repository, entity_type)
entities = await _find_by_note_type(entity_repository, note_type)
notes_data = [_entity_to_note_data(entity) for entity in entities]
result = diff_schema(schema_def, notes_data)
return DriftReport(
entity_type=entity_type,
note_type=note_type,
new_fields=[
DriftFieldResponse(
name=f.name,
@@ -271,19 +271,19 @@ async def diff_schema_endpoint(
# --- Helpers ---
async def _find_by_entity_type(
async def _find_by_note_type(
entity_repository: EntityRepositoryV2ExternalDep,
entity_type: str,
note_type: str,
) -> list[Entity]:
"""Find all entities of a given type using the repository's select pattern."""
query = entity_repository.select().where(Entity.entity_type == entity_type)
query = entity_repository.select().where(Entity.note_type == note_type)
result = await entity_repository.execute_query(query)
return list(result.scalars().all())
async def _find_schema_entities(
entity_repository: EntityRepositoryV2ExternalDep,
target_entity_type: str,
target_note_type: str,
*,
allow_reference_match: bool = False,
) -> list[Entity]:
@@ -295,11 +295,11 @@ async def _find_schema_entities(
2) Only when allow_reference_match=True and no entity match was found, try
exact reference matching by title/permalink (explicit schema references)
"""
query = entity_repository.select().where(Entity.entity_type == "schema")
query = entity_repository.select().where(Entity.note_type == "schema")
result = await entity_repository.execute_query(query)
entities = list(result.scalars().all())
normalized_target = generate_permalink(target_entity_type)
normalized_target = generate_permalink(target_note_type)
entity_matches = [
e
+1 -1
View File
@@ -61,7 +61,7 @@ async def run_doctor() -> None:
api_note = Entity(
title=api_note_title,
directory="doctor",
entity_type="note",
note_type="note",
content_type="text/markdown",
content=f"# {api_note_title}\n\n- [note] API to file check",
entity_metadata={"tags": ["doctor"]},
+9 -9
View File
@@ -775,16 +775,16 @@ def display_project_info(
console.print(stats_table)
# Entity types
if info.statistics.entity_types:
entity_types_table = Table(title="Entity Types")
entity_types_table.add_column("Type", style="blue")
entity_types_table.add_column("Count", style="green")
# Note types
if info.statistics.note_types:
note_types_table = Table(title="Note Types")
note_types_table.add_column("Type", style="blue")
note_types_table.add_column("Count", style="green")
for entity_type, count in info.statistics.entity_types.items():
entity_types_table.add_row(entity_type, str(count))
for note_type, count in info.statistics.note_types.items():
note_types_table.add_row(note_type, str(count))
console.print(entity_types_table)
console.print(note_types_table)
# Most connected entities
if info.statistics.most_connected_entities: # pragma: no cover
@@ -815,7 +815,7 @@ def display_project_info(
)
recent_table.add_row(
entity["title"],
entity["entity_type"],
entity["note_type"],
updated_at.strftime("%Y-%m-%d %H:%M"),
)
+13 -13
View File
@@ -47,8 +47,8 @@ def _resolve_project_name(project: Optional[str]) -> Optional[str]:
def _render_validate_table(data: dict) -> None:
"""Render a validation report dict as a Rich table."""
entity_type = data.get("entity_type")
title_label = entity_type or "all"
note_type = data.get("note_type")
title_label = note_type or "all"
table = Table(title=f"Schema Validation: {title_label}")
table.add_column("Note", style="cyan")
@@ -84,12 +84,12 @@ def _render_validate_table(data: dict) -> None:
def _render_infer_table(data: dict) -> None:
"""Render an inference report dict as a Rich table."""
entity_type = data.get("entity_type", "")
note_type = data.get("note_type", "")
notes_analyzed = data.get("notes_analyzed", 0)
suggested_required = data.get("suggested_required", [])
suggested_optional = data.get("suggested_optional", [])
console.print(f"\n[bold]Analyzing {notes_analyzed} notes with type: {entity_type}...[/bold]\n")
console.print(f"\n[bold]Analyzing {notes_analyzed} notes with type: {note_type}...[/bold]\n")
table = Table(title="Field Frequencies")
table.add_column("Field", style="cyan")
@@ -126,7 +126,7 @@ def _render_infer_table(data: dict) -> None:
def _render_diff_output(data: dict) -> None:
"""Render a drift report dict as Rich output."""
entity_type = data.get("entity_type", "")
note_type = data.get("note_type", "")
new_fields = data.get("new_fields", [])
dropped_fields = data.get("dropped_fields", [])
cardinality_changes = data.get("cardinality_changes", [])
@@ -134,10 +134,10 @@ def _render_diff_output(data: dict) -> None:
has_drift = new_fields or dropped_fields or cardinality_changes
if not has_drift:
console.print(f"[green]No drift detected for {entity_type} schema.[/green]")
console.print(f"[green]No drift detected for {note_type} schema.[/green]")
return
console.print(f"\n[bold]Schema drift detected for {entity_type}:[/bold]\n")
console.print(f"\n[bold]Schema drift detected for {note_type}:[/bold]\n")
if new_fields:
console.print("[green]+ New fields (common in notes, not in schema):[/green]")
@@ -233,7 +233,7 @@ def validate(
@schema_app.command()
def infer(
entity_type: Annotated[
note_type: Annotated[
str,
typer.Argument(help="Note type to analyze (e.g., person, meeting)"),
],
@@ -268,7 +268,7 @@ def infer(
with force_routing(local=local, cloud=cloud):
result = run_with_cleanup(
mcp_schema_infer(
note_type=entity_type,
note_type=note_type,
threshold=threshold,
project=project_name,
output_format="json",
@@ -285,7 +285,7 @@ def infer(
# Handle zero notes
if result.get("notes_analyzed", 0) == 0:
console.print(f"[yellow]No notes found with type: {entity_type}[/yellow]")
console.print(f"[yellow]No notes found with type: {note_type}[/yellow]")
return
_render_infer_table(result)
@@ -293,7 +293,7 @@ def infer(
if save:
console.print(
f"\n[yellow]--save not yet implemented. "
f"Copy the schema above into schema/{entity_type}.md[/yellow]"
f"Copy the schema above into schema/{note_type}.md[/yellow]"
)
except ValueError as e:
console.print(f"[red]Error: {e}[/red]")
@@ -308,7 +308,7 @@ def infer(
@schema_app.command()
def diff(
entity_type: Annotated[
note_type: Annotated[
str,
typer.Argument(help="Note type to check for drift"),
],
@@ -337,7 +337,7 @@ def diff(
with force_routing(local=local, cloud=cloud):
result = run_with_cleanup(
mcp_schema_diff(
note_type=entity_type,
note_type=note_type,
project=project_name,
output_format="json",
)
+5 -5
View File
@@ -493,7 +493,7 @@ def search_notes(
page=page,
after_date=after_date,
page_size=page_size,
types=note_types,
note_types=note_types,
entity_types=entity_types,
metadata_filters=metadata_filters,
tags=tags,
@@ -654,7 +654,7 @@ def schema_validate(
@tool_app.command("schema-infer")
def schema_infer(
entity_type: Annotated[
note_type: Annotated[
str,
typer.Argument(help="Note type to analyze (e.g., person, meeting)"),
],
@@ -688,7 +688,7 @@ def schema_infer(
with force_routing(local=local, cloud=cloud):
result = run_with_cleanup(
mcp_schema_infer(
note_type=entity_type,
note_type=note_type,
threshold=threshold,
project=project,
workspace=workspace,
@@ -711,7 +711,7 @@ def schema_infer(
@tool_app.command("schema-diff")
def schema_diff(
entity_type: Annotated[
note_type: Annotated[
str,
typer.Argument(help="Note type to check for drift"),
],
@@ -741,7 +741,7 @@ def schema_diff(
with force_routing(local=local, cloud=cloud):
result = run_with_cleanup(
mcp_schema_diff(
note_type=entity_type,
note_type=note_type,
project=project,
workspace=workspace,
output_format="json",
+2 -2
View File
@@ -255,8 +255,8 @@ class EntityParser:
else:
metadata["title"] = title
entity_type = metadata.get("type")
metadata["type"] = entity_type if entity_type is not None else "note"
note_type = metadata.get("type")
metadata["type"] = note_type if note_type is not None else "note"
tags = parse_tags(metadata.get("tags", [])) # pyright: ignore
if tags:
+3 -3
View File
@@ -50,7 +50,7 @@ def entity_model_from_markdown(
# Update basic fields
model.title = markdown.frontmatter.title
model.entity_type = markdown.frontmatter.type
model.note_type = markdown.frontmatter.type
# Only update permalink if it exists in frontmatter, otherwise preserve existing
if markdown.frontmatter.permalink is not None:
model.permalink = markdown.frontmatter.permalink
@@ -86,7 +86,7 @@ async def schema_to_markdown(schema: Any) -> Post:
Convert schema to markdown Post object.
Args:
schema: Schema to convert (must have title, entity_type, and permalink attributes)
schema: Schema to convert (must have title, note_type, and permalink attributes)
Returns:
Post object with frontmatter metadata
@@ -113,7 +113,7 @@ async def schema_to_markdown(schema: Any) -> Post:
post = Post(
content,
title=schema.title,
type=schema.entity_type,
type=schema.note_type,
)
# set the permalink if passed in
if schema.permalink:
+11 -11
View File
@@ -24,7 +24,7 @@ class SchemaClient:
Usage:
async with get_client() as http_client:
client = SchemaClient(http_client, project_id)
report = await client.validate(entity_type="Person")
report = await client.validate(note_type="person")
"""
def __init__(self, http_client: AsyncClient, project_id: str):
@@ -41,13 +41,13 @@ class SchemaClient:
async def validate(
self,
*,
entity_type: str | None = None,
note_type: str | None = None,
identifier: str | None = None,
) -> ValidationReport:
"""Validate notes against their resolved schemas.
Args:
entity_type: Optional entity type to batch-validate
note_type: Optional note type to batch-validate
identifier: Optional specific note to validate
Returns:
@@ -57,8 +57,8 @@ class SchemaClient:
ToolError: If the request fails
"""
params: dict[str, str] = {}
if entity_type:
params["entity_type"] = entity_type
if note_type:
params["note_type"] = note_type
if identifier:
params["identifier"] = identifier
@@ -71,14 +71,14 @@ class SchemaClient:
async def infer(
self,
entity_type: str,
note_type: str,
*,
threshold: float = 0.25,
) -> InferenceReport:
"""Infer a schema from existing notes of a given type.
Args:
entity_type: The entity type to analyze
note_type: The note type to analyze
threshold: Minimum frequency for optional fields (0-1)
Returns:
@@ -90,15 +90,15 @@ class SchemaClient:
response = await call_post(
self.http_client,
f"{self._base_path}/infer",
params={"entity_type": entity_type, "threshold": threshold},
params={"note_type": note_type, "threshold": threshold},
)
return InferenceReport.model_validate(response.json())
async def diff(self, entity_type: str) -> DriftReport:
async def diff(self, note_type: str) -> DriftReport:
"""Show drift between schema definition and actual usage.
Args:
entity_type: The entity type to check for drift
note_type: The note type to check for drift
Returns:
DriftReport with detected differences
@@ -108,6 +108,6 @@ class SchemaClient:
"""
response = await call_get(
self.http_client,
f"{self._base_path}/diff/{entity_type}",
f"{self._base_path}/diff/{note_type}",
)
return DriftReport.model_validate(response.json())
+2 -2
View File
@@ -128,7 +128,7 @@ async def schema_validate(
schema_client = SchemaClient(client, active_project.external_id)
result = await schema_client.validate(
entity_type=note_type,
note_type=note_type,
identifier=identifier,
)
@@ -285,7 +285,7 @@ async def schema_infer(
f"Error inferring schema for type '{note_type}': {e}\n\n"
f"## Troubleshooting\n"
f"1. Ensure notes of type '{note_type}' exist in the project\n"
f'2. Try searching: `search_notes("{note_type}", types=["{note_type}"])`\n'
f'2. Try searching: `search_notes("{note_type}", note_types=["{note_type}"])`\n'
f"3. Verify the project has been synced: `basic-memory status`\n"
)
+13 -13
View File
@@ -113,7 +113,7 @@ def _format_search_error_response(
## Alternative search strategies:
- Break into simpler terms: `search_notes("{project}", "{" ".join(clean_query.split()[:2])}")`
- Try different search types: `search_notes("{project}","{clean_query}", search_type="title")`
- Use filtering: `search_notes("{project}","{clean_query}", types=["entity"])`
- Use filtering: `search_notes("{project}","{clean_query}", note_types=["note"])`
""").strip()
# Project not found errors (check before general "not found")
@@ -164,7 +164,7 @@ def _format_search_error_response(
- Remove restrictive terms: Focus on the most important keywords
5. **Use filtering to narrow scope**:
- By content type: `search_notes("{project}","{query}", types=["entity"])`
- By content type: `search_notes("{project}","{query}", note_types=["note"])`
- By recent content: `search_notes("{project}","{query}", after_date="1 week")`
- By entity type: `search_notes("{project}","{query}", entity_types=["observation"])`
@@ -233,7 +233,7 @@ Error searching for '{query}': {error_message}
- **Different search types**:
- Title only: `search_notes("{project}","{query}", search_type="title")`
- Permalink patterns: `search_notes("{project}","{query}*", search_type="permalink")`
- **With filters**: `search_notes("{project}","{query}", types=["entity"])`
- **With filters**: `search_notes("{project}","{query}", note_types=["note"])`
- **Recent content**: `search_notes("{project}","{query}", after_date="1 week")`
- **Boolean variations**: `search_notes("{project}","{" OR ".join(query.split()[:2])}")`
@@ -264,7 +264,7 @@ async def search_notes(
page_size: int = 10,
search_type: str | None = None,
output_format: Literal["text", "json"] = "text",
types: List[str] | None = None,
note_types: List[str] | None = None,
entity_types: List[str] | None = None,
after_date: Optional[str] = None,
metadata_filters: Optional[Dict[str, Any]] = None,
@@ -309,8 +309,8 @@ async def search_notes(
text when disabled)
### Filtering Options
- `search_notes("my-project", "query", types=["entity"])` - Search only entities
- `search_notes("work-docs", "query", types=["note", "person"])` - Multiple content types
- `search_notes("my-project", "query", note_types=["note"])` - Search only notes
- `search_notes("work-docs", "query", note_types=["note", "person"])` - Multiple note types
- `search_notes("research", "query", entity_types=["observation"])` - Filter by entity type
- `search_notes("team-docs", "query", after_date="2024-01-01")` - Recent content only
- `search_notes("my-project", "query", after_date="1 week")` - Relative date filtering
@@ -353,7 +353,7 @@ async def search_notes(
Default is dynamic: "hybrid" when semantic search is enabled, otherwise "text".
output_format: "text" preserves existing structured search response behavior.
"json" returns a machine-readable dictionary payload.
types: Optional list of note types to search (e.g., ["note", "person"])
note_types: Optional list of note types to search (e.g., ["note", "person"])
entity_types: Optional list of entity types to filter by (e.g., ["entity", "observation"])
after_date: Optional date filter for recent content (e.g., "1 week", "2d", "2024-01-01")
metadata_filters: Optional structured frontmatter filters (e.g., {"status": "in-progress"})
@@ -387,10 +387,10 @@ async def search_notes(
# Exact phrase search
results = await search_notes("\"weekly standup meeting\"")
# Search with type filter
# Search with note type filter
results = await search_notes(
"meeting notes",
types=["entity"],
note_types=["note"],
)
# Search with entity type filter
@@ -420,7 +420,7 @@ async def search_notes(
# Complex search with multiple filters
results = await search_notes(
"(bug OR issue) AND NOT resolved",
types=["entity"],
note_types=["note"],
after_date="2024-01-01"
)
@@ -428,7 +428,7 @@ async def search_notes(
results = await search_notes("project planning", project="my-project")
"""
# Avoid mutable-default-argument footguns. Treat None as "no filter".
types = types or []
note_types = note_types or []
entity_types = entity_types or []
# Detect project from memory URL prefix before routing
@@ -477,8 +477,8 @@ async def search_notes(
# Add optional filters if provided (empty lists are treated as no filter)
if entity_types:
search_query.entity_types = [SearchItemType(t) for t in entity_types]
if types:
search_query.types = types
if note_types:
search_query.note_types = note_types
if after_date:
search_query.after_date = after_date
if metadata_filters:
+2 -2
View File
@@ -28,7 +28,7 @@ async def search_notes_ui(
page: int = 1,
page_size: int = 10,
search_type: Optional[str] = None,
types: List[str] | None = None,
note_types: List[str] | None = None,
entity_types: List[str] | None = None,
after_date: Optional[str] = None,
metadata_filters: Optional[Dict[str, Any]] = None,
@@ -44,7 +44,7 @@ async def search_notes_ui(
page_size=page_size,
search_type=search_type,
output_format="json",
types=types,
note_types=note_types,
entity_types=entity_types,
after_date=after_date,
metadata_filters=metadata_filters,
+1 -1
View File
@@ -174,7 +174,7 @@ async def write_note(
entity = Entity(
title=title,
directory=directory,
entity_type=note_type,
note_type=note_type,
content_type="text/markdown",
content=content,
entity_metadata=entity_metadata or None,
+3 -3
View File
@@ -37,7 +37,7 @@ class Entity(Base):
__tablename__ = "entity"
__table_args__ = (
# Regular indexes
Index("ix_entity_type", "entity_type"),
Index("ix_note_type", "note_type"),
Index("ix_entity_title", "title"),
Index("ix_entity_external_id", "external_id", unique=True),
Index("ix_entity_created_at", "created_at"), # For timeline queries
@@ -64,7 +64,7 @@ class Entity(Base):
# External UUID for API references - stable identifier that won't change
external_id: Mapped[str] = mapped_column(String, unique=True, default=lambda: str(uuid.uuid4()))
title: Mapped[str] = mapped_column(String)
entity_type: Mapped[str] = mapped_column(String)
note_type: Mapped[str] = mapped_column(String)
entity_metadata: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
content_type: Mapped[str] = mapped_column(String)
@@ -133,7 +133,7 @@ class Entity(Base):
return value
def __repr__(self) -> str:
return f"Entity(id={self.id}, external_id='{self.external_id}', name='{self.title}', type='{self.entity_type}', checksum='{self.checksum}')"
return f"Entity(id={self.id}, external_id='{self.external_id}', name='{self.title}', type='{self.note_type}', checksum='{self.checksum}')"
class Observation(Base):
@@ -603,7 +603,7 @@ class PostgresSearchRepository(SearchRepositoryBase):
permalink: Optional[str] = None,
permalink_match: Optional[str] = None,
title: Optional[str] = None,
types: Optional[List[str]] = None,
note_types: Optional[List[str]] = None,
after_date: Optional[datetime] = None,
search_item_types: Optional[List[SearchItemType]] = None,
metadata_filters: Optional[dict] = None,
@@ -619,7 +619,7 @@ class PostgresSearchRepository(SearchRepositoryBase):
permalink=permalink,
permalink_match=permalink_match,
title=title,
types=types,
note_types=note_types,
after_date=after_date,
search_item_types=search_item_types,
metadata_filters=metadata_filters,
@@ -682,14 +682,14 @@ class PostgresSearchRepository(SearchRepositoryBase):
type_list = ", ".join(f"'{t.value}'" for t in search_item_types)
conditions.append(f"search_index.type IN ({type_list})")
# Handle entity type filter using JSONB containment
if types:
# Handle note type filter using JSONB containment (frontmatter type field)
if note_types:
# Use JSONB @> operator for efficient containment queries
type_conditions = []
for entity_type in types:
# Create JSONB containment condition for each type
for note_type in note_types:
# Create JSONB containment condition for each note type
type_conditions.append(
f'search_index.metadata @> \'{{"entity_type": "{entity_type}"}}\''
f'search_index.metadata @> \'{{"note_type": "{note_type}"}}\''
)
conditions.append(f"({' OR '.join(type_conditions)})")
@@ -37,7 +37,7 @@ class SearchRepository(Protocol):
permalink: Optional[str] = None,
permalink_match: Optional[str] = None,
title: Optional[str] = None,
types: Optional[List[str]] = None,
note_types: Optional[List[str]] = None,
after_date: Optional[datetime] = None,
search_item_types: Optional[List[SearchItemType]] = None,
metadata_filters: Optional[dict] = None,
@@ -108,7 +108,7 @@ class SearchRepositoryBase(ABC):
permalink: Optional[str] = None,
permalink_match: Optional[str] = None,
title: Optional[str] = None,
types: Optional[List[str]] = None,
note_types: Optional[List[str]] = None,
after_date: Optional[datetime] = None,
search_item_types: Optional[List[SearchItemType]] = None,
metadata_filters: Optional[Dict[str, Any]] = None,
@@ -124,7 +124,7 @@ class SearchRepositoryBase(ABC):
permalink: Exact permalink match
permalink_match: Permalink pattern match (supports *)
title: Title search
types: Filter by entity types (from metadata.entity_type)
note_types: Filter by note types (from metadata.note_type)
after_date: Filter by created_at > after_date
search_item_types: Filter by SearchItemType (ENTITY, OBSERVATION, RELATION)
metadata_filters: Structured frontmatter metadata filters
@@ -761,7 +761,7 @@ class SearchRepositoryBase(ABC):
permalink: Optional[str],
permalink_match: Optional[str],
title: Optional[str],
types: Optional[List[str]],
note_types: Optional[List[str]],
after_date: Optional[datetime],
search_item_types: Optional[List[SearchItemType]],
metadata_filters: Optional[dict],
@@ -794,7 +794,7 @@ class SearchRepositoryBase(ABC):
permalink=permalink,
permalink_match=permalink_match,
title=title,
types=types,
note_types=note_types,
after_date=after_date,
search_item_types=search_item_types,
metadata_filters=metadata_filters,
@@ -813,7 +813,7 @@ class SearchRepositoryBase(ABC):
permalink=permalink,
permalink_match=permalink_match,
title=title,
types=types,
note_types=note_types,
after_date=after_date,
search_item_types=search_item_types,
metadata_filters=metadata_filters,
@@ -842,7 +842,7 @@ class SearchRepositoryBase(ABC):
permalink: Optional[str],
permalink_match: Optional[str],
title: Optional[str],
types: Optional[List[str]],
note_types: Optional[List[str]],
after_date: Optional[datetime],
search_item_types: Optional[List[SearchItemType]],
metadata_filters: Optional[dict],
@@ -911,7 +911,7 @@ class SearchRepositoryBase(ABC):
permalink,
permalink_match,
title,
types,
note_types,
after_date,
search_item_types,
metadata_filters,
@@ -924,7 +924,7 @@ class SearchRepositoryBase(ABC):
permalink=permalink,
permalink_match=permalink_match,
title=title,
types=types,
note_types=note_types,
after_date=after_date,
search_item_types=search_item_types,
metadata_filters=metadata_filters,
@@ -1054,7 +1054,7 @@ class SearchRepositoryBase(ABC):
permalink: Optional[str],
permalink_match: Optional[str],
title: Optional[str],
types: Optional[List[str]],
note_types: Optional[List[str]],
after_date: Optional[datetime],
search_item_types: Optional[List[SearchItemType]],
metadata_filters: Optional[dict],
@@ -1074,7 +1074,7 @@ class SearchRepositoryBase(ABC):
permalink=permalink,
permalink_match=permalink_match,
title=title,
types=types,
note_types=note_types,
after_date=after_date,
search_item_types=search_item_types,
metadata_filters=metadata_filters,
@@ -1087,7 +1087,7 @@ class SearchRepositoryBase(ABC):
permalink=permalink,
permalink_match=permalink_match,
title=title,
types=types,
note_types=note_types,
after_date=after_date,
search_item_types=search_item_types,
metadata_filters=metadata_filters,
@@ -588,7 +588,7 @@ class SQLiteSearchRepository(SearchRepositoryBase):
permalink: Optional[str] = None,
permalink_match: Optional[str] = None,
title: Optional[str] = None,
types: Optional[List[str]] = None,
note_types: Optional[List[str]] = None,
after_date: Optional[datetime] = None,
search_item_types: Optional[List[SearchItemType]] = None,
metadata_filters: Optional[dict] = None,
@@ -604,7 +604,7 @@ class SQLiteSearchRepository(SearchRepositoryBase):
permalink=permalink,
permalink_match=permalink_match,
title=title,
types=types,
note_types=note_types,
after_date=after_date,
search_item_types=search_item_types,
metadata_filters=metadata_filters,
@@ -671,11 +671,11 @@ class SQLiteSearchRepository(SearchRepositoryBase):
type_list = ", ".join(f"'{t.value}'" for t in search_item_types)
conditions.append(f"search_index.type IN ({type_list})")
# Handle type filter
if types:
type_list = ", ".join(f"'{t}'" for t in types)
# Handle note type filter (frontmatter type field)
if note_types:
type_list = ", ".join(f"'{t}'" for t in note_types)
conditions.append(
f"json_extract(search_index.metadata, '$.entity_type') IN ({type_list})"
f"json_extract(search_index.metadata, '$.note_type') IN ({type_list})"
)
# Handle date filter using datetime() for proper comparison
+9 -9
View File
@@ -30,14 +30,14 @@ class FieldFrequency:
percentage: float
sample_values: list[str] = field(default_factory=list)
is_array: bool = False # True if typically appears multiple times per note
target_type: str | None = None # For relations, the most common target entity type
target_type: str | None = None # For relations, the most common target note type
@dataclass
class InferenceResult:
"""Complete inference result with frequency analysis and suggested schema."""
entity_type: str
note_type: str
notes_analyzed: int
field_frequencies: list[FieldFrequency]
suggested_schema: dict # Ready-to-use Picoschema YAML dict
@@ -65,7 +65,7 @@ class RelationData:
relation_type: str
target_name: str
target_entity_type: str | None = None
target_note_type: str | None = None
@dataclass
@@ -85,7 +85,7 @@ class NoteData:
def infer_schema(
entity_type: str,
note_type: str,
notes: list[NoteData],
required_threshold: float = 0.95,
optional_threshold: float = 0.25,
@@ -98,7 +98,7 @@ def infer_schema(
appear less frequently become optional.
Args:
entity_type: The entity type being analyzed (e.g., "Person").
note_type: The note type being analyzed (e.g., "person").
notes: List of NoteData objects to analyze.
required_threshold: Frequency at or above which a field is required (default 0.95).
optional_threshold: Frequency at or above which a field is optional (default 0.25).
@@ -110,7 +110,7 @@ def infer_schema(
total = len(notes)
if total == 0:
return InferenceResult(
entity_type=entity_type,
note_type=note_type,
notes_analyzed=0,
field_frequencies=[],
suggested_schema={},
@@ -145,7 +145,7 @@ def infer_schema(
)
return InferenceResult(
entity_type=entity_type,
note_type=note_type,
notes_analyzed=total,
field_frequencies=all_frequencies,
suggested_schema=suggested_schema,
@@ -255,8 +255,8 @@ def analyze_relations(
# Track target entity types from individual relations (not the source note)
target_counter = rel_target_types.setdefault(rel_type, Counter())
for rel in note_rel_objects[rel_type]:
if rel.target_entity_type:
target_counter[rel.target_entity_type] += 1
if rel.target_note_type:
target_counter[rel.target_note_type] += 1
frequencies: list[FieldFrequency] = []
for rel_type, count in rel_note_count.most_common():
+2 -2
View File
@@ -8,7 +8,7 @@ import everything from basic_memory.schemas.
# Base types and models
from basic_memory.schemas.base import (
Observation,
EntityType,
NoteType,
RelationType,
Relation,
Entity,
@@ -56,7 +56,7 @@ from basic_memory.schemas.sync_report import (
__all__ = [
# Base
"Observation",
"EntityType",
"NoteType",
"RelationType",
"Relation",
"Entity",
+3 -3
View File
@@ -156,8 +156,8 @@ Permalink = Annotated[str, MinLen(1)]
"""Unique identifier in format '{path}/{normalized_name}'."""
EntityType = Annotated[str, BeforeValidator(to_snake_case), MinLen(1), MaxLen(200)]
"""Classification of entity (e.g., 'person', 'project', 'concept'). """
NoteType = Annotated[str, BeforeValidator(to_snake_case), MinLen(1), MaxLen(200)]
"""Classification of note (e.g., 'note', 'person', 'spec', 'schema'). """
ALLOWED_CONTENT_TYPES = {
"text/markdown",
@@ -228,7 +228,7 @@ class Entity(BaseModel):
title: str
content: Optional[str] = None
directory: str
entity_type: EntityType = "note"
note_type: NoteType = "note"
entity_metadata: Optional[Dict] = Field(default=None, description="Optional metadata")
content_type: ContentType = Field(
description="MIME type of the content (e.g. text/markdown, image/jpeg)",
+1 -1
View File
@@ -18,7 +18,7 @@ class DirectoryNode(BaseModel):
permalink: Optional[str] = None
external_id: Optional[str] = None # UUID (primary API identifier for v2)
entity_id: Optional[int] = None # Internal numeric ID
entity_type: Optional[str] = None
note_type: Optional[str] = None
content_type: Optional[str] = None
updated_at: Optional[datetime] = None
+2 -2
View File
@@ -22,8 +22,8 @@ class ProjectStatistics(BaseModel):
)
# Entity counts by type
entity_types: Dict[str, int] = Field(
description="Count of entities by type (e.g., note, conversation)"
note_types: Dict[str, int] = Field(
description="Count of entities by note type (e.g., note, conversation)"
)
# Observation counts by category
+6 -6
View File
@@ -16,7 +16,7 @@ from typing import List, Optional, Dict
from pydantic import BaseModel, ConfigDict, Field, model_validator
from basic_memory.schemas.base import Relation, Permalink, EntityType, ContentType, Observation
from basic_memory.schemas.base import Relation, Permalink, NoteType, ContentType, Observation
class SQLAlchemyModel(BaseModel):
@@ -162,7 +162,7 @@ class EntityResponse(SQLAlchemyModel):
{
"permalink": "component/memory-service",
"file_path": "MemoryService",
"entity_type": "component",
"note_type": "component",
"entity_metadata": {}
"content_type: "text/markdown"
"observations": [
@@ -191,7 +191,7 @@ class EntityResponse(SQLAlchemyModel):
permalink: Optional[Permalink]
title: str
file_path: str
entity_type: EntityType
note_type: NoteType
entity_metadata: Optional[Dict] = None
checksum: Optional[str] = None
content_type: ContentType
@@ -215,7 +215,7 @@ class EntityListResponse(SQLAlchemyModel):
{
"permalink": "component/search_service",
"title": "SearchService",
"entity_type": "component",
"note_type": "component",
"description": "Knowledge graph search",
"observations": [
{
@@ -227,7 +227,7 @@ class EntityListResponse(SQLAlchemyModel):
{
"permalink": "document/api_docs",
"title": "API_Documentation",
"entity_type": "document",
"note_type": "document",
"description": "API Reference",
"observations": [
{
@@ -255,7 +255,7 @@ class SearchNodesResponse(SQLAlchemyModel):
{
"permalink": "component/memory-service",
"title": "MemoryService",
"entity_type": "component",
"note_type": "component",
"description": "Core service",
"observations": [...],
"relations": [...]
+4 -4
View File
@@ -45,7 +45,7 @@ class NoteValidationResponse(BaseModel):
class ValidationReport(BaseModel):
"""Full validation report for one or more notes."""
entity_type: str | None = None
note_type: str | None = None
total_notes: int = 0
total_entities: int = 0
valid_count: int = 0
@@ -72,14 +72,14 @@ class FieldFrequencyResponse(BaseModel):
)
target_type: str | None = Field(
default=None,
description="For relations, the most common target entity type",
description="For relations, the most common target note type",
)
class InferenceReport(BaseModel):
"""Inference result with suggested schema definition."""
entity_type: str
note_type: str
notes_analyzed: int
field_frequencies: list[FieldFrequencyResponse] = Field(default_factory=list)
suggested_schema: dict = Field(
@@ -110,7 +110,7 @@ class DriftFieldResponse(BaseModel):
class DriftReport(BaseModel):
"""Schema drift analysis comparing schema definition to actual usage."""
entity_type: str
note_type: str
schema_found: bool = Field(
default=True,
description="Whether a schema was found for this type",
+4 -4
View File
@@ -40,7 +40,7 @@ class SearchQuery(BaseModel):
- title: Title only search
Optionally filter results by:
- types: Limit to specific entity types (frontmatter "type")
- note_types: Limit to specific note types (frontmatter "type")
- entity_types: Limit to search item types (entity/observation/relation)
- after_date: Only items after date
- metadata_filters: Structured frontmatter filters (field -> value)
@@ -61,7 +61,7 @@ class SearchQuery(BaseModel):
title: Optional[str] = None # title only search
# Optional filters
types: Optional[List[str]] = None # Filter by type
note_types: Optional[List[str]] = None # Filter by note type (frontmatter "type")
entity_types: Optional[List[SearchItemType]] = None # Filter by entity type
after_date: Optional[Union[datetime, str]] = None # Time-based filter
metadata_filters: Optional[dict[str, Any]] = None # Structured frontmatter filters
@@ -83,7 +83,7 @@ class SearchQuery(BaseModel):
metadata_is_empty = not self.metadata_filters
tags_is_empty = not self.tags
status_is_empty = self.status is None or (isinstance(self.status, str) and not self.status)
types_is_empty = not self.types
note_types_is_empty = not self.note_types
entity_types_is_empty = not self.entity_types
return (
self.permalink is None
@@ -91,7 +91,7 @@ class SearchQuery(BaseModel):
and self.title is None
and text_is_empty
and self.after_date is None
and types_is_empty
and note_types_is_empty
and entity_types_is_empty
and metadata_is_empty
and tags_is_empty
+1 -1
View File
@@ -118,7 +118,7 @@ class EntityResponseV2(BaseModel):
# Core entity fields
title: str = Field(..., description="Entity title")
entity_type: str = Field(..., description="Entity type")
note_type: str = Field(..., description="Note type (from frontmatter 'type' field)")
content_type: str = Field(default="text/markdown", description="Content MIME type")
# Secondary identifiers (for compatibility and convenience)
@@ -89,7 +89,7 @@ class DirectoryService:
permalink=file.permalink,
external_id=file.external_id, # UUID for v2 API
entity_id=file.id,
entity_type=file.entity_type,
note_type=file.note_type,
content_type=file.content_type,
updated_at=_mtime_to_datetime(file),
)
@@ -254,7 +254,7 @@ class DirectoryService:
permalink=file.permalink,
external_id=file.external_id, # UUID for v2 API
entity_id=file.id,
entity_type=file.entity_type,
note_type=file.note_type,
content_type=file.content_type,
updated_at=_mtime_to_datetime(file),
)
+15 -15
View File
@@ -206,14 +206,14 @@ class EntityService(BaseService[EntityModel]):
return self._project_permalink
def _build_frontmatter_markdown(
self, title: str, entity_type: str, permalink: str
self, title: str, note_type: str, permalink: str
) -> EntityMarkdown:
"""Build a minimal EntityMarkdown object for permalink resolution."""
from basic_memory.markdown.schemas import EntityFrontmatter
frontmatter_metadata = {
"title": title,
"type": entity_type,
"type": note_type,
"permalink": permalink,
}
frontmatter_obj = EntityFrontmatter(metadata=frontmatter_metadata)
@@ -257,18 +257,18 @@ class EntityService(BaseService[EntityModel]):
f"file for entity {schema.directory}/{schema.title} already exists: {file_path}"
)
# Parse content frontmatter to check for user-specified permalink and entity_type
# Parse content frontmatter to check for user-specified permalink and note_type
content_markdown = None
if schema.content and has_frontmatter(schema.content):
content_frontmatter = parse_frontmatter(schema.content)
# If content has entity_type/type, use it to override the schema entity_type
# If content has type, use it to override the schema note_type
if "type" in content_frontmatter:
schema.entity_type = content_frontmatter["type"]
schema.note_type = content_frontmatter["type"]
if "permalink" in content_frontmatter:
content_markdown = self._build_frontmatter_markdown(
schema.title, schema.entity_type, content_frontmatter["permalink"]
schema.title, schema.note_type, content_frontmatter["permalink"]
)
# Get unique permalink (prioritizing content frontmatter) unless disabled
@@ -315,18 +315,18 @@ class EntityService(BaseService[EntityModel]):
content=existing_content,
)
# Parse content frontmatter to check for user-specified permalink and entity_type
# Parse content frontmatter to check for user-specified permalink and note_type
content_markdown = None
if schema.content and has_frontmatter(schema.content):
content_frontmatter = parse_frontmatter(schema.content)
# If content has entity_type/type, use it to override the schema entity_type
# If content has type, use it to override the schema note_type
if "type" in content_frontmatter:
schema.entity_type = content_frontmatter["type"]
schema.note_type = content_frontmatter["type"]
if "permalink" in content_frontmatter:
content_markdown = self._build_frontmatter_markdown(
schema.title, schema.entity_type, content_frontmatter["permalink"]
schema.title, schema.note_type, content_frontmatter["permalink"]
)
# Check if we need to update the permalink based on content frontmatter (unless disabled)
@@ -406,11 +406,11 @@ class EntityService(BaseService[EntityModel]):
content_frontmatter = parse_frontmatter(schema.content)
if "type" in content_frontmatter:
schema.entity_type = content_frontmatter["type"]
schema.note_type = content_frontmatter["type"]
if "permalink" in content_frontmatter:
content_markdown = self._build_frontmatter_markdown(
schema.title, schema.entity_type, content_frontmatter["permalink"]
schema.title, schema.note_type, content_frontmatter["permalink"]
)
# --- Permalink Resolution ---
@@ -436,7 +436,7 @@ class EntityService(BaseService[EntityModel]):
entity_metadata = {k: v for k, v in metadata.items() if v is not None}
update_data = {
"title": schema.title,
"entity_type": schema.entity_type,
"note_type": schema.note_type,
"file_path": file_path.as_posix(),
"content_type": schema.content_type,
"entity_metadata": entity_metadata or None,
@@ -488,12 +488,12 @@ class EntityService(BaseService[EntityModel]):
if "title" in content_frontmatter:
update_data["title"] = content_frontmatter["title"]
if "type" in content_frontmatter:
update_data["entity_type"] = content_frontmatter["type"]
update_data["note_type"] = content_frontmatter["type"]
if "permalink" in content_frontmatter:
content_markdown = self._build_frontmatter_markdown(
update_data.get("title", entity.title),
update_data.get("entity_type", entity.entity_type),
update_data.get("note_type", entity.note_type),
content_frontmatter["permalink"],
)
+9 -9
View File
@@ -692,14 +692,14 @@ class ProjectService:
)
total_unresolved = unresolved_count_result.scalar() or 0
# Get entity counts by type
entity_types_result = await self.repository.execute_query(
# Get entity counts by note type
note_types_result = await self.repository.execute_query(
text(
"SELECT entity_type, COUNT(*) FROM entity WHERE project_id = :project_id GROUP BY entity_type"
"SELECT note_type, COUNT(*) FROM entity WHERE project_id = :project_id GROUP BY note_type"
),
{"project_id": project_id},
)
entity_types = {row[0]: row[1] for row in entity_types_result.fetchall()}
note_types = {row[0]: row[1] for row in note_types_result.fetchall()}
# Get observation counts by category
category_result = await self.repository.execute_query(
@@ -761,7 +761,7 @@ class ProjectService:
total_observations=total_observations,
total_relations=total_relations,
total_unresolved_relations=total_unresolved,
entity_types=entity_types,
note_types=note_types,
observation_categories=observation_categories,
relation_types=relation_types,
most_connected_entities=most_connected,
@@ -780,7 +780,7 @@ class ProjectService:
# Get recently created entities (project filtered)
created_result = await self.repository.execute_query(
text("""
SELECT id, title, permalink, entity_type, created_at, file_path
SELECT id, title, permalink, note_type, created_at, file_path
FROM entity
WHERE project_id = :project_id
ORDER BY created_at DESC
@@ -793,7 +793,7 @@ class ProjectService:
"id": row[0],
"title": row[1],
"permalink": row[2],
"entity_type": row[3],
"note_type": row[3],
"created_at": row[4],
"file_path": row[5],
}
@@ -803,7 +803,7 @@ class ProjectService:
# Get recently updated entities (project filtered)
updated_result = await self.repository.execute_query(
text("""
SELECT id, title, permalink, entity_type, updated_at, file_path
SELECT id, title, permalink, note_type, updated_at, file_path
FROM entity
WHERE project_id = :project_id
ORDER BY updated_at DESC
@@ -816,7 +816,7 @@ class ProjectService:
"id": row[0],
"title": row[1],
"permalink": row[2],
"entity_type": row[3],
"note_type": row[3],
"updated_at": row[4],
"file_path": row[5],
}
+4 -4
View File
@@ -178,7 +178,7 @@ class SearchService:
permalink=query.permalink,
permalink_match=query.permalink_match,
title=query.title,
types=query.types,
note_types=query.note_types,
search_item_types=query.entity_types,
after_date=after_date,
metadata_filters=metadata_filters,
@@ -210,7 +210,7 @@ class SearchService:
permalink=query.permalink,
permalink_match=query.permalink_match,
title=query.title,
types=query.types,
note_types=query.note_types,
search_item_types=query.entity_types,
after_date=after_date,
metadata_filters=metadata_filters,
@@ -415,7 +415,7 @@ class SearchService:
permalink=entity.permalink, # Required for Postgres NOT NULL constraint
file_path=entity.file_path,
metadata={
"entity_type": entity.entity_type,
"note_type": entity.note_type,
},
created_at=entity.created_at,
updated_at=_mtime_to_datetime(entity),
@@ -500,7 +500,7 @@ class SearchService:
file_path=entity.file_path,
entity_id=entity.id,
metadata={
"entity_type": entity.entity_type,
"note_type": entity.note_type,
},
created_at=entity.created_at,
updated_at=_mtime_to_datetime(entity),
+1 -1
View File
@@ -761,7 +761,7 @@ class SyncService:
try:
entity = await self.entity_repository.add(
Entity(
entity_type="file",
note_type="file",
file_path=path,
checksum=checksum,
title=file_path.name,