mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
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:
@@ -97,6 +97,8 @@ dev = [
|
||||
"pyright>=1.1.408",
|
||||
"pytest-testmon>=2.2.0",
|
||||
"ty>=0.0.18",
|
||||
"cst-lsp>=0.1.3",
|
||||
"libcst>=1.8.6",
|
||||
]
|
||||
|
||||
[tool.hatch.version]
|
||||
@@ -115,6 +117,7 @@ ignore = ["test/"]
|
||||
defineConstant = { DEBUG = true }
|
||||
reportMissingImports = "error"
|
||||
reportMissingTypeStubs = false
|
||||
reportUnusedImport = "none"
|
||||
pythonVersion = "3.12"
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]},
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": [...]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -250,9 +250,9 @@ class TestProjectCommandsAcceptFlags:
|
||||
assert "No such option: --local" not in result.output
|
||||
|
||||
def test_project_move_local_only(self, app_config):
|
||||
"""project move should accept --local flag."""
|
||||
"""project move should reject --cloud flag."""
|
||||
result = runner.invoke(cli_app, ["project", "move", "test", "/tmp/dest", "--cloud"])
|
||||
assert "No such option: --cloud" in result.output
|
||||
assert result.exit_code == 2
|
||||
|
||||
def test_project_ls_accepts_local_flag(self, app_config):
|
||||
"""project ls should accept --local flag."""
|
||||
|
||||
@@ -398,7 +398,7 @@ Related concepts: {keyword_line}.
|
||||
entity = await search_service.entity_repository.create(
|
||||
{
|
||||
"title": f"{topic.title()} Benchmark Note {note_index}",
|
||||
"entity_type": "benchmark",
|
||||
"note_type": "benchmark",
|
||||
"entity_metadata": {"tags": ["benchmark", topic], "status": "active"},
|
||||
"content_type": "text/markdown",
|
||||
"permalink": permalink,
|
||||
|
||||
@@ -109,7 +109,7 @@ async def seed_diagnostic_notes(search_service):
|
||||
entity = await search_service.entity_repository.create(
|
||||
{
|
||||
"title": note["title"],
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"entity_metadata": {"tags": note.get("tags", [])},
|
||||
"content_type": "text/markdown",
|
||||
"permalink": note["permalink"],
|
||||
|
||||
@@ -200,7 +200,7 @@ async def test_postgres_vector_dimension_detection(postgres_engine_factory, tmp_
|
||||
entity = await search_service.entity_repository.create(
|
||||
{
|
||||
"title": "Dimension Test Note",
|
||||
"entity_type": "benchmark",
|
||||
"note_type": "benchmark",
|
||||
"entity_metadata": {"tags": ["test"]},
|
||||
"content_type": "text/markdown",
|
||||
"permalink": "bench/dim-test",
|
||||
@@ -243,7 +243,7 @@ async def test_postgres_incremental_vector_update(postgres_engine_factory, tmp_p
|
||||
entity = await search_service.entity_repository.create(
|
||||
{
|
||||
"title": "Update Test Note",
|
||||
"entity_type": "benchmark",
|
||||
"note_type": "benchmark",
|
||||
"entity_metadata": {"tags": ["test"]},
|
||||
"content_type": "text/markdown",
|
||||
"permalink": "bench/update-test",
|
||||
|
||||
@@ -62,7 +62,7 @@ async def test_disable_permalinks_create_entity(tmp_path, engine_factory, app_co
|
||||
entity_data = EntitySchema(
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="Test content",
|
||||
)
|
||||
|
||||
|
||||
@@ -251,7 +251,7 @@ async def _seed_benchmark_notes(search_service, note_count: int):
|
||||
entity = await search_service.entity_repository.create(
|
||||
{
|
||||
"title": f"{topic.title()} Benchmark Note {note_index}",
|
||||
"entity_type": "benchmark",
|
||||
"note_type": "benchmark",
|
||||
"entity_metadata": {"tags": ["benchmark", topic], "status": "active"},
|
||||
"content_type": "text/markdown",
|
||||
"permalink": permalink,
|
||||
|
||||
@@ -154,7 +154,7 @@ async def test_create_entity(client: AsyncClient, file_service, v2_project_url):
|
||||
data = {
|
||||
"title": "TestV2Entity",
|
||||
"directory": "test",
|
||||
"entity_type": "test",
|
||||
"note_type": "test",
|
||||
"content_type": "text/markdown",
|
||||
"content": "TestContent for V2",
|
||||
}
|
||||
@@ -173,7 +173,7 @@ async def test_create_entity(client: AsyncClient, file_service, v2_project_url):
|
||||
|
||||
assert entity.permalink == "test-project/test/test-v2-entity"
|
||||
assert entity.file_path == "test/TestV2Entity.md"
|
||||
assert entity.entity_type == data["entity_type"]
|
||||
assert entity.note_type == data["note_type"]
|
||||
|
||||
# Verify file was created
|
||||
file_path = file_service.get_entity_path(entity)
|
||||
@@ -187,7 +187,7 @@ async def test_create_entity_conflict_returns_409(client: AsyncClient, v2_projec
|
||||
data = {
|
||||
"title": "TestV2EntityConflict",
|
||||
"directory": "conflict",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"content": "Original content for conflict",
|
||||
}
|
||||
@@ -215,7 +215,7 @@ async def test_create_entity_returns_content(client: AsyncClient, file_service,
|
||||
data = {
|
||||
"title": "TestContentReturn",
|
||||
"directory": "test",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"content": "Body content for return test",
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ async def test_get_recent_context(
|
||||
"""Test getting recent activity context."""
|
||||
entity_data = {
|
||||
"title": "Recent Test Entity",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"file_path": "recent_test.md",
|
||||
"checksum": "abc123",
|
||||
@@ -74,7 +74,7 @@ async def test_get_recent_context_with_pagination(
|
||||
for i in range(5):
|
||||
entity_data = {
|
||||
"title": f"Entity {i}",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"file_path": f"entity_{i}.md",
|
||||
"checksum": f"checksum{i}",
|
||||
@@ -108,7 +108,7 @@ async def test_get_recent_context_with_type_filter(
|
||||
# Create a test entity
|
||||
entity_data = {
|
||||
"title": "Filtered Entity",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"file_path": "filtered.md",
|
||||
"checksum": "xyz789",
|
||||
@@ -162,7 +162,7 @@ async def test_get_memory_context_by_permalink(
|
||||
# Create a test entity
|
||||
entity_data = {
|
||||
"title": "Context Test",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"file_path": "context_test.md",
|
||||
"checksum": "def456",
|
||||
@@ -193,7 +193,7 @@ async def test_get_memory_context_by_id(
|
||||
# Create a test entity
|
||||
entity_data = {
|
||||
"title": "ID Context Test",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"file_path": "id_context_test.md",
|
||||
"checksum": "ghi789",
|
||||
@@ -223,7 +223,7 @@ async def test_get_memory_context_with_depth(
|
||||
# Create a test entity
|
||||
entity_data = {
|
||||
"title": "Depth Test",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"file_path": "depth_test.md",
|
||||
"checksum": "jkl012",
|
||||
@@ -268,7 +268,7 @@ async def test_get_memory_context_with_timeframe(
|
||||
# Create a test entity
|
||||
entity_data = {
|
||||
"title": "Timeframe Test",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"file_path": "timeframe_test.md",
|
||||
"checksum": "mno345",
|
||||
@@ -302,7 +302,7 @@ async def test_recent_context_has_more(
|
||||
for i in range(4):
|
||||
entity_data = {
|
||||
"title": f"HasMore Memory {i}",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"file_path": f"hasmore_mem_{i}.md",
|
||||
"checksum": f"hasmoremem{i}",
|
||||
|
||||
@@ -28,7 +28,7 @@ async def create_person_entities(entity_service, search_service):
|
||||
EntitySchema(
|
||||
title="Alice",
|
||||
directory="people",
|
||||
entity_type="person",
|
||||
note_type="person",
|
||||
content=dedent("""\
|
||||
## Observations
|
||||
- [name] Alice Smith
|
||||
@@ -42,7 +42,7 @@ async def create_person_entities(entity_service, search_service):
|
||||
EntitySchema(
|
||||
title="Bob",
|
||||
directory="people",
|
||||
entity_type="person",
|
||||
note_type="person",
|
||||
content=dedent("""\
|
||||
## Observations
|
||||
- [name] Bob Jones
|
||||
@@ -56,7 +56,7 @@ async def create_person_entities(entity_service, search_service):
|
||||
EntitySchema(
|
||||
title="Carol",
|
||||
directory="people",
|
||||
entity_type="person",
|
||||
note_type="person",
|
||||
content=dedent("""\
|
||||
## Observations
|
||||
- [name] Carol Lee
|
||||
@@ -93,13 +93,13 @@ async def test_infer_schema(
|
||||
|
||||
response = await client.post(
|
||||
f"{v2_project_url}/schema/infer",
|
||||
params={"entity_type": "person"},
|
||||
params={"note_type": "person"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
assert data["entity_type"] == "person"
|
||||
assert data["note_type"] == "person"
|
||||
assert data["notes_analyzed"] == 3
|
||||
assert isinstance(data["field_frequencies"], list)
|
||||
assert isinstance(data["suggested_schema"], dict)
|
||||
@@ -125,7 +125,7 @@ async def test_infer_schema_no_matching_notes(
|
||||
"""Infer returns empty result when no notes of the given type exist."""
|
||||
response = await client.post(
|
||||
f"{v2_project_url}/schema/infer",
|
||||
params={"entity_type": "nonexistent"},
|
||||
params={"note_type": "nonexistent"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -150,7 +150,7 @@ async def test_infer_schema_includes_relations(
|
||||
|
||||
response = await client.post(
|
||||
f"{v2_project_url}/schema/infer",
|
||||
params={"entity_type": "person"},
|
||||
params={"note_type": "person"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -184,7 +184,7 @@ async def test_validate_with_inline_schema(
|
||||
EntitySchema(
|
||||
title="Dave",
|
||||
directory="people",
|
||||
entity_type="person",
|
||||
note_type="person",
|
||||
entity_metadata={
|
||||
"schema": {"name": "string", "role": "string"},
|
||||
},
|
||||
@@ -199,7 +199,7 @@ async def test_validate_with_inline_schema(
|
||||
|
||||
response = await client.post(
|
||||
f"{v2_project_url}/schema/validate",
|
||||
params={"entity_type": "person"},
|
||||
params={"note_type": "person"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -233,7 +233,7 @@ async def test_validate_with_explicit_schema_reference_by_permalink_slug(
|
||||
EntitySchema(
|
||||
title="Strict Person V2",
|
||||
directory="schemas",
|
||||
entity_type="schema",
|
||||
note_type="schema",
|
||||
entity_metadata={
|
||||
"entity": "person",
|
||||
"schema": {"name": "string", "role": "string"},
|
||||
@@ -250,7 +250,7 @@ async def test_validate_with_explicit_schema_reference_by_permalink_slug(
|
||||
EntitySchema(
|
||||
title="Frank",
|
||||
directory="people",
|
||||
entity_type="person",
|
||||
note_type="person",
|
||||
entity_metadata={
|
||||
# Explicit schema identifier does not equal entity metadata field ("person")
|
||||
# and must resolve by schema identifier, not fuzzy text search.
|
||||
@@ -292,7 +292,7 @@ async def test_validate_missing_required_field(
|
||||
EntitySchema(
|
||||
title="Eve",
|
||||
directory="people",
|
||||
entity_type="person",
|
||||
note_type="person",
|
||||
entity_metadata={
|
||||
"schema": {"name": "string", "role": "string"},
|
||||
},
|
||||
@@ -306,7 +306,7 @@ async def test_validate_missing_required_field(
|
||||
|
||||
response = await client.post(
|
||||
f"{v2_project_url}/schema/validate",
|
||||
params={"entity_type": "person"},
|
||||
params={"note_type": "person"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -329,7 +329,7 @@ async def test_validate_no_matching_notes(
|
||||
"""Validate returns empty result when no notes of the given type exist."""
|
||||
response = await client.post(
|
||||
f"{v2_project_url}/schema/validate",
|
||||
params={"entity_type": "nonexistent"},
|
||||
params={"note_type": "nonexistent"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -352,7 +352,7 @@ async def test_validate_total_entities_without_schema(
|
||||
|
||||
response = await client.post(
|
||||
f"{v2_project_url}/schema/validate",
|
||||
params={"entity_type": "person"},
|
||||
params={"note_type": "person"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -381,7 +381,7 @@ async def test_validate_with_frontmatter_rules_passes(
|
||||
EntitySchema(
|
||||
title="Person Schema FM",
|
||||
directory="schemas",
|
||||
entity_type="schema",
|
||||
note_type="schema",
|
||||
entity_metadata={
|
||||
"entity": "person_fm",
|
||||
"schema": {"name": "string"},
|
||||
@@ -406,7 +406,7 @@ async def test_validate_with_frontmatter_rules_passes(
|
||||
EntitySchema(
|
||||
title="Grace",
|
||||
directory="people",
|
||||
entity_type="person_fm",
|
||||
note_type="person_fm",
|
||||
entity_metadata={
|
||||
"tags": ["engineer", "python"],
|
||||
"status": "published",
|
||||
@@ -449,7 +449,7 @@ async def test_validate_frontmatter_missing_required_key_warns(
|
||||
EntitySchema(
|
||||
title="Person Schema FM2",
|
||||
directory="schemas",
|
||||
entity_type="schema",
|
||||
note_type="schema",
|
||||
entity_metadata={
|
||||
"entity": "person_fm2",
|
||||
"schema": {"name": "string"},
|
||||
@@ -473,7 +473,7 @@ async def test_validate_frontmatter_missing_required_key_warns(
|
||||
EntitySchema(
|
||||
title="Hank",
|
||||
directory="people",
|
||||
entity_type="person_fm2",
|
||||
note_type="person_fm2",
|
||||
content=dedent("""\
|
||||
## Observations
|
||||
- [name] Hank Pym
|
||||
@@ -507,7 +507,7 @@ async def test_validate_frontmatter_enum_mismatch_warns(
|
||||
EntitySchema(
|
||||
title="Person Schema FM3",
|
||||
directory="schemas",
|
||||
entity_type="schema",
|
||||
note_type="schema",
|
||||
entity_metadata={
|
||||
"entity": "person_fm3",
|
||||
"schema": {"name": "string"},
|
||||
@@ -531,7 +531,7 @@ async def test_validate_frontmatter_enum_mismatch_warns(
|
||||
EntitySchema(
|
||||
title="Iris",
|
||||
directory="people",
|
||||
entity_type="person_fm3",
|
||||
note_type="person_fm3",
|
||||
entity_metadata={
|
||||
"status": "archived", # not in [draft, published]
|
||||
},
|
||||
@@ -575,7 +575,7 @@ async def test_diff_no_schema_found(
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["entity_type"] == "nonexistent"
|
||||
assert data["note_type"] == "nonexistent"
|
||||
assert data["new_fields"] == []
|
||||
assert data["dropped_fields"] == []
|
||||
assert data["cardinality_changes"] == []
|
||||
@@ -597,7 +597,7 @@ async def test_diff_with_schema_note(
|
||||
EntitySchema(
|
||||
title="Person Schema",
|
||||
directory="schemas",
|
||||
entity_type="schema",
|
||||
note_type="schema",
|
||||
entity_metadata={
|
||||
"entity": "person",
|
||||
"version": 1,
|
||||
@@ -620,7 +620,7 @@ async def test_diff_with_schema_note(
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["entity_type"] == "person"
|
||||
assert data["note_type"] == "person"
|
||||
assert isinstance(data["new_fields"], list)
|
||||
assert isinstance(data["dropped_fields"], list)
|
||||
assert isinstance(data["cardinality_changes"], list)
|
||||
|
||||
@@ -44,7 +44,7 @@ async def test_search_entities(
|
||||
# Create a test entity
|
||||
entity_data = {
|
||||
"title": "Searchable Entity",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"file_path": "searchable.md",
|
||||
"checksum": "search123",
|
||||
@@ -79,7 +79,7 @@ async def test_search_with_pagination(
|
||||
for i in range(5):
|
||||
entity_data = {
|
||||
"title": f"Search Entity {i}",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"file_path": f"search_{i}.md",
|
||||
"checksum": f"searchsum{i}",
|
||||
@@ -114,7 +114,7 @@ async def test_search_by_permalink(
|
||||
# Create a test entity with permalink
|
||||
entity_data = {
|
||||
"title": "Permalink Search",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"file_path": "permalink_search.md",
|
||||
"checksum": "perm123",
|
||||
@@ -147,7 +147,7 @@ async def test_search_by_title(
|
||||
# Create a test entity
|
||||
entity_data = {
|
||||
"title": "Unique Title For Search",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"file_path": "unique_title.md",
|
||||
"checksum": "title123",
|
||||
@@ -175,13 +175,13 @@ async def test_search_with_type_filter(
|
||||
):
|
||||
"""Test searching with entity type filter."""
|
||||
# Create test entities of different types
|
||||
for entity_type in ["note", "document"]:
|
||||
for note_type in ["note", "document"]:
|
||||
entity_data = {
|
||||
"title": f"Type {entity_type}",
|
||||
"entity_type": entity_type,
|
||||
"title": f"Type {note_type}",
|
||||
"note_type": note_type,
|
||||
"content_type": "text/markdown",
|
||||
"file_path": f"type_{entity_type}.md",
|
||||
"checksum": f"type{entity_type}",
|
||||
"file_path": f"type_{note_type}.md",
|
||||
"checksum": f"type{note_type}",
|
||||
}
|
||||
await create_test_entity(
|
||||
test_project, entity_data, entity_repository, search_service, file_service
|
||||
@@ -189,7 +189,7 @@ async def test_search_with_type_filter(
|
||||
|
||||
# Search with type filter
|
||||
response = await client.post(
|
||||
f"{v2_project_url}/search/", json={"search_text": "Type", "types": ["note"]}
|
||||
f"{v2_project_url}/search/", json={"search_text": "Type", "note_types": ["note"]}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -210,7 +210,7 @@ async def test_search_with_date_filter(
|
||||
# Create a test entity
|
||||
entity_data = {
|
||||
"title": "Date Filtered",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"file_path": "date_filtered.md",
|
||||
"checksum": "date123",
|
||||
@@ -377,7 +377,7 @@ async def test_search_has_more_when_more_results_exist(
|
||||
for i in range(4):
|
||||
entity_data = {
|
||||
"title": f"HasMore Entity {i}",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"file_path": f"hasmore_{i}.md",
|
||||
"checksum": f"hasmore{i}",
|
||||
@@ -411,7 +411,7 @@ async def test_search_has_more_false_on_last_page(
|
||||
"""has_more should be False when all results fit on the current page."""
|
||||
entity_data = {
|
||||
"title": "Solo Search Entity",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"file_path": "solo_search.md",
|
||||
"checksum": "solo123",
|
||||
|
||||
@@ -15,7 +15,7 @@ runner = CliRunner()
|
||||
# --- Shared mock data ---
|
||||
|
||||
VALIDATE_REPORT = {
|
||||
"entity_type": "person",
|
||||
"note_type": "person",
|
||||
"total_notes": 2,
|
||||
"total_entities": 2,
|
||||
"valid_count": 1,
|
||||
@@ -40,7 +40,7 @@ VALIDATE_REPORT = {
|
||||
}
|
||||
|
||||
INFER_REPORT = {
|
||||
"entity_type": "person",
|
||||
"note_type": "person",
|
||||
"notes_analyzed": 5,
|
||||
"field_frequencies": [
|
||||
{"name": "name", "source": "observation", "count": 5, "total": 5, "percentage": 1.0},
|
||||
@@ -53,7 +53,7 @@ INFER_REPORT = {
|
||||
}
|
||||
|
||||
DIFF_REPORT_WITH_DRIFT = {
|
||||
"entity_type": "person",
|
||||
"note_type": "person",
|
||||
"schema_found": True,
|
||||
"new_fields": [
|
||||
{"name": "email", "source": "observation", "count": 3, "total": 5, "percentage": 0.6}
|
||||
@@ -65,7 +65,7 @@ DIFF_REPORT_WITH_DRIFT = {
|
||||
}
|
||||
|
||||
DIFF_REPORT_NO_DRIFT = {
|
||||
"entity_type": "person",
|
||||
"note_type": "person",
|
||||
"schema_found": True,
|
||||
"new_fields": [],
|
||||
"dropped_fields": [],
|
||||
@@ -214,7 +214,7 @@ def test_infer_error_response(mock_mcp, mock_config_cls):
|
||||
"basic_memory.cli.commands.schema.mcp_schema_infer",
|
||||
new_callable=AsyncMock,
|
||||
return_value={
|
||||
"entity_type": "person",
|
||||
"note_type": "person",
|
||||
"notes_analyzed": 0,
|
||||
"field_frequencies": [],
|
||||
"suggested_schema": {},
|
||||
|
||||
@@ -475,7 +475,7 @@ def test_routing_both_flags_error():
|
||||
# --- schema-validate ---
|
||||
|
||||
SCHEMA_VALIDATE_RESULT = {
|
||||
"entity_type": "person",
|
||||
"note_type": "person",
|
||||
"total_notes": 2,
|
||||
"total_entities": 2,
|
||||
"valid_count": 1,
|
||||
@@ -514,7 +514,7 @@ def test_schema_validate_json_output(mock_mcp):
|
||||
|
||||
assert result.exit_code == 0, f"CLI failed: {result.output}"
|
||||
data = json.loads(result.output)
|
||||
assert data["entity_type"] == "person"
|
||||
assert data["note_type"] == "person"
|
||||
assert data["total_notes"] == 2
|
||||
mock_mcp.assert_called_once()
|
||||
assert mock_mcp.call_args.kwargs["output_format"] == "json"
|
||||
@@ -557,7 +557,7 @@ def test_schema_validate_error_response(mock_mcp):
|
||||
# --- schema-infer ---
|
||||
|
||||
SCHEMA_INFER_RESULT = {
|
||||
"entity_type": "person",
|
||||
"note_type": "person",
|
||||
"notes_analyzed": 5,
|
||||
"field_frequencies": [
|
||||
{"name": "name", "source": "observation", "count": 5, "total": 5, "percentage": 1.0},
|
||||
@@ -584,7 +584,7 @@ def test_schema_infer_json_output(mock_mcp):
|
||||
|
||||
assert result.exit_code == 0, f"CLI failed: {result.output}"
|
||||
data = json.loads(result.output)
|
||||
assert data["entity_type"] == "person"
|
||||
assert data["note_type"] == "person"
|
||||
assert data["notes_analyzed"] == 5
|
||||
mock_mcp.assert_called_once()
|
||||
assert mock_mcp.call_args.kwargs["output_format"] == "json"
|
||||
@@ -609,7 +609,7 @@ def test_schema_infer_threshold_passthrough(mock_mcp):
|
||||
# --- schema-diff ---
|
||||
|
||||
SCHEMA_DIFF_RESULT = {
|
||||
"entity_type": "person",
|
||||
"note_type": "person",
|
||||
"schema_found": True,
|
||||
"new_fields": [
|
||||
{"name": "email", "source": "observation", "count": 3, "total": 5, "percentage": 0.6}
|
||||
@@ -633,7 +633,7 @@ def test_schema_diff_json_output(mock_mcp):
|
||||
|
||||
assert result.exit_code == 0, f"CLI failed: {result.output}"
|
||||
data = json.loads(result.output)
|
||||
assert data["entity_type"] == "person"
|
||||
assert data["note_type"] == "person"
|
||||
assert data["schema_found"] is True
|
||||
assert len(data["new_fields"]) == 1
|
||||
mock_mcp.assert_called_once()
|
||||
|
||||
+7
-7
@@ -453,7 +453,7 @@ async def sample_entity(entity_repository: EntityRepository) -> Entity:
|
||||
entity_data = {
|
||||
"project_id": entity_repository.project_id,
|
||||
"title": "Test Entity",
|
||||
"entity_type": "test",
|
||||
"note_type": "test",
|
||||
"permalink": "test/test-entity",
|
||||
"file_path": "test/test_entity.md",
|
||||
"content_type": "text/markdown",
|
||||
@@ -481,7 +481,7 @@ async def full_entity(sample_entity, entity_repository, file_service, entity_ser
|
||||
EntitySchema(
|
||||
title="Search_Entity",
|
||||
directory="test",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
content=dedent("""
|
||||
## Observations
|
||||
- [tech] Tech note
|
||||
@@ -511,7 +511,7 @@ async def test_graph(
|
||||
deeper, _ = await entity_service.create_or_update_entity(
|
||||
EntitySchema(
|
||||
title="Deeper Entity",
|
||||
entity_type="deeper",
|
||||
note_type="deeper",
|
||||
directory="test",
|
||||
content=dedent("""
|
||||
# Deeper Entity
|
||||
@@ -522,7 +522,7 @@ async def test_graph(
|
||||
deep, _ = await entity_service.create_or_update_entity(
|
||||
EntitySchema(
|
||||
title="Deep Entity",
|
||||
entity_type="deep",
|
||||
note_type="deep",
|
||||
directory="test",
|
||||
content=dedent("""
|
||||
# Deep Entity
|
||||
@@ -534,7 +534,7 @@ async def test_graph(
|
||||
connected_2, _ = await entity_service.create_or_update_entity(
|
||||
EntitySchema(
|
||||
title="Connected Entity 2",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
directory="test",
|
||||
content=dedent("""
|
||||
# Connected Entity 2
|
||||
@@ -546,7 +546,7 @@ async def test_graph(
|
||||
connected_1, _ = await entity_service.create_or_update_entity(
|
||||
EntitySchema(
|
||||
title="Connected Entity 1",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
directory="test",
|
||||
content=dedent("""
|
||||
# Connected Entity 1
|
||||
@@ -559,7 +559,7 @@ async def test_graph(
|
||||
root, _ = await entity_service.create_or_update_entity(
|
||||
EntitySchema(
|
||||
title="Root",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
directory="test",
|
||||
content=dedent("""
|
||||
# Root Entity
|
||||
|
||||
@@ -65,7 +65,7 @@ async def test_issue_254_foreign_key_constraint_fix(project_service: ProjectServ
|
||||
# Create entity
|
||||
entity_data = {
|
||||
"title": "Issue 254 Test Entity",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"project_id": project.id,
|
||||
"permalink": "issue-254-entity",
|
||||
@@ -146,7 +146,7 @@ async def test_issue_254_reproduction(project_service: ProjectService):
|
||||
|
||||
entity_data = {
|
||||
"title": "Reproduction Entity",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"project_id": project.id,
|
||||
"permalink": "reproduction-entity",
|
||||
|
||||
@@ -79,12 +79,12 @@ async def test_parse_file_with_completely_invalid_yaml(tmp_path):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_file_without_entity_type(tmp_path):
|
||||
"""Test that files without entity_type get a default value (issue #184).
|
||||
async def test_parse_file_without_note_type(tmp_path):
|
||||
"""Test that files without note_type get a default value (issue #184).
|
||||
|
||||
This reproduces the NOT NULL constraint error where entity_type was missing.
|
||||
This reproduces the NOT NULL constraint error where note_type was missing.
|
||||
"""
|
||||
# Create a file without entity_type in frontmatter
|
||||
# Create a file without note_type in frontmatter
|
||||
test_file = tmp_path / "no_type.md"
|
||||
content = dedent(
|
||||
"""
|
||||
@@ -102,7 +102,7 @@ async def test_parse_file_without_entity_type(tmp_path):
|
||||
parser = EntityParser(tmp_path)
|
||||
result = await parser.parse_file(test_file)
|
||||
|
||||
# Should have default entity_type
|
||||
# Should have default note_type
|
||||
assert result is not None
|
||||
assert result.frontmatter.type == "note" # Default type applied
|
||||
assert result.frontmatter.title == "The Invisible Weight of Mental Habits"
|
||||
@@ -159,9 +159,9 @@ async def test_parse_file_without_frontmatter(tmp_path):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_file_with_null_entity_type(tmp_path):
|
||||
"""Test that files with explicit null entity_type get default (issue #184)."""
|
||||
# Create a file with null/None entity_type
|
||||
async def test_parse_file_with_null_note_type(tmp_path):
|
||||
"""Test that files with explicit null note_type get default (issue #184)."""
|
||||
# Create a file with null/None note_type
|
||||
test_file = tmp_path / "null_type.md"
|
||||
content = dedent(
|
||||
"""
|
||||
@@ -402,7 +402,7 @@ async def test_schema_to_markdown_empty_metadata_no_metadata_key():
|
||||
|
||||
schema = SimpleNamespace(
|
||||
title="Empty Metadata Test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink="empty-metadata-test",
|
||||
content="# Empty Metadata Test\n\nSome content.",
|
||||
entity_metadata={},
|
||||
|
||||
@@ -16,7 +16,7 @@ class TestEntityModelFromMarkdown:
|
||||
def _create_markdown(
|
||||
self,
|
||||
title: str = "Test Entity",
|
||||
entity_type: str = "note",
|
||||
note_type: str = "note",
|
||||
permalink: str = "test/test-entity",
|
||||
created: datetime | None = None,
|
||||
modified: datetime | None = None,
|
||||
@@ -27,7 +27,7 @@ class TestEntityModelFromMarkdown:
|
||||
return EntityMarkdown(
|
||||
frontmatter=EntityFrontmatter(
|
||||
title=title,
|
||||
type=entity_type,
|
||||
type=note_type,
|
||||
permalink=permalink,
|
||||
),
|
||||
content=f"# {title}\n\nTest content.",
|
||||
@@ -127,7 +127,7 @@ class TestEntityModelFromMarkdown:
|
||||
"""
|
||||
markdown = self._create_markdown(
|
||||
title="My Test Entity",
|
||||
entity_type="component",
|
||||
note_type="component",
|
||||
permalink="components/my-test",
|
||||
)
|
||||
file_path = Path("components/my-test.md")
|
||||
|
||||
@@ -34,7 +34,7 @@ class TestKnowledgeClient:
|
||||
"permalink": "test",
|
||||
"title": "Test",
|
||||
"file_path": "test.md",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"observations": [],
|
||||
"relations": [],
|
||||
|
||||
@@ -43,7 +43,7 @@ def test_entity_data():
|
||||
"entities": [
|
||||
{
|
||||
"title": "Test Entity",
|
||||
"entity_type": "test",
|
||||
"note_type": "test",
|
||||
"summary": "", # Empty string instead of None
|
||||
}
|
||||
]
|
||||
|
||||
@@ -45,9 +45,9 @@ class TestSchemaClientValidate:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_no_params(self, schema_client, monkeypatch):
|
||||
"""Validate with no entity_type or identifier sends empty params."""
|
||||
"""Validate with no note_type or identifier sends empty params."""
|
||||
report_data = {
|
||||
"entity_type": None,
|
||||
"note_type": None,
|
||||
"total_notes": 0,
|
||||
"valid_count": 0,
|
||||
"warning_count": 0,
|
||||
@@ -70,10 +70,10 @@ class TestSchemaClientValidate:
|
||||
assert result.total_notes == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_with_entity_type(self, schema_client, monkeypatch):
|
||||
"""Validate sends entity_type as query param."""
|
||||
async def test_validate_with_note_type(self, schema_client, monkeypatch):
|
||||
"""Validate sends note_type as query param."""
|
||||
report_data = {
|
||||
"entity_type": "person",
|
||||
"note_type": "person",
|
||||
"total_notes": 5,
|
||||
"valid_count": 4,
|
||||
"warning_count": 1,
|
||||
@@ -85,20 +85,20 @@ class TestSchemaClientValidate:
|
||||
mock_response = Response(200, json=report_data, request=request)
|
||||
|
||||
async def mock_call_post(client, url, **kwargs):
|
||||
assert kwargs["params"]["entity_type"] == "person"
|
||||
assert kwargs["params"]["note_type"] == "person"
|
||||
return mock_response
|
||||
|
||||
monkeypatch.setattr("basic_memory.mcp.clients.schema.call_post", mock_call_post)
|
||||
|
||||
result = await schema_client.validate(entity_type="person")
|
||||
assert result.entity_type == "person"
|
||||
result = await schema_client.validate(note_type="person")
|
||||
assert result.note_type == "person"
|
||||
assert result.total_notes == 5
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_with_identifier(self, schema_client, monkeypatch):
|
||||
"""Validate sends identifier as query param."""
|
||||
report_data = {
|
||||
"entity_type": None,
|
||||
"note_type": None,
|
||||
"total_notes": 1,
|
||||
"valid_count": 1,
|
||||
"warning_count": 0,
|
||||
@@ -124,9 +124,9 @@ class TestSchemaClientInfer:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_infer_default_threshold(self, schema_client, monkeypatch):
|
||||
"""Infer sends entity_type and default threshold."""
|
||||
"""Infer sends note_type and default threshold."""
|
||||
report_data = {
|
||||
"entity_type": "person",
|
||||
"note_type": "person",
|
||||
"notes_analyzed": 10,
|
||||
"field_frequencies": [],
|
||||
"suggested_schema": {},
|
||||
@@ -140,7 +140,7 @@ class TestSchemaClientInfer:
|
||||
|
||||
async def mock_call_post(client, url, **kwargs):
|
||||
assert url == "/v2/projects/test-project-id/schema/infer"
|
||||
assert kwargs["params"]["entity_type"] == "person"
|
||||
assert kwargs["params"]["note_type"] == "person"
|
||||
assert kwargs["params"]["threshold"] == 0.25
|
||||
return mock_response
|
||||
|
||||
@@ -155,7 +155,7 @@ class TestSchemaClientInfer:
|
||||
async def test_infer_custom_threshold(self, schema_client, monkeypatch):
|
||||
"""Infer passes custom threshold."""
|
||||
report_data = {
|
||||
"entity_type": "meeting",
|
||||
"note_type": "meeting",
|
||||
"notes_analyzed": 5,
|
||||
"field_frequencies": [],
|
||||
"suggested_schema": {},
|
||||
@@ -174,7 +174,7 @@ class TestSchemaClientInfer:
|
||||
monkeypatch.setattr("basic_memory.mcp.clients.schema.call_post", mock_call_post)
|
||||
|
||||
result = await schema_client.infer("meeting", threshold=0.5)
|
||||
assert result.entity_type == "meeting"
|
||||
assert result.note_type == "meeting"
|
||||
|
||||
|
||||
class TestSchemaClientDiff:
|
||||
@@ -182,9 +182,9 @@ class TestSchemaClientDiff:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_diff(self, schema_client, monkeypatch):
|
||||
"""Diff calls GET with entity_type in path."""
|
||||
"""Diff calls GET with note_type in path."""
|
||||
report_data = {
|
||||
"entity_type": "person",
|
||||
"note_type": "person",
|
||||
"new_fields": [],
|
||||
"dropped_fields": [],
|
||||
"cardinality_changes": [],
|
||||
@@ -201,13 +201,13 @@ class TestSchemaClientDiff:
|
||||
|
||||
result = await schema_client.diff("person")
|
||||
assert isinstance(result, DriftReport)
|
||||
assert result.entity_type == "person"
|
||||
assert result.note_type == "person"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_diff_with_drift(self, schema_client, monkeypatch):
|
||||
"""Diff returns populated drift report."""
|
||||
report_data = {
|
||||
"entity_type": "person",
|
||||
"note_type": "person",
|
||||
"new_fields": [
|
||||
{
|
||||
"name": "role",
|
||||
|
||||
@@ -82,7 +82,7 @@ EXPECTED_TOOL_SIGNATURES: dict[str, list[str]] = {
|
||||
"page_size",
|
||||
"search_type",
|
||||
"output_format",
|
||||
"types",
|
||||
"note_types",
|
||||
"entity_types",
|
||||
"after_date",
|
||||
"metadata_filters",
|
||||
|
||||
@@ -129,7 +129,7 @@ async def test_schema_infer(app, test_project, sync_service):
|
||||
)
|
||||
|
||||
assert isinstance(result, InferenceReport)
|
||||
assert result.entity_type == "person"
|
||||
assert result.note_type == "person"
|
||||
assert result.notes_analyzed >= 3
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ permalink: people/dave
|
||||
)
|
||||
|
||||
assert isinstance(result, DriftReport)
|
||||
assert result.entity_type == "person"
|
||||
assert result.note_type == "person"
|
||||
|
||||
|
||||
# --- write_note metadata → schema workflow ---
|
||||
|
||||
@@ -186,17 +186,17 @@ async def test_search_pagination(client, test_project):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_with_type_filter(client, test_project):
|
||||
"""Test search with entity type filter."""
|
||||
"""Test search with note type filter."""
|
||||
# Create test content
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Entity Type Test",
|
||||
title="Note Type Test",
|
||||
directory="test",
|
||||
content="# Test\nFiltered by type",
|
||||
)
|
||||
|
||||
# Search with type filter
|
||||
response = await search_notes(project=test_project.name, query="type", types=["note"])
|
||||
# Search with note type filter
|
||||
response = await search_notes(project=test_project.name, query="type", note_types=["note"])
|
||||
|
||||
# Verify results - handle both success and error cases
|
||||
if isinstance(response, SearchResponse):
|
||||
@@ -209,7 +209,7 @@ async def test_search_with_type_filter(client, test_project):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_with_entity_type_filter(client, test_project):
|
||||
"""Test search with entity type filter."""
|
||||
"""Test search with entity_types (SearchItemType) filter."""
|
||||
# Create test content
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
@@ -218,7 +218,7 @@ async def test_search_with_entity_type_filter(client, test_project):
|
||||
content="# Test\nFiltered by type",
|
||||
)
|
||||
|
||||
# Search with entity type filter
|
||||
# Search with entity_types (SearchItemType) filter
|
||||
response = await search_notes(project=test_project.name, query="type", entity_types=["entity"])
|
||||
|
||||
# Verify results - handle both success and error cases
|
||||
|
||||
@@ -533,10 +533,10 @@ async def test_write_note_permalink_collision_fix_issue_139(app, test_project):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_note_with_custom_entity_type(app, test_project):
|
||||
"""Test creating a note with custom entity_type parameter.
|
||||
async def test_write_note_with_custom_note_type(app, test_project):
|
||||
"""Test creating a note with custom note_type parameter.
|
||||
|
||||
This test verifies the fix for Issue #144 where entity_type parameter
|
||||
This test verifies the fix for Issue #144 where note_type parameter
|
||||
was hardcoded to "note" instead of allowing custom types.
|
||||
"""
|
||||
result = await write_note(
|
||||
@@ -557,7 +557,7 @@ async def test_write_note_with_custom_entity_type(app, test_project):
|
||||
assert "- guide, documentation" in result
|
||||
assert f"[Session: Using project '{test_project.name}']" in result
|
||||
|
||||
# Verify the entity type is correctly set in the frontmatter
|
||||
# Verify the note type is correctly set in the frontmatter
|
||||
content = await read_note("guides/test-guide", project=test_project.name)
|
||||
expected = normalize_newlines(
|
||||
dedent("""
|
||||
@@ -580,7 +580,7 @@ async def test_write_note_with_custom_entity_type(app, test_project):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_note_with_report_entity_type(app, test_project):
|
||||
async def test_write_note_with_report_note_type(app, test_project):
|
||||
"""Test creating a note with note_type="report"."""
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
@@ -598,14 +598,14 @@ async def test_write_note_with_report_entity_type(app, test_project):
|
||||
assert f"permalink: {test_project.name}/reports/monthly-report" in result
|
||||
assert f"[Session: Using project '{test_project.name}']" in result
|
||||
|
||||
# Verify the entity type is correctly set in the frontmatter
|
||||
# Verify the note type is correctly set in the frontmatter
|
||||
content = await read_note("reports/monthly-report", project=test_project.name)
|
||||
assert "type: report" in content
|
||||
assert "# Monthly Report" in content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_note_with_config_entity_type(app, test_project):
|
||||
async def test_write_note_with_config_note_type(app, test_project):
|
||||
"""Test creating a note with note_type="config"."""
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
@@ -622,18 +622,18 @@ async def test_write_note_with_config_entity_type(app, test_project):
|
||||
assert f"permalink: {test_project.name}/config/system-config" in result
|
||||
assert f"[Session: Using project '{test_project.name}']" in result
|
||||
|
||||
# Verify the entity type is correctly set in the frontmatter
|
||||
# Verify the note type is correctly set in the frontmatter
|
||||
content = await read_note("config/system-config", project=test_project.name)
|
||||
assert "type: config" in content
|
||||
assert "# System Configuration" in content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_note_entity_type_default_behavior(app, test_project):
|
||||
"""Test that the entity_type parameter defaults to "note" when not specified.
|
||||
async def test_write_note_note_type_default_behavior(app, test_project):
|
||||
"""Test that the note_type parameter defaults to "note" when not specified.
|
||||
|
||||
This ensures backward compatibility - existing code that doesn't specify
|
||||
entity_type should continue to work as before.
|
||||
note_type should continue to work as before.
|
||||
"""
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
@@ -650,15 +650,15 @@ async def test_write_note_entity_type_default_behavior(app, test_project):
|
||||
assert f"permalink: {test_project.name}/test/default-type-test" in result
|
||||
assert f"[Session: Using project '{test_project.name}']" in result
|
||||
|
||||
# Verify the entity type defaults to "note"
|
||||
# Verify the note type defaults to "note"
|
||||
content = await read_note("test/default-type-test", project=test_project.name)
|
||||
assert "type: note" in content
|
||||
assert "# Default Type Test" in content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_note_update_existing_with_different_entity_type(app, test_project):
|
||||
"""Test updating an existing note with a different entity_type."""
|
||||
async def test_write_note_update_existing_with_different_note_type(app, test_project):
|
||||
"""Test updating an existing note with a different note_type."""
|
||||
# Create initial note as "note" type
|
||||
result1 = await write_note(
|
||||
project=test_project.name,
|
||||
@@ -687,7 +687,7 @@ async def test_write_note_update_existing_with_different_entity_type(app, test_p
|
||||
assert "# Updated note" in result2
|
||||
assert f"project: {test_project.name}" in result2
|
||||
|
||||
# Verify the entity type was updated
|
||||
# Verify the note type was updated
|
||||
content = await read_note("test/changeable-type", project=test_project.name)
|
||||
assert "type: guide" in content
|
||||
assert "# Updated Content" in content
|
||||
@@ -695,10 +695,10 @@ async def test_write_note_update_existing_with_different_entity_type(app, test_p
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_note_respects_frontmatter_entity_type(app, test_project):
|
||||
"""Test that entity_type in frontmatter is respected when parameter is not provided.
|
||||
async def test_write_note_respects_frontmatter_note_type(app, test_project):
|
||||
"""Test that note_type in frontmatter is respected when parameter is not provided.
|
||||
|
||||
This verifies that when write_note is called without entity_type parameter,
|
||||
This verifies that when write_note is called without note_type parameter,
|
||||
but the content includes frontmatter with a 'type' field, that type is respected
|
||||
instead of defaulting to 'note'.
|
||||
"""
|
||||
@@ -716,7 +716,7 @@ async def test_write_note_respects_frontmatter_entity_type(app, test_project):
|
||||
This is a guide
|
||||
""").strip()
|
||||
|
||||
# Call write_note without entity_type parameter - it should respect frontmatter type
|
||||
# Call write_note without note_type parameter - it should respect frontmatter type
|
||||
result = await write_note(
|
||||
project=test_project.name, title="Test Guide", directory="guides", content=note
|
||||
)
|
||||
@@ -728,7 +728,7 @@ async def test_write_note_respects_frontmatter_entity_type(app, test_project):
|
||||
assert "permalink: guides/test-guide" in result
|
||||
assert f"[Session: Using project '{test_project.name}']" in result
|
||||
|
||||
# Verify the entity type from frontmatter is respected (should be "guide", not "note")
|
||||
# Verify the note type from frontmatter is respected (should be "guide", not "note")
|
||||
content = await read_note("guides/test-guide", project=test_project.name)
|
||||
assert "type: guide" in content
|
||||
assert "# Guide Content" in content
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Tests for the write_note `metadata` parameter.
|
||||
|
||||
Covers positive, negative, and edge-case scenarios for passing arbitrary
|
||||
frontmatter fields through entity_metadata.
|
||||
frontmatter fields through note_metadata.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
@@ -192,7 +192,7 @@ async def test_metadata_empty_dict(app, test_project):
|
||||
async def test_metadata_title_key_stripped(app, test_project):
|
||||
"""A 'title' key in metadata does not override the title parameter.
|
||||
|
||||
schema_to_markdown pops 'title' from entity_metadata so the Entity.title wins.
|
||||
schema_to_markdown pops 'title' from note_metadata so the Entity.title wins.
|
||||
"""
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
@@ -213,7 +213,7 @@ async def test_metadata_title_key_stripped(app, test_project):
|
||||
async def test_metadata_type_key_stripped(app, test_project):
|
||||
"""A 'type' key in metadata does not override note_type parameter.
|
||||
|
||||
schema_to_markdown pops 'type' from entity_metadata so Entity.entity_type wins.
|
||||
schema_to_markdown pops 'type' from note_metadata so Entity.note_type wins.
|
||||
"""
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
@@ -235,7 +235,7 @@ async def test_metadata_type_key_stripped(app, test_project):
|
||||
async def test_metadata_permalink_key_stripped(app, test_project):
|
||||
"""A 'permalink' key in metadata does not hijack the canonical permalink.
|
||||
|
||||
schema_to_markdown pops 'permalink' from entity_metadata.
|
||||
schema_to_markdown pops 'permalink' from note_metadata.
|
||||
"""
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
|
||||
@@ -39,7 +39,7 @@ async def related_results(session_maker, test_project: Project):
|
||||
source = Entity(
|
||||
project_id=test_project.id,
|
||||
title="source",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="source/source",
|
||||
file_path="source/source.md",
|
||||
content_type="text/markdown",
|
||||
@@ -49,7 +49,7 @@ async def related_results(session_maker, test_project: Project):
|
||||
target = Entity(
|
||||
project_id=test_project.id,
|
||||
title="target",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="target/target",
|
||||
file_path="target/target.md",
|
||||
content_type="text/markdown",
|
||||
@@ -78,7 +78,7 @@ async def test_create_entity(entity_repository: EntityRepository):
|
||||
entity_data = {
|
||||
"project_id": entity_repository.project_id,
|
||||
"title": "Test",
|
||||
"entity_type": "test",
|
||||
"note_type": "test",
|
||||
"permalink": "test/test",
|
||||
"file_path": "test/test.md",
|
||||
"content_type": "text/markdown",
|
||||
@@ -112,7 +112,7 @@ async def test_create_all(entity_repository: EntityRepository):
|
||||
{
|
||||
"project_id": entity_repository.project_id,
|
||||
"title": "Test_1",
|
||||
"entity_type": "test",
|
||||
"note_type": "test",
|
||||
"permalink": "test/test-1",
|
||||
"file_path": "test/test_1.md",
|
||||
"content_type": "text/markdown",
|
||||
@@ -122,7 +122,7 @@ async def test_create_all(entity_repository: EntityRepository):
|
||||
{
|
||||
"project_id": entity_repository.project_id,
|
||||
"title": "Test-2",
|
||||
"entity_type": "test",
|
||||
"note_type": "test",
|
||||
"permalink": "test/test-2",
|
||||
"file_path": "test/test_2.md",
|
||||
"content_type": "text/markdown",
|
||||
@@ -191,7 +191,7 @@ async def test_update_entity_returns_with_relations_and_observations(
|
||||
target = Entity(
|
||||
project_id=test_project.id,
|
||||
title="target",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="target/target",
|
||||
file_path="target/target.md",
|
||||
content_type="text/markdown",
|
||||
@@ -264,12 +264,12 @@ async def test_delete_entity_with_observations(
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_entities_by_type(entity_repository: EntityRepository, sample_entity):
|
||||
"""Test deleting entities by type."""
|
||||
result = await entity_repository.delete_by_fields(entity_type=sample_entity.entity_type)
|
||||
result = await entity_repository.delete_by_fields(note_type=sample_entity.note_type)
|
||||
assert result is True
|
||||
|
||||
# Verify deletion
|
||||
async with db.scoped_session(entity_repository.session_maker) as session:
|
||||
query = select(Entity).filter(Entity.entity_type == sample_entity.entity_type)
|
||||
query = select(Entity).filter(Entity.note_type == sample_entity.note_type)
|
||||
result = await session.execute(query)
|
||||
remaining = result.scalars().all()
|
||||
assert len(remaining) == 0
|
||||
@@ -311,7 +311,7 @@ async def test_entities(session_maker, test_project: Project):
|
||||
Entity(
|
||||
project_id=test_project.id,
|
||||
title="entity1",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="type1/entity1",
|
||||
file_path="type1/entity1.md",
|
||||
content_type="text/markdown",
|
||||
@@ -321,7 +321,7 @@ async def test_entities(session_maker, test_project: Project):
|
||||
Entity(
|
||||
project_id=test_project.id,
|
||||
title="entity2",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="type1/entity2",
|
||||
file_path="type1/entity2.md",
|
||||
content_type="text/markdown",
|
||||
@@ -331,7 +331,7 @@ async def test_entities(session_maker, test_project: Project):
|
||||
Entity(
|
||||
project_id=test_project.id,
|
||||
title="entity3",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="type2/entity3",
|
||||
file_path="type2/entity3.md",
|
||||
content_type="text/markdown",
|
||||
@@ -390,7 +390,7 @@ async def test_generate_permalink_from_file_path():
|
||||
# Verify the result passes validation
|
||||
Entity(
|
||||
title="test",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink=result,
|
||||
file_path=input_path,
|
||||
content_type="text/markdown",
|
||||
@@ -406,7 +406,7 @@ async def test_get_by_title(entity_repository: EntityRepository, session_maker):
|
||||
Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Unique Title",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/unique-title",
|
||||
file_path="test/unique-title.md",
|
||||
content_type="text/markdown",
|
||||
@@ -416,7 +416,7 @@ async def test_get_by_title(entity_repository: EntityRepository, session_maker):
|
||||
Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Another Title",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/another-title",
|
||||
file_path="test/another-title.md",
|
||||
content_type="text/markdown",
|
||||
@@ -426,7 +426,7 @@ async def test_get_by_title(entity_repository: EntityRepository, session_maker):
|
||||
Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Another Title",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/another-title-1",
|
||||
file_path="test/another-title-1.md",
|
||||
content_type="text/markdown",
|
||||
@@ -473,7 +473,7 @@ async def test_get_by_title_returns_shortest_path_first(
|
||||
Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="My Note",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink="archive/old/2024/my-note",
|
||||
file_path="archive/old/2024/My Note.md", # longest path
|
||||
content_type="text/markdown",
|
||||
@@ -483,7 +483,7 @@ async def test_get_by_title_returns_shortest_path_first(
|
||||
Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="My Note",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink="docs/my-note",
|
||||
file_path="docs/My Note.md", # medium path
|
||||
content_type="text/markdown",
|
||||
@@ -493,7 +493,7 @@ async def test_get_by_title_returns_shortest_path_first(
|
||||
Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="My Note",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink="my-note",
|
||||
file_path="My Note.md", # shortest path (root)
|
||||
content_type="text/markdown",
|
||||
@@ -525,7 +525,7 @@ async def test_get_by_file_path(entity_repository: EntityRepository, session_mak
|
||||
Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Unique Title",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/unique-title",
|
||||
file_path="test/unique-title.md",
|
||||
content_type="text/markdown",
|
||||
@@ -555,7 +555,7 @@ async def test_get_distinct_directories(entity_repository: EntityRepository, ses
|
||||
Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="File 1",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="docs/guides/file1",
|
||||
file_path="docs/guides/file1.md",
|
||||
content_type="text/markdown",
|
||||
@@ -565,7 +565,7 @@ async def test_get_distinct_directories(entity_repository: EntityRepository, ses
|
||||
Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="File 2",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="docs/guides/file2",
|
||||
file_path="docs/guides/file2.md",
|
||||
content_type="text/markdown",
|
||||
@@ -575,7 +575,7 @@ async def test_get_distinct_directories(entity_repository: EntityRepository, ses
|
||||
Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="File 3",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="docs/api/file3",
|
||||
file_path="docs/api/file3.md",
|
||||
content_type="text/markdown",
|
||||
@@ -585,7 +585,7 @@ async def test_get_distinct_directories(entity_repository: EntityRepository, ses
|
||||
Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="File 4",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="specs/file4",
|
||||
file_path="specs/file4.md",
|
||||
content_type="text/markdown",
|
||||
@@ -595,7 +595,7 @@ async def test_get_distinct_directories(entity_repository: EntityRepository, ses
|
||||
Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="File 5",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="notes/2024/q1/file5",
|
||||
file_path="notes/2024/q1/file5.md",
|
||||
content_type="text/markdown",
|
||||
@@ -649,7 +649,7 @@ async def test_find_by_directory_prefix(entity_repository: EntityRepository, ses
|
||||
Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="File 1",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="docs/file1",
|
||||
file_path="docs/file1.md",
|
||||
content_type="text/markdown",
|
||||
@@ -659,7 +659,7 @@ async def test_find_by_directory_prefix(entity_repository: EntityRepository, ses
|
||||
Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="File 2",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="docs/guides/file2",
|
||||
file_path="docs/guides/file2.md",
|
||||
content_type="text/markdown",
|
||||
@@ -669,7 +669,7 @@ async def test_find_by_directory_prefix(entity_repository: EntityRepository, ses
|
||||
Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="File 3",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="docs/api/file3",
|
||||
file_path="docs/api/file3.md",
|
||||
content_type="text/markdown",
|
||||
@@ -679,7 +679,7 @@ async def test_find_by_directory_prefix(entity_repository: EntityRepository, ses
|
||||
Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="File 4",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="specs/file4",
|
||||
file_path="specs/file4.md",
|
||||
content_type="text/markdown",
|
||||
@@ -734,7 +734,7 @@ async def test_find_by_directory_prefix_basic_fields_only(
|
||||
entity = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Test Entity",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="docs/test",
|
||||
file_path="docs/test.md",
|
||||
content_type="text/markdown",
|
||||
@@ -753,7 +753,7 @@ async def test_find_by_directory_prefix_basic_fields_only(
|
||||
assert entity.title == "Test Entity"
|
||||
assert entity.file_path == "docs/test.md"
|
||||
assert entity.permalink == "docs/test"
|
||||
assert entity.entity_type == "test"
|
||||
assert entity.note_type == "test"
|
||||
assert entity.content_type == "text/markdown"
|
||||
assert entity.updated_at is not None
|
||||
|
||||
@@ -767,7 +767,7 @@ async def test_get_all_file_paths(entity_repository: EntityRepository, session_m
|
||||
Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="File 1",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="docs/file1",
|
||||
file_path="docs/file1.md",
|
||||
content_type="text/markdown",
|
||||
@@ -777,7 +777,7 @@ async def test_get_all_file_paths(entity_repository: EntityRepository, session_m
|
||||
Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="File 2",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="specs/file2",
|
||||
file_path="specs/file2.md",
|
||||
content_type="text/markdown",
|
||||
@@ -787,7 +787,7 @@ async def test_get_all_file_paths(entity_repository: EntityRepository, session_m
|
||||
Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="File 3",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="notes/file3",
|
||||
file_path="notes/file3.md",
|
||||
content_type="text/markdown",
|
||||
@@ -827,7 +827,7 @@ async def test_get_all_file_paths_performance(entity_repository: EntityRepositor
|
||||
entity1 = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Entity 1",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/entity1",
|
||||
file_path="test/entity1.md",
|
||||
content_type="text/markdown",
|
||||
@@ -837,7 +837,7 @@ async def test_get_all_file_paths_performance(entity_repository: EntityRepositor
|
||||
entity2 = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Entity 2",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/entity2",
|
||||
file_path="test/entity2.md",
|
||||
content_type="text/markdown",
|
||||
@@ -889,7 +889,7 @@ async def test_get_all_file_paths_project_isolation(
|
||||
entity1 = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Project 1 File",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/file1",
|
||||
file_path="test/file1.md",
|
||||
content_type="text/markdown",
|
||||
@@ -908,7 +908,7 @@ async def test_get_all_file_paths_project_isolation(
|
||||
entity2 = Entity(
|
||||
project_id=project2.id,
|
||||
title="Project 2 File",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/file2",
|
||||
file_path="test/file2.md",
|
||||
content_type="text/markdown",
|
||||
@@ -951,7 +951,7 @@ async def test_permalink_exists_project_isolation(
|
||||
entity1 = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Project 1 Entity",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/entity1",
|
||||
file_path="test/entity1.md",
|
||||
content_type="text/markdown",
|
||||
@@ -968,7 +968,7 @@ async def test_permalink_exists_project_isolation(
|
||||
entity2 = Entity(
|
||||
project_id=project2.id,
|
||||
title="Project 2 Entity",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/entity2",
|
||||
file_path="test/entity2.md",
|
||||
content_type="text/markdown",
|
||||
@@ -1019,7 +1019,7 @@ async def test_get_all_permalinks(entity_repository: EntityRepository, session_m
|
||||
entity1 = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Entity 1",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/entity1",
|
||||
file_path="test/entity1.md",
|
||||
content_type="text/markdown",
|
||||
@@ -1029,7 +1029,7 @@ async def test_get_all_permalinks(entity_repository: EntityRepository, session_m
|
||||
entity2 = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Entity 2",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/entity2",
|
||||
file_path="test/entity2.md",
|
||||
content_type="text/markdown",
|
||||
@@ -1055,7 +1055,7 @@ async def test_get_permalink_to_file_path_map(entity_repository: EntityRepositor
|
||||
entity1 = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Entity 1",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/entity1",
|
||||
file_path="test/entity1.md",
|
||||
content_type="text/markdown",
|
||||
@@ -1065,7 +1065,7 @@ async def test_get_permalink_to_file_path_map(entity_repository: EntityRepositor
|
||||
entity2 = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Entity 2",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/entity2",
|
||||
file_path="test/entity2.md",
|
||||
content_type="text/markdown",
|
||||
@@ -1088,7 +1088,7 @@ async def test_get_file_path_to_permalink_map(entity_repository: EntityRepositor
|
||||
entity1 = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Entity 1",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/entity1",
|
||||
file_path="test/entity1.md",
|
||||
content_type="text/markdown",
|
||||
@@ -1098,7 +1098,7 @@ async def test_get_file_path_to_permalink_map(entity_repository: EntityRepositor
|
||||
entity2 = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Entity 2",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/entity2",
|
||||
file_path="test/entity2.md",
|
||||
content_type="text/markdown",
|
||||
|
||||
@@ -15,7 +15,7 @@ async def test_upsert_entity_new_entity(entity_repository: EntityRepository):
|
||||
entity = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Test Entity",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink="test/test-entity",
|
||||
file_path="test/test-entity.md",
|
||||
content_type="text/markdown",
|
||||
@@ -38,7 +38,7 @@ async def test_upsert_entity_same_file_update(entity_repository: EntityRepositor
|
||||
entity1 = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Original Title",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink="test/test-entity",
|
||||
file_path="test/test-entity.md",
|
||||
content_type="text/markdown",
|
||||
@@ -53,7 +53,7 @@ async def test_upsert_entity_same_file_update(entity_repository: EntityRepositor
|
||||
entity2 = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Updated Title",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink="test/test-entity", # Same permalink
|
||||
file_path="test/test-entity.md", # Same file_path
|
||||
content_type="text/markdown",
|
||||
@@ -77,7 +77,7 @@ async def test_upsert_entity_permalink_conflict_different_file(entity_repository
|
||||
entity1 = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="First Entity",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink="test/shared-permalink",
|
||||
file_path="test/first-file.md",
|
||||
content_type="text/markdown",
|
||||
@@ -92,7 +92,7 @@ async def test_upsert_entity_permalink_conflict_different_file(entity_repository
|
||||
entity2 = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Second Entity",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink="test/shared-permalink", # Same permalink
|
||||
file_path="test/second-file.md", # Different file_path
|
||||
content_type="text/markdown",
|
||||
@@ -126,7 +126,7 @@ async def test_upsert_entity_multiple_permalink_conflicts(entity_repository: Ent
|
||||
entity = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title=f"Entity {i + 1}",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink=base_permalink, # All try to use same permalink
|
||||
file_path=f"test/file-{i + 1}.md", # Different file paths
|
||||
content_type="text/markdown",
|
||||
@@ -160,7 +160,7 @@ async def test_upsert_entity_race_condition_file_path(entity_repository: EntityR
|
||||
entity1 = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Original Entity",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink="test/original",
|
||||
file_path="test/race-file.md",
|
||||
content_type="text/markdown",
|
||||
@@ -176,7 +176,7 @@ async def test_upsert_entity_race_condition_file_path(entity_repository: EntityR
|
||||
entity2 = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Race Condition Test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink="test/race-entity",
|
||||
file_path="test/race-file.md", # Same file path as entity1
|
||||
content_type="text/markdown",
|
||||
@@ -208,7 +208,7 @@ async def test_upsert_entity_gap_in_suffixes(entity_repository: EntityRepository
|
||||
entity = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title=f"Entity {i + 1}",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink=permalink,
|
||||
file_path=f"test/gap-file-{i + 1}.md",
|
||||
content_type="text/markdown",
|
||||
@@ -221,7 +221,7 @@ async def test_upsert_entity_gap_in_suffixes(entity_repository: EntityRepository
|
||||
new_entity = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Gap Filler",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink=base_permalink, # Will conflict
|
||||
file_path="test/gap-new-file.md",
|
||||
content_type="text/markdown",
|
||||
@@ -275,7 +275,7 @@ async def test_upsert_entity_project_scoping_isolation(session_maker):
|
||||
entity1 = Entity(
|
||||
project_id=project1.id,
|
||||
title="Shared Entity",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink="docs/shared-name",
|
||||
file_path="docs/shared-name.md",
|
||||
content_type="text/markdown",
|
||||
@@ -286,7 +286,7 @@ async def test_upsert_entity_project_scoping_isolation(session_maker):
|
||||
entity2 = Entity(
|
||||
project_id=project2.id,
|
||||
title="Shared Entity",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink="docs/shared-name", # Same permalink
|
||||
file_path="docs/shared-name.md", # Same file_path
|
||||
content_type="text/markdown",
|
||||
@@ -311,7 +311,7 @@ async def test_upsert_entity_project_scoping_isolation(session_maker):
|
||||
entity1_update = Entity(
|
||||
project_id=project1.id,
|
||||
title="Updated Shared Entity",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink="docs/shared-name",
|
||||
file_path="docs/shared-name.md",
|
||||
content_type="text/markdown",
|
||||
@@ -322,7 +322,7 @@ async def test_upsert_entity_project_scoping_isolation(session_maker):
|
||||
entity2_update = Entity(
|
||||
project_id=project2.id,
|
||||
title="Also Updated Shared Entity",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink="docs/shared-name",
|
||||
file_path="docs/shared-name.md",
|
||||
content_type="text/markdown",
|
||||
@@ -389,7 +389,7 @@ async def test_upsert_entity_permalink_conflict_within_project_only(session_make
|
||||
entity1 = Entity(
|
||||
project_id=project1.id,
|
||||
title="Original Entity",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink="test/conflict-permalink",
|
||||
file_path="test/original.md",
|
||||
content_type="text/markdown",
|
||||
@@ -404,7 +404,7 @@ async def test_upsert_entity_permalink_conflict_within_project_only(session_make
|
||||
entity2 = Entity(
|
||||
project_id=project2.id,
|
||||
title="Cross-Project Entity",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink="test/conflict-permalink", # Same permalink, different project
|
||||
file_path="test/cross-project.md",
|
||||
content_type="text/markdown",
|
||||
@@ -420,7 +420,7 @@ async def test_upsert_entity_permalink_conflict_within_project_only(session_make
|
||||
entity3 = Entity(
|
||||
project_id=project1.id,
|
||||
title="Conflict Entity",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink="test/conflict-permalink", # Same permalink, same project
|
||||
file_path="test/conflict.md",
|
||||
content_type="text/markdown",
|
||||
@@ -449,7 +449,7 @@ async def test_upsert_entity_with_invalid_project_id(entity_repository: EntityRe
|
||||
# Create entity with non-existent project_id
|
||||
entity = Entity(
|
||||
title="Test Entity",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
file_path="test.md",
|
||||
permalink="test",
|
||||
project_id=99999, # This project doesn't exist
|
||||
|
||||
@@ -18,7 +18,7 @@ async def test_upsert_entity_with_observations_conflict(entity_repository: Entit
|
||||
entity1 = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Original Title",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink="debugging/backup-system/coderabbit-feedback-resolution",
|
||||
file_path="debugging/backup-system/CodeRabbit Feedback Resolution - Backup System Issues.md",
|
||||
content_type="text/markdown",
|
||||
@@ -47,7 +47,7 @@ async def test_upsert_entity_with_observations_conflict(entity_repository: Entit
|
||||
entity2 = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Updated Title",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink="debugging/backup-system/coderabbit-feedback-resolution", # Same permalink
|
||||
file_path="debugging/backup-system/CodeRabbit Feedback Resolution - Backup System Issues.md", # Same file_path
|
||||
content_type="text/markdown",
|
||||
@@ -99,7 +99,7 @@ async def test_upsert_entity_repeated_sync_same_file(entity_repository: EntityRe
|
||||
entity1 = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Complete Process for Uploading New Training Videos",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink=permalink,
|
||||
file_path=file_path,
|
||||
content_type="text/markdown",
|
||||
@@ -116,7 +116,7 @@ async def test_upsert_entity_repeated_sync_same_file(entity_repository: EntityRe
|
||||
entity_new = Entity(
|
||||
project_id=entity_repository.project_id,
|
||||
title="Complete Process for Uploading New Training Videos",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink=permalink,
|
||||
file_path=file_path,
|
||||
content_type="text/markdown",
|
||||
|
||||
@@ -86,7 +86,7 @@ HYBRID_KWARGS = dict(
|
||||
permalink=None,
|
||||
permalink_match=None,
|
||||
title=None,
|
||||
types=None,
|
||||
note_types=None,
|
||||
after_date=None,
|
||||
search_item_types=None,
|
||||
metadata_filters=None,
|
||||
|
||||
@@ -26,7 +26,7 @@ async def _index_entity_with_metadata(search_repository, session_maker, title, e
|
||||
entity = Entity(
|
||||
project_id=search_repository.project_id,
|
||||
title=title,
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink=permalink,
|
||||
file_path=file_path,
|
||||
content_type="text/markdown",
|
||||
@@ -46,7 +46,7 @@ async def _index_entity_with_metadata(search_repository, session_maker, title, e
|
||||
permalink=entity.permalink,
|
||||
file_path=entity.file_path,
|
||||
entity_id=entity.id,
|
||||
metadata={"entity_type": entity.entity_type},
|
||||
metadata={"note_type": entity.note_type},
|
||||
created_at=entity.created_at,
|
||||
updated_at=entity.updated_at,
|
||||
project_id=search_repository.project_id,
|
||||
|
||||
@@ -95,7 +95,7 @@ async def test_delete_observations(session_maker: async_sessionmaker, repo, test
|
||||
entity = Entity(
|
||||
project_id=test_project.id,
|
||||
title="test_entity",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/test-entity",
|
||||
file_path="test/test_entity.md",
|
||||
content_type="text/markdown",
|
||||
@@ -137,7 +137,7 @@ async def test_delete_observation_by_id(
|
||||
entity = Entity(
|
||||
project_id=test_project.id,
|
||||
title="test_entity",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/test-entity",
|
||||
file_path="test/test_entity.md",
|
||||
content_type="text/markdown",
|
||||
@@ -174,7 +174,7 @@ async def test_delete_observation_by_content(
|
||||
entity = Entity(
|
||||
project_id=test_project.id,
|
||||
title="test_entity",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/test-entity",
|
||||
file_path="test/test_entity.md",
|
||||
content_type="text/markdown",
|
||||
@@ -215,7 +215,7 @@ async def test_find_by_category(session_maker: async_sessionmaker, repo, test_pr
|
||||
entity = Entity(
|
||||
project_id=test_project.id,
|
||||
title="test_entity",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/test-entity",
|
||||
file_path="test/test_entity.md",
|
||||
content_type="text/markdown",
|
||||
@@ -276,7 +276,7 @@ async def test_observation_categories(
|
||||
entity = Entity(
|
||||
project_id=test_project.id,
|
||||
title="test_entity",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/test-entity",
|
||||
file_path="test/test_entity.md",
|
||||
content_type="text/markdown",
|
||||
@@ -344,7 +344,7 @@ async def test_find_by_category_case_sensitivity(
|
||||
entity = Entity(
|
||||
project_id=test_project.id,
|
||||
title="test_entity",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/test-entity",
|
||||
file_path="test/test_entity.md",
|
||||
content_type="text/markdown",
|
||||
@@ -389,7 +389,7 @@ async def test_observation_permalink_truncates_long_content(
|
||||
entity = Entity(
|
||||
project_id=test_project.id,
|
||||
title="test_entity",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/test-entity",
|
||||
file_path="test/test_entity.md",
|
||||
content_type="text/markdown",
|
||||
@@ -440,7 +440,7 @@ async def test_observation_permalink_short_content_unchanged(
|
||||
entity = Entity(
|
||||
project_id=test_project.id,
|
||||
title="test_entity",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/test-entity",
|
||||
file_path="test/test_entity.md",
|
||||
content_type="text/markdown",
|
||||
|
||||
@@ -79,7 +79,7 @@ async def test_postgres_search_repository_index_and_search(session_maker, test_p
|
||||
permalink="docs/coffee-brewing",
|
||||
file_path="docs/coffee-brewing.md",
|
||||
type="entity",
|
||||
metadata={"entity_type": "note"},
|
||||
metadata={"note_type": "note"},
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
@@ -109,8 +109,8 @@ async def test_postgres_search_repository_index_and_search(session_maker, test_p
|
||||
results = await repo.search(search_item_types=[SearchItemType.ENTITY])
|
||||
assert any(r.permalink == "docs/coffee-brewing" for r in results)
|
||||
|
||||
# Entity type filter via metadata JSONB containment
|
||||
results = await repo.search(types=["note"])
|
||||
# Note type filter via metadata JSONB containment
|
||||
results = await repo.search(note_types=["note"])
|
||||
assert any(r.permalink == "docs/coffee-brewing" for r in results)
|
||||
|
||||
# Date filter (also exercises order_by_clause)
|
||||
@@ -150,7 +150,7 @@ async def test_postgres_search_repository_bulk_index_items_and_prepare_terms(
|
||||
permalink="docs/pour-over",
|
||||
file_path="docs/pour-over.md",
|
||||
type="entity",
|
||||
metadata={"entity_type": "note"},
|
||||
metadata={"note_type": "note"},
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
),
|
||||
@@ -163,7 +163,7 @@ async def test_postgres_search_repository_bulk_index_items_and_prepare_terms(
|
||||
permalink="docs/french-press",
|
||||
file_path="docs/french-press.md",
|
||||
type="entity",
|
||||
metadata={"entity_type": "note"},
|
||||
metadata={"note_type": "note"},
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
),
|
||||
@@ -194,7 +194,7 @@ async def test_postgres_search_repository_wildcard_text_and_permalink_match_exac
|
||||
permalink="docs/x",
|
||||
file_path="docs/x.md",
|
||||
type="entity",
|
||||
metadata={"entity_type": "note"},
|
||||
metadata={"note_type": "note"},
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
@@ -254,7 +254,7 @@ async def test_bulk_index_items_strips_nul_bytes(session_maker, test_project):
|
||||
permalink="test/nul-row",
|
||||
file_path="test/nul.md",
|
||||
type="entity",
|
||||
metadata={"entity_type": "note"},
|
||||
metadata={"note_type": "note"},
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
@@ -280,7 +280,7 @@ async def test_index_item_strips_nul_bytes(session_maker, test_project):
|
||||
permalink="test/nul-single",
|
||||
file_path="test/nul-single.md",
|
||||
type="entity",
|
||||
metadata={"entity_type": "note"},
|
||||
metadata={"note_type": "note"},
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
@@ -342,7 +342,7 @@ async def test_postgres_semantic_vector_search_returns_ranked_entities(session_m
|
||||
file_path="specs/authentication.md",
|
||||
type=SearchItemType.ENTITY.value,
|
||||
entity_id=401,
|
||||
metadata={"entity_type": "spec"},
|
||||
metadata={"note_type": "spec"},
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
),
|
||||
@@ -356,7 +356,7 @@ async def test_postgres_semantic_vector_search_returns_ranked_entities(session_m
|
||||
file_path="specs/migrations.md",
|
||||
type=SearchItemType.ENTITY.value,
|
||||
entity_id=402,
|
||||
metadata={"entity_type": "spec"},
|
||||
metadata={"note_type": "spec"},
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
),
|
||||
@@ -408,7 +408,7 @@ async def test_postgres_semantic_hybrid_search_combines_fts_and_vector(session_m
|
||||
file_path="specs/task-queue-worker.md",
|
||||
type=SearchItemType.ENTITY.value,
|
||||
entity_id=411,
|
||||
metadata={"entity_type": "spec"},
|
||||
metadata={"note_type": "spec"},
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
),
|
||||
@@ -422,7 +422,7 @@ async def test_postgres_semantic_hybrid_search_combines_fts_and_vector(session_m
|
||||
file_path="specs/search-index.md",
|
||||
type=SearchItemType.ENTITY.value,
|
||||
entity_id=412,
|
||||
metadata={"entity_type": "spec"},
|
||||
metadata={"note_type": "spec"},
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
),
|
||||
|
||||
@@ -17,7 +17,7 @@ async def source_entity(session_maker, test_project: Project):
|
||||
entity = Entity(
|
||||
project_id=test_project.id,
|
||||
title="test_source",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="source/test-source",
|
||||
file_path="source/test_source.md",
|
||||
content_type="text/markdown",
|
||||
@@ -36,7 +36,7 @@ async def target_entity(session_maker, test_project: Project):
|
||||
entity = Entity(
|
||||
project_id=test_project.id,
|
||||
title="test_target",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="target/test-target",
|
||||
file_path="target/test_target.md",
|
||||
content_type="text/markdown",
|
||||
@@ -79,7 +79,7 @@ async def related_entity(entity_repository):
|
||||
"""Create a second entity for testing relations"""
|
||||
entity_data = {
|
||||
"title": "Related Entity",
|
||||
"entity_type": "test",
|
||||
"note_type": "test",
|
||||
"permalink": "test/related-entity",
|
||||
"file_path": "test/related_entity.md",
|
||||
"summary": "A related test entity",
|
||||
|
||||
@@ -26,7 +26,7 @@ async def search_entity(session_maker, test_project: Project):
|
||||
entity = Entity(
|
||||
project_id=test_project.id,
|
||||
title="Search Test Entity",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/search-test-entity",
|
||||
file_path="test/search_test_entity.md",
|
||||
content_type="text/markdown",
|
||||
@@ -68,7 +68,7 @@ async def second_entity(session_maker, second_project: Project):
|
||||
entity = Entity(
|
||||
project_id=second_project.id,
|
||||
title="Second Project Entity",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
permalink="test/second-project-entity",
|
||||
file_path="test/second_project_entity.md",
|
||||
content_type="text/markdown",
|
||||
@@ -128,7 +128,7 @@ async def test_init_search_index_preserves_data(search_repository, search_entity
|
||||
permalink=search_entity.permalink,
|
||||
file_path=search_entity.file_path,
|
||||
entity_id=search_entity.id,
|
||||
metadata={"entity_type": search_entity.entity_type},
|
||||
metadata={"note_type": search_entity.note_type},
|
||||
created_at=search_entity.created_at,
|
||||
updated_at=search_entity.updated_at,
|
||||
project_id=search_repository.project_id,
|
||||
@@ -162,7 +162,7 @@ async def test_index_item(search_repository, search_entity):
|
||||
permalink=search_entity.permalink,
|
||||
file_path=search_entity.file_path,
|
||||
entity_id=search_entity.id,
|
||||
metadata={"entity_type": search_entity.entity_type},
|
||||
metadata={"note_type": search_entity.note_type},
|
||||
created_at=search_entity.created_at,
|
||||
updated_at=search_entity.updated_at,
|
||||
project_id=search_repository.project_id,
|
||||
@@ -197,7 +197,7 @@ async def test_index_item_upsert_on_duplicate_permalink(search_repository, searc
|
||||
permalink=search_entity.permalink,
|
||||
file_path=search_entity.file_path,
|
||||
entity_id=search_entity.id,
|
||||
metadata={"entity_type": search_entity.entity_type},
|
||||
metadata={"note_type": search_entity.note_type},
|
||||
created_at=search_entity.created_at,
|
||||
updated_at=search_entity.updated_at,
|
||||
project_id=search_repository.project_id,
|
||||
@@ -220,7 +220,7 @@ async def test_index_item_upsert_on_duplicate_permalink(search_repository, searc
|
||||
permalink=search_entity.permalink, # Same permalink!
|
||||
file_path=search_entity.file_path,
|
||||
entity_id=search_entity.id,
|
||||
metadata={"entity_type": search_entity.entity_type},
|
||||
metadata={"note_type": search_entity.note_type},
|
||||
created_at=search_entity.created_at,
|
||||
updated_at=search_entity.updated_at,
|
||||
project_id=search_repository.project_id,
|
||||
@@ -254,7 +254,7 @@ async def test_bulk_index_items_upsert_on_duplicate_permalink(search_repository,
|
||||
permalink=search_entity.permalink,
|
||||
file_path=search_entity.file_path,
|
||||
entity_id=search_entity.id,
|
||||
metadata={"entity_type": search_entity.entity_type},
|
||||
metadata={"note_type": search_entity.note_type},
|
||||
created_at=search_entity.created_at,
|
||||
updated_at=search_entity.updated_at,
|
||||
project_id=search_repository.project_id,
|
||||
@@ -276,7 +276,7 @@ async def test_bulk_index_items_upsert_on_duplicate_permalink(search_repository,
|
||||
permalink=search_entity.permalink, # Same permalink!
|
||||
file_path=search_entity.file_path,
|
||||
entity_id=search_entity.id,
|
||||
metadata={"entity_type": search_entity.entity_type},
|
||||
metadata={"note_type": search_entity.note_type},
|
||||
created_at=search_entity.created_at,
|
||||
updated_at=search_entity.updated_at,
|
||||
project_id=search_repository.project_id,
|
||||
@@ -305,7 +305,7 @@ async def test_project_isolation(
|
||||
permalink=search_entity.permalink,
|
||||
file_path=search_entity.file_path,
|
||||
entity_id=search_entity.id,
|
||||
metadata={"entity_type": search_entity.entity_type},
|
||||
metadata={"note_type": search_entity.note_type},
|
||||
created_at=search_entity.created_at,
|
||||
updated_at=search_entity.updated_at,
|
||||
project_id=search_repository.project_id,
|
||||
@@ -320,7 +320,7 @@ async def test_project_isolation(
|
||||
permalink=second_entity.permalink,
|
||||
file_path=second_entity.file_path,
|
||||
entity_id=second_entity.id,
|
||||
metadata={"entity_type": second_entity.entity_type},
|
||||
metadata={"note_type": second_entity.note_type},
|
||||
created_at=second_entity.created_at,
|
||||
updated_at=second_entity.updated_at,
|
||||
project_id=second_project_repository.project_id,
|
||||
@@ -364,7 +364,7 @@ async def test_delete_by_permalink(search_repository, search_entity):
|
||||
permalink=search_entity.permalink,
|
||||
file_path=search_entity.file_path,
|
||||
entity_id=search_entity.id,
|
||||
metadata={"entity_type": search_entity.entity_type},
|
||||
metadata={"note_type": search_entity.note_type},
|
||||
created_at=search_entity.created_at,
|
||||
updated_at=search_entity.updated_at,
|
||||
project_id=search_repository.project_id,
|
||||
@@ -397,7 +397,7 @@ async def test_delete_by_entity_id(search_repository, search_entity):
|
||||
permalink=search_entity.permalink,
|
||||
file_path=search_entity.file_path,
|
||||
entity_id=search_entity.id,
|
||||
metadata={"entity_type": search_entity.entity_type},
|
||||
metadata={"note_type": search_entity.note_type},
|
||||
created_at=search_entity.created_at,
|
||||
updated_at=search_entity.updated_at,
|
||||
project_id=search_repository.project_id,
|
||||
@@ -725,7 +725,7 @@ class TestSearchTermPreparation:
|
||||
permalink=search_entity.permalink,
|
||||
file_path=search_entity.file_path,
|
||||
entity_id=search_entity.id,
|
||||
metadata={"entity_type": search_entity.entity_type},
|
||||
metadata={"note_type": search_entity.note_type},
|
||||
created_at=search_entity.created_at,
|
||||
updated_at=search_entity.updated_at,
|
||||
project_id=search_repository.project_id,
|
||||
@@ -759,7 +759,7 @@ class TestSearchTermPreparation:
|
||||
permalink=search_entity.permalink,
|
||||
file_path=search_entity.file_path,
|
||||
entity_id=search_entity.id,
|
||||
metadata={"entity_type": search_entity.entity_type},
|
||||
metadata={"note_type": search_entity.note_type},
|
||||
created_at=search_entity.created_at,
|
||||
updated_at=search_entity.updated_at,
|
||||
project_id=search_repository.project_id,
|
||||
@@ -847,7 +847,7 @@ async def _index_entity_with_metadata(search_repository, session_maker, title, e
|
||||
entity = Entity(
|
||||
project_id=search_repository.project_id,
|
||||
title=title,
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink=permalink,
|
||||
file_path=file_path,
|
||||
content_type="text/markdown",
|
||||
@@ -867,7 +867,7 @@ async def _index_entity_with_metadata(search_repository, session_maker, title, e
|
||||
permalink=entity.permalink,
|
||||
file_path=entity.file_path,
|
||||
entity_id=entity.id,
|
||||
metadata={"entity_type": entity.entity_type},
|
||||
metadata={"note_type": entity.note_type},
|
||||
created_at=entity.created_at,
|
||||
updated_at=entity.updated_at,
|
||||
project_id=search_repository.project_id,
|
||||
|
||||
@@ -99,7 +99,7 @@ async def test_index_item_respects_project_isolation_during_edit():
|
||||
permalink=same_permalink,
|
||||
file_path="notes/test_note.md",
|
||||
entity_id=1,
|
||||
metadata={"entity_type": "note"},
|
||||
metadata={"note_type": "note"},
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
project_id=project1_id,
|
||||
@@ -114,7 +114,7 @@ async def test_index_item_respects_project_isolation_during_edit():
|
||||
permalink=same_permalink, # SAME permalink as project 1
|
||||
file_path="notes/test_note.md",
|
||||
entity_id=2,
|
||||
metadata={"entity_type": "note"},
|
||||
metadata={"note_type": "note"},
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
project_id=project2_id,
|
||||
@@ -146,7 +146,7 @@ async def test_index_item_respects_project_isolation_during_edit():
|
||||
permalink=same_permalink,
|
||||
file_path="notes/test_note.md",
|
||||
entity_id=1,
|
||||
metadata={"entity_type": "note"},
|
||||
metadata={"note_type": "note"},
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
project_id=project1_id,
|
||||
@@ -222,7 +222,7 @@ async def test_index_item_updates_existing_record_same_project():
|
||||
permalink=permalink,
|
||||
file_path="test/my_note.md",
|
||||
entity_id=1,
|
||||
metadata={"entity_type": "note"},
|
||||
metadata={"note_type": "note"},
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
project_id=project_id,
|
||||
@@ -246,7 +246,7 @@ async def test_index_item_updates_existing_record_same_project():
|
||||
permalink=permalink, # Same permalink
|
||||
file_path="test/my_note.md",
|
||||
entity_id=1,
|
||||
metadata={"entity_type": "note"},
|
||||
metadata={"note_type": "note"},
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
project_id=project_id,
|
||||
|
||||
@@ -20,7 +20,7 @@ async def _index_entity(search_repository, session_maker, title: str, status: st
|
||||
entity = Entity(
|
||||
project_id=search_repository.project_id,
|
||||
title=title,
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
permalink=permalink,
|
||||
file_path=file_path,
|
||||
content_type="text/markdown",
|
||||
@@ -42,7 +42,7 @@ async def _index_entity(search_repository, session_maker, title: str, status: st
|
||||
permalink=entity.permalink,
|
||||
file_path=entity.file_path,
|
||||
entity_id=entity.id,
|
||||
metadata={"entity_type": entity.entity_type},
|
||||
metadata={"note_type": entity.note_type},
|
||||
created_at=entity.created_at,
|
||||
updated_at=entity.updated_at,
|
||||
)
|
||||
|
||||
@@ -52,7 +52,7 @@ def _entity_row(
|
||||
title=title,
|
||||
permalink=permalink,
|
||||
file_path=f"{permalink}.md",
|
||||
metadata={"entity_type": "spec"},
|
||||
metadata={"note_type": "spec"},
|
||||
entity_id=entity_id,
|
||||
content_stems=content_stems,
|
||||
content_snippet=content_stems,
|
||||
|
||||
@@ -116,7 +116,7 @@ async def test_page1_scores_gte_page2_scores():
|
||||
permalink=None,
|
||||
permalink_match=None,
|
||||
title=None,
|
||||
types=None,
|
||||
note_types=None,
|
||||
after_date=None,
|
||||
search_item_types=None,
|
||||
metadata_filters=None,
|
||||
|
||||
@@ -89,7 +89,7 @@ COMMON_SEARCH_KWARGS = dict(
|
||||
permalink=None,
|
||||
permalink_match=None,
|
||||
title=None,
|
||||
types=None,
|
||||
note_types=None,
|
||||
after_date=None,
|
||||
search_item_types=None,
|
||||
metadata_filters=None,
|
||||
|
||||
@@ -236,14 +236,14 @@ class TestInferSuggestedSchema:
|
||||
|
||||
class TestInferRelationTargetType:
|
||||
"""Verify that relation target type comes from the individual relation,
|
||||
not from the source note's entity_type."""
|
||||
not from the source note's note_type."""
|
||||
|
||||
def test_target_type_from_relation_data(self):
|
||||
"""Relations with target_entity_type produce correct schema suggestions."""
|
||||
"""Relations with target_note_type produce correct schema suggestions."""
|
||||
notes = [
|
||||
_note(
|
||||
f"n{i}",
|
||||
relations=[Rel("works_at", f"Org{i}", target_entity_type="organization")],
|
||||
relations=[Rel("works_at", f"Org{i}", target_note_type="organization")],
|
||||
)
|
||||
for i in range(3)
|
||||
]
|
||||
@@ -252,7 +252,7 @@ class TestInferRelationTargetType:
|
||||
assert works_at.target_type == "organization"
|
||||
|
||||
def test_target_type_none_when_unresolved(self):
|
||||
"""Relations without target_entity_type produce None target_type."""
|
||||
"""Relations without target_note_type produce None target_type."""
|
||||
notes = [_note(f"n{i}", relations=[Rel("knows", f"P{i}")]) for i in range(3)]
|
||||
result = infer_schema("Person", notes)
|
||||
knows = next(f for f in result.field_frequencies if f.name == "knows")
|
||||
@@ -261,9 +261,9 @@ class TestInferRelationTargetType:
|
||||
def test_mixed_target_types_uses_most_common(self):
|
||||
"""When relations point to different types, the most common wins."""
|
||||
notes = [
|
||||
_note("n0", relations=[Rel("works_at", "OrgA", target_entity_type="organization")]),
|
||||
_note("n1", relations=[Rel("works_at", "OrgB", target_entity_type="organization")]),
|
||||
_note("n2", relations=[Rel("works_at", "SchoolC", target_entity_type="school")]),
|
||||
_note("n0", relations=[Rel("works_at", "OrgA", target_note_type="organization")]),
|
||||
_note("n1", relations=[Rel("works_at", "OrgB", target_note_type="organization")]),
|
||||
_note("n2", relations=[Rel("works_at", "SchoolC", target_note_type="school")]),
|
||||
]
|
||||
result = infer_schema("Person", notes)
|
||||
works_at = next(f for f in result.field_frequencies if f.name == "works_at")
|
||||
|
||||
@@ -19,20 +19,20 @@ from basic_memory.schemas.base import to_snake_case, TimeFrame, parse_timeframe,
|
||||
|
||||
def test_entity_project_name():
|
||||
"""Test creating EntityIn with minimal required fields."""
|
||||
data = {"title": "Test Entity", "directory": "test", "entity_type": "knowledge"}
|
||||
data = {"title": "Test Entity", "directory": "test", "note_type": "knowledge"}
|
||||
entity = Entity.model_validate(data)
|
||||
assert entity.file_path == os.path.join("test", "Test Entity.md")
|
||||
assert entity.permalink == "test/test-entity"
|
||||
assert entity.entity_type == "knowledge"
|
||||
assert entity.note_type == "knowledge"
|
||||
|
||||
|
||||
def test_entity_project_id():
|
||||
"""Test creating EntityIn with minimal required fields."""
|
||||
data = {"project": 2, "title": "Test Entity", "directory": "test", "entity_type": "knowledge"}
|
||||
data = {"project": 2, "title": "Test Entity", "directory": "test", "note_type": "knowledge"}
|
||||
entity = Entity.model_validate(data)
|
||||
assert entity.file_path == os.path.join("test", "Test Entity.md")
|
||||
assert entity.permalink == "test/test-entity"
|
||||
assert entity.entity_type == "knowledge"
|
||||
assert entity.note_type == "knowledge"
|
||||
|
||||
|
||||
def test_entity_non_markdown():
|
||||
@@ -40,19 +40,19 @@ def test_entity_non_markdown():
|
||||
data = {
|
||||
"title": "Test Entity.txt",
|
||||
"directory": "test",
|
||||
"entity_type": "file",
|
||||
"note_type": "file",
|
||||
"content_type": "text/plain",
|
||||
}
|
||||
entity = Entity.model_validate(data)
|
||||
assert entity.file_path == os.path.join("test", "Test Entity.txt")
|
||||
assert entity.permalink == "test/test-entity"
|
||||
assert entity.entity_type == "file"
|
||||
assert entity.note_type == "file"
|
||||
|
||||
|
||||
def test_entity_in_validation():
|
||||
"""Test validation errors for EntityIn."""
|
||||
with pytest.raises(ValidationError):
|
||||
Entity.model_validate({"entity_type": "test"}) # Missing required fields
|
||||
Entity.model_validate({"note_type": "test"}) # Missing required fields
|
||||
|
||||
|
||||
def test_relation_in_validation():
|
||||
@@ -141,7 +141,7 @@ def test_entity_out_from_attributes():
|
||||
"title": "Test Entity",
|
||||
"permalink": "test/test",
|
||||
"file_path": "test",
|
||||
"entity_type": "knowledge",
|
||||
"note_type": "knowledge",
|
||||
"content_type": "text/markdown",
|
||||
"observations": [
|
||||
{
|
||||
@@ -183,7 +183,7 @@ def test_entity_response_with_none_permalink():
|
||||
"title": "Test Entity",
|
||||
"permalink": None, # This should not cause validation errors
|
||||
"file_path": "test/test-entity.md",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"observations": [],
|
||||
"relations": [],
|
||||
@@ -196,7 +196,7 @@ def test_entity_response_with_none_permalink():
|
||||
assert entity.permalink is None
|
||||
assert entity.title == "Test Entity"
|
||||
assert entity.file_path == "test/test-entity.md"
|
||||
assert entity.entity_type == "note"
|
||||
assert entity.note_type == "note"
|
||||
assert len(entity.observations) == 0
|
||||
assert len(entity.relations) == 0
|
||||
|
||||
|
||||
@@ -34,11 +34,11 @@ def test_search_filters():
|
||||
query = SearchQuery(
|
||||
text="search",
|
||||
entity_types=[SearchItemType.ENTITY],
|
||||
types=["component"],
|
||||
note_types=["component"],
|
||||
after_date=datetime(2024, 1, 1),
|
||||
)
|
||||
assert query.entity_types == [SearchItemType.ENTITY]
|
||||
assert query.types == ["component"]
|
||||
assert query.note_types == ["component"]
|
||||
assert query.after_date == "2024-01-01T00:00:00"
|
||||
|
||||
|
||||
@@ -58,13 +58,13 @@ def test_search_result():
|
||||
type=SearchItemType.ENTITY,
|
||||
entity="some_entity",
|
||||
score=0.8,
|
||||
metadata={"entity_type": "component"},
|
||||
metadata={"note_type": "component"},
|
||||
permalink="specs/search",
|
||||
file_path="specs/search.md",
|
||||
)
|
||||
assert result.type == SearchItemType.ENTITY
|
||||
assert result.score == 0.8
|
||||
assert result.metadata == {"entity_type": "component"}
|
||||
assert result.metadata == {"note_type": "component"}
|
||||
|
||||
|
||||
def test_observation_result():
|
||||
|
||||
@@ -250,7 +250,7 @@ async def test_project_isolation_in_find_related(session_maker, app_config):
|
||||
# Create entities in project1
|
||||
entity1_p1 = Entity(
|
||||
title="Entity1_P1",
|
||||
entity_type="document",
|
||||
note_type="document",
|
||||
content_type="text/markdown",
|
||||
project_id=project1.id,
|
||||
permalink="project1/entity1",
|
||||
@@ -260,7 +260,7 @@ async def test_project_isolation_in_find_related(session_maker, app_config):
|
||||
)
|
||||
entity2_p1 = Entity(
|
||||
title="Entity2_P1",
|
||||
entity_type="document",
|
||||
note_type="document",
|
||||
content_type="text/markdown",
|
||||
project_id=project1.id,
|
||||
permalink="project1/entity2",
|
||||
@@ -272,7 +272,7 @@ async def test_project_isolation_in_find_related(session_maker, app_config):
|
||||
# Create entities in project2
|
||||
entity1_p2 = Entity(
|
||||
title="Entity1_P2",
|
||||
entity_type="document",
|
||||
note_type="document",
|
||||
content_type="text/markdown",
|
||||
project_id=project2.id,
|
||||
permalink="project2/entity1",
|
||||
|
||||
@@ -39,7 +39,7 @@ async def test_directory_tree(directory_service: DirectoryService, test_graph):
|
||||
assert node_0.type == "directory"
|
||||
assert node_0.content_type is None
|
||||
assert node_0.entity_id is None
|
||||
assert node_0.entity_type is None
|
||||
assert node_0.note_type is None
|
||||
assert node_0.title is None
|
||||
assert node_0.directory_path == "/test"
|
||||
assert node_0.has_children is True
|
||||
@@ -51,7 +51,7 @@ async def test_directory_tree(directory_service: DirectoryService, test_graph):
|
||||
assert node_file.type == "file"
|
||||
assert node_file.content_type == "text/markdown"
|
||||
assert node_file.entity_id == 1
|
||||
assert node_file.entity_type == "deeper"
|
||||
assert node_file.note_type == "deeper"
|
||||
assert node_file.title == "Deeper Entity"
|
||||
assert node_file.permalink == "test-project/test/deeper-entity"
|
||||
assert node_file.directory_path == "/test/Deeper Entity.md"
|
||||
@@ -250,7 +250,7 @@ async def test_directory_structure(directory_service: DirectoryService, test_gra
|
||||
# Verify no file metadata is present
|
||||
assert node_0.content_type is None
|
||||
assert node_0.entity_id is None
|
||||
assert node_0.entity_type is None
|
||||
assert node_0.note_type is None
|
||||
assert node_0.title is None
|
||||
assert node_0.permalink is None
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ async def test_create_entity(
|
||||
entity_data = EntitySchema(
|
||||
title="Test Entity",
|
||||
directory="",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
)
|
||||
# Save expected permalink before create_entity mutates entity_data._permalink
|
||||
expected_permalink = f"{generate_permalink(project_config.name)}/{entity_data.permalink}"
|
||||
@@ -39,14 +39,14 @@ async def test_create_entity(
|
||||
assert isinstance(entity, EntityModel)
|
||||
assert entity.permalink == expected_permalink
|
||||
assert entity.file_path == entity_data.file_path
|
||||
assert entity.entity_type == "test"
|
||||
assert entity.note_type == "test"
|
||||
assert entity.created_at is not None
|
||||
assert len(entity.relations) == 0
|
||||
|
||||
# Verify we can retrieve it using permalink
|
||||
retrieved = await entity_service.get_by_permalink(entity.permalink)
|
||||
assert retrieved.title == "Test Entity"
|
||||
assert retrieved.entity_type == "test"
|
||||
assert retrieved.note_type == "test"
|
||||
assert retrieved.created_at is not None
|
||||
|
||||
# Verify file was written
|
||||
@@ -59,7 +59,7 @@ async def test_create_entity(
|
||||
|
||||
# Verify frontmatter contents
|
||||
assert metadata["permalink"] == entity.permalink
|
||||
assert metadata["type"] == entity.entity_type
|
||||
assert metadata["type"] == entity.note_type
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -70,7 +70,7 @@ async def test_create_entity_file_exists(
|
||||
entity_data = EntitySchema(
|
||||
title="Test Entity",
|
||||
directory="",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
content="first",
|
||||
)
|
||||
|
||||
@@ -90,7 +90,7 @@ async def test_create_entity_file_exists(
|
||||
entity_data = EntitySchema(
|
||||
title="Test Entity",
|
||||
directory="",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
content="second",
|
||||
)
|
||||
|
||||
@@ -109,7 +109,7 @@ async def test_create_entity_unique_permalink(
|
||||
entity_data = EntitySchema(
|
||||
title="Test Entity",
|
||||
directory="test",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
)
|
||||
|
||||
entity = await entity_service.create_entity(entity_data)
|
||||
@@ -143,14 +143,14 @@ async def test_get_by_permalink(entity_service: EntityService):
|
||||
entity1_data = EntitySchema(
|
||||
title="TestEntity1",
|
||||
directory="test",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
)
|
||||
entity1 = await entity_service.create_entity(entity1_data)
|
||||
|
||||
entity2_data = EntitySchema(
|
||||
title="TestEntity2",
|
||||
directory="test",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
)
|
||||
entity2 = await entity_service.create_entity(entity2_data)
|
||||
|
||||
@@ -158,13 +158,13 @@ async def test_get_by_permalink(entity_service: EntityService):
|
||||
found = await entity_service.get_by_permalink(entity1_data.permalink)
|
||||
assert found is not None
|
||||
assert found.id == entity1.id
|
||||
assert found.entity_type == entity1.entity_type
|
||||
assert found.note_type == entity1.note_type
|
||||
|
||||
# Find by type2 and name
|
||||
found = await entity_service.get_by_permalink(entity2_data.permalink)
|
||||
assert found is not None
|
||||
assert found.id == entity2.id
|
||||
assert found.entity_type == entity2.entity_type
|
||||
assert found.note_type == entity2.note_type
|
||||
|
||||
# Test not found case
|
||||
with pytest.raises(EntityNotFoundError):
|
||||
@@ -177,7 +177,7 @@ async def test_get_entity_success(entity_service: EntityService):
|
||||
entity_data = EntitySchema(
|
||||
title="TestEntity",
|
||||
directory="test",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
)
|
||||
await entity_service.create_entity(entity_data)
|
||||
|
||||
@@ -186,7 +186,7 @@ async def test_get_entity_success(entity_service: EntityService):
|
||||
|
||||
assert isinstance(retrieved, EntityModel)
|
||||
assert retrieved.title == "TestEntity"
|
||||
assert retrieved.entity_type == "test"
|
||||
assert retrieved.note_type == "test"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -195,7 +195,7 @@ async def test_delete_entity_success(entity_service: EntityService):
|
||||
entity_data = EntitySchema(
|
||||
title="TestEntity",
|
||||
directory="test",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
)
|
||||
await entity_service.create_entity(entity_data)
|
||||
|
||||
@@ -214,7 +214,7 @@ async def test_delete_entity_by_id(entity_service: EntityService):
|
||||
entity_data = EntitySchema(
|
||||
title="TestEntity",
|
||||
directory="test",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
)
|
||||
created = await entity_service.create_entity(entity_data)
|
||||
|
||||
@@ -247,7 +247,7 @@ async def test_create_entity_with_special_chars(entity_service: EntityService):
|
||||
entity_data = EntitySchema(
|
||||
title=name,
|
||||
directory="test",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
)
|
||||
entity = await entity_service.create_entity(entity_data)
|
||||
|
||||
@@ -264,12 +264,12 @@ async def test_get_entities_by_permalinks(entity_service: EntityService):
|
||||
entity1_data = EntitySchema(
|
||||
title="Entity1",
|
||||
directory="test",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
)
|
||||
entity2_data = EntitySchema(
|
||||
title="Entity2",
|
||||
directory="test",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
)
|
||||
await entity_service.create_entity(entity1_data)
|
||||
await entity_service.create_entity(entity2_data)
|
||||
@@ -297,7 +297,7 @@ async def test_get_entities_some_not_found(entity_service: EntityService):
|
||||
entity_data = EntitySchema(
|
||||
title="Entity1",
|
||||
directory="test",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
)
|
||||
await entity_service.create_entity(entity_data)
|
||||
|
||||
@@ -315,7 +315,7 @@ async def test_get_entity_path(entity_service: EntityService):
|
||||
entity = EntityModel(
|
||||
permalink="test-entity",
|
||||
file_path="test-entity.md",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
)
|
||||
path = entity_service.file_service.get_entity_path(entity)
|
||||
assert path == Path(entity_service.file_service.base_path / "test-entity.md")
|
||||
@@ -328,7 +328,7 @@ async def test_update_note_entity_content(entity_service: EntityService, file_se
|
||||
schema = EntitySchema(
|
||||
title="test",
|
||||
directory="test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
entity_metadata={"status": "draft"},
|
||||
)
|
||||
|
||||
@@ -383,7 +383,7 @@ async def test_fast_write_and_reindex_entity(
|
||||
schema = EntitySchema(
|
||||
title="Reindex Target",
|
||||
directory="test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content=dedent("""
|
||||
# Reindex Target
|
||||
|
||||
@@ -413,7 +413,7 @@ async def test_fast_write_entity_generates_external_id(entity_service: EntitySer
|
||||
schema = EntitySchema(
|
||||
title=title,
|
||||
directory="test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
)
|
||||
|
||||
fast_entity = await entity_service.fast_write_entity(schema)
|
||||
@@ -428,7 +428,7 @@ async def test_create_or_update_new(entity_service: EntityService, file_service:
|
||||
EntitySchema(
|
||||
title="test",
|
||||
directory="test",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
entity_metadata={"status": "draft"},
|
||||
)
|
||||
)
|
||||
@@ -444,7 +444,7 @@ async def test_create_or_update_existing(entity_service: EntityService, file_ser
|
||||
EntitySchema(
|
||||
title="test",
|
||||
directory="test",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
content="Test entity",
|
||||
entity_metadata={"status": "final"},
|
||||
)
|
||||
@@ -487,14 +487,14 @@ async def test_create_with_content(entity_service: EntityService, file_service:
|
||||
EntitySchema(
|
||||
title="Git Workflow Guide",
|
||||
directory="test",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
content=content,
|
||||
)
|
||||
)
|
||||
|
||||
assert created is True
|
||||
assert entity.title == "Git Workflow Guide"
|
||||
assert entity.entity_type == "test"
|
||||
assert entity.note_type == "test"
|
||||
assert entity.permalink == "git-workflow-guide"
|
||||
assert entity.file_path == "test/Git Workflow Guide.md"
|
||||
|
||||
@@ -556,7 +556,7 @@ async def test_update_with_content(
|
||||
entity, created = await entity_service.create_or_update_entity(
|
||||
EntitySchema(
|
||||
title="Git Workflow Guide",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
directory="test",
|
||||
content=content,
|
||||
)
|
||||
@@ -617,7 +617,7 @@ async def test_update_with_content(
|
||||
EntitySchema(
|
||||
title="Git Workflow Guide",
|
||||
directory="test",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
content=update_content,
|
||||
)
|
||||
)
|
||||
@@ -673,7 +673,7 @@ async def test_create_with_no_frontmatter(
|
||||
|
||||
assert file_path.as_posix() == created.file_path
|
||||
assert created.title == "Git Workflow Guide"
|
||||
assert created.entity_type == "note"
|
||||
assert created.note_type == "note"
|
||||
assert created.permalink is None
|
||||
|
||||
# assert file
|
||||
@@ -691,7 +691,7 @@ async def test_edit_entity_append(entity_service: EntityService, file_service: F
|
||||
EntitySchema(
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="Original content",
|
||||
)
|
||||
)
|
||||
@@ -717,7 +717,7 @@ async def test_edit_entity_prepend(entity_service: EntityService, file_service:
|
||||
EntitySchema(
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="Original content",
|
||||
)
|
||||
)
|
||||
@@ -743,7 +743,7 @@ async def test_edit_entity_find_replace(entity_service: EntityService, file_serv
|
||||
EntitySchema(
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="This is old content that needs updating",
|
||||
)
|
||||
)
|
||||
@@ -783,7 +783,7 @@ async def test_edit_entity_replace_section(
|
||||
EntitySchema(
|
||||
title="Sample Note",
|
||||
directory="docs",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content=content,
|
||||
)
|
||||
)
|
||||
@@ -814,7 +814,7 @@ async def test_edit_entity_replace_section_create_new(
|
||||
EntitySchema(
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="# Main Title\n\nSome content",
|
||||
)
|
||||
)
|
||||
@@ -851,7 +851,7 @@ async def test_edit_entity_invalid_operation(entity_service: EntityService):
|
||||
EntitySchema(
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="Original content",
|
||||
)
|
||||
)
|
||||
@@ -870,7 +870,7 @@ async def test_edit_entity_find_replace_missing_find_text(entity_service: Entity
|
||||
EntitySchema(
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="Original content",
|
||||
)
|
||||
)
|
||||
@@ -889,7 +889,7 @@ async def test_edit_entity_replace_section_missing_section(entity_service: Entit
|
||||
EntitySchema(
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="Original content",
|
||||
)
|
||||
)
|
||||
@@ -919,7 +919,7 @@ async def test_edit_entity_with_observations_and_relations(
|
||||
EntitySchema(
|
||||
title="Sample Note",
|
||||
directory="docs",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content=content,
|
||||
)
|
||||
)
|
||||
@@ -1028,7 +1028,7 @@ async def test_edit_entity_find_replace_not_found(entity_service: EntityService)
|
||||
EntitySchema(
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="This is some content",
|
||||
)
|
||||
)
|
||||
@@ -1053,7 +1053,7 @@ async def test_edit_entity_find_replace_multiple_occurrences_expected_one(
|
||||
EntitySchema(
|
||||
title="Sample Note",
|
||||
directory="docs",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="The word banana appears here. Another banana word here.",
|
||||
)
|
||||
)
|
||||
@@ -1079,7 +1079,7 @@ async def test_edit_entity_find_replace_multiple_occurrences_success(
|
||||
EntitySchema(
|
||||
title="Sample Note",
|
||||
directory="docs",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="The word banana appears here. Another banana word here.",
|
||||
)
|
||||
)
|
||||
@@ -1107,7 +1107,7 @@ async def test_edit_entity_find_replace_empty_find_text(entity_service: EntitySe
|
||||
EntitySchema(
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="Some content",
|
||||
)
|
||||
)
|
||||
@@ -1142,7 +1142,7 @@ async def test_edit_entity_find_replace_multiline(
|
||||
EntitySchema(
|
||||
title="Sample Note",
|
||||
directory="docs",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content=content,
|
||||
)
|
||||
)
|
||||
@@ -1184,7 +1184,7 @@ async def test_edit_entity_replace_section_multiple_sections_error(entity_servic
|
||||
EntitySchema(
|
||||
title="Sample Note",
|
||||
directory="docs",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content=content,
|
||||
)
|
||||
)
|
||||
@@ -1207,7 +1207,7 @@ async def test_edit_entity_replace_section_empty_section(entity_service: EntityS
|
||||
EntitySchema(
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="Some content",
|
||||
)
|
||||
)
|
||||
@@ -1242,7 +1242,7 @@ async def test_edit_entity_replace_section_header_variations(
|
||||
EntitySchema(
|
||||
title="Sample Note",
|
||||
directory="docs",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content=content,
|
||||
)
|
||||
)
|
||||
@@ -1282,7 +1282,7 @@ async def test_edit_entity_replace_section_at_end_of_document(
|
||||
EntitySchema(
|
||||
title="Sample Note",
|
||||
directory="docs",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content=content,
|
||||
)
|
||||
)
|
||||
@@ -1329,7 +1329,7 @@ async def test_edit_entity_replace_section_with_subsections(
|
||||
EntitySchema(
|
||||
title="Sample Note",
|
||||
directory="docs",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content=content,
|
||||
)
|
||||
)
|
||||
@@ -1373,7 +1373,7 @@ async def test_edit_entity_replace_section_strips_duplicate_header(
|
||||
EntitySchema(
|
||||
title="Sample Note",
|
||||
directory="docs",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content=content,
|
||||
)
|
||||
)
|
||||
@@ -1415,7 +1415,7 @@ async def test_move_entity_success(
|
||||
EntitySchema(
|
||||
title="Test Note",
|
||||
directory="original",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="Original content",
|
||||
)
|
||||
)
|
||||
@@ -1464,7 +1464,7 @@ async def test_move_entity_with_permalink_update(
|
||||
EntitySchema(
|
||||
title="Test Note",
|
||||
directory="original",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="Original content",
|
||||
)
|
||||
)
|
||||
@@ -1505,7 +1505,7 @@ async def test_move_entity_creates_destination_directory(
|
||||
EntitySchema(
|
||||
title="Test Note",
|
||||
directory="original",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="Original content",
|
||||
)
|
||||
)
|
||||
@@ -1555,7 +1555,7 @@ async def test_move_entity_source_file_missing(
|
||||
EntitySchema(
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="Original content",
|
||||
)
|
||||
)
|
||||
@@ -1587,7 +1587,7 @@ async def test_move_entity_destination_exists(
|
||||
EntitySchema(
|
||||
title="Test Note 1",
|
||||
directory="test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="Content 1",
|
||||
)
|
||||
)
|
||||
@@ -1596,7 +1596,7 @@ async def test_move_entity_destination_exists(
|
||||
EntitySchema(
|
||||
title="Test Note 2",
|
||||
directory="test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="Content 2",
|
||||
)
|
||||
)
|
||||
@@ -1624,7 +1624,7 @@ async def test_move_entity_invalid_destination_path(
|
||||
EntitySchema(
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="Original content",
|
||||
)
|
||||
)
|
||||
@@ -1663,7 +1663,7 @@ async def test_move_entity_by_title(
|
||||
EntitySchema(
|
||||
title="Test Note",
|
||||
directory="original",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="Original content",
|
||||
)
|
||||
)
|
||||
@@ -1708,7 +1708,7 @@ async def test_move_entity_preserves_observations_and_relations(
|
||||
EntitySchema(
|
||||
title="Test Note",
|
||||
directory="original",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content=content,
|
||||
)
|
||||
)
|
||||
@@ -1755,7 +1755,7 @@ async def test_move_entity_rollback_on_database_failure(
|
||||
EntitySchema(
|
||||
title="Test Note",
|
||||
directory="original",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="Original content",
|
||||
)
|
||||
)
|
||||
@@ -1815,7 +1815,7 @@ async def test_move_entity_with_complex_observations(
|
||||
EntitySchema(
|
||||
title="Complex Note",
|
||||
directory="docs",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content=content,
|
||||
)
|
||||
)
|
||||
@@ -1878,7 +1878,7 @@ async def test_move_entity_with_null_permalink_generates_permalink(
|
||||
entity_data = {
|
||||
"title": "Test Entity",
|
||||
"file_path": "test/null-permalink-entity.md",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"permalink": None, # This is the key - null permalink from migration
|
||||
"created_at": datetime.now(timezone.utc),
|
||||
@@ -1947,7 +1947,7 @@ async def test_create_or_update_entity_fuzzy_search_bug(
|
||||
entity_a = EntitySchema(
|
||||
title="Node A",
|
||||
directory="edge-cases",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="# Node A\n\nOriginal content for Node A",
|
||||
)
|
||||
|
||||
@@ -1971,7 +1971,7 @@ async def test_create_or_update_entity_fuzzy_search_bug(
|
||||
entity_b = EntitySchema(
|
||||
title="Node B",
|
||||
directory="edge-cases",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="# Node B\n\nContent for Node B",
|
||||
)
|
||||
|
||||
@@ -1984,7 +1984,7 @@ async def test_create_or_update_entity_fuzzy_search_bug(
|
||||
entity_c = EntitySchema(
|
||||
title="Node C",
|
||||
directory="edge-cases",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="# Node C\n\nContent for Node C",
|
||||
)
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ async def test_create_entity_with_permalinks_disabled(
|
||||
entity_data = EntitySchema(
|
||||
title="Test Entity",
|
||||
directory="test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="Test content",
|
||||
)
|
||||
|
||||
@@ -81,7 +81,7 @@ async def test_update_entity_with_permalinks_disabled(
|
||||
entity_data = EntitySchema(
|
||||
title="Test Entity",
|
||||
directory="test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="Original content",
|
||||
)
|
||||
|
||||
@@ -151,7 +151,7 @@ async def test_create_entity_with_content_frontmatter_permalinks_disabled(
|
||||
entity_data = EntitySchema(
|
||||
title="Test Entity",
|
||||
directory="test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content=content,
|
||||
)
|
||||
|
||||
@@ -197,7 +197,7 @@ async def test_move_entity_with_permalinks_disabled(
|
||||
entity_data = EntitySchema(
|
||||
title="Test Entity",
|
||||
directory="test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="Test content",
|
||||
)
|
||||
|
||||
|
||||
@@ -301,7 +301,7 @@ async def test_semantic_embedding_backfill_syncs_each_entity(
|
||||
entity = await entity_repository.create(
|
||||
{
|
||||
"title": f"Backfill Entity {i}",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"entity_metadata": {},
|
||||
"content_type": "text/markdown",
|
||||
"file_path": f"test/backfill-{i}.md",
|
||||
|
||||
@@ -30,7 +30,7 @@ async def test_entities(entity_service, file_service):
|
||||
e1, _ = await entity_service.create_or_update_entity(
|
||||
EntitySchema(
|
||||
title="Core Service",
|
||||
entity_type="component",
|
||||
note_type="component",
|
||||
directory="components",
|
||||
project=entity_service.repository.project_id,
|
||||
)
|
||||
@@ -38,7 +38,7 @@ async def test_entities(entity_service, file_service):
|
||||
e2, _ = await entity_service.create_or_update_entity(
|
||||
EntitySchema(
|
||||
title="Service Config",
|
||||
entity_type="config",
|
||||
note_type="config",
|
||||
directory="config",
|
||||
project=entity_service.repository.project_id,
|
||||
)
|
||||
@@ -46,7 +46,7 @@ async def test_entities(entity_service, file_service):
|
||||
e3, _ = await entity_service.create_or_update_entity(
|
||||
EntitySchema(
|
||||
title="Auth Service",
|
||||
entity_type="component",
|
||||
note_type="component",
|
||||
directory="components",
|
||||
project=entity_service.repository.project_id,
|
||||
)
|
||||
@@ -54,7 +54,7 @@ async def test_entities(entity_service, file_service):
|
||||
e4, _ = await entity_service.create_or_update_entity(
|
||||
EntitySchema(
|
||||
title="Core Features",
|
||||
entity_type="specs",
|
||||
note_type="specs",
|
||||
directory="specs",
|
||||
project=entity_service.repository.project_id,
|
||||
)
|
||||
@@ -62,7 +62,7 @@ async def test_entities(entity_service, file_service):
|
||||
e5, _ = await entity_service.create_or_update_entity(
|
||||
EntitySchema(
|
||||
title="Sub Features 1",
|
||||
entity_type="specs",
|
||||
note_type="specs",
|
||||
directory="specs/subspec",
|
||||
project=entity_service.repository.project_id,
|
||||
)
|
||||
@@ -70,7 +70,7 @@ async def test_entities(entity_service, file_service):
|
||||
e6, _ = await entity_service.create_or_update_entity(
|
||||
EntitySchema(
|
||||
title="Sub Features 2",
|
||||
entity_type="specs",
|
||||
note_type="specs",
|
||||
directory="specs/subspec",
|
||||
project=entity_service.repository.project_id,
|
||||
)
|
||||
@@ -80,7 +80,7 @@ async def test_entities(entity_service, file_service):
|
||||
e7 = await entity_service.repository.add(
|
||||
EntityModel(
|
||||
title="Image.png",
|
||||
entity_type="file",
|
||||
note_type="file",
|
||||
content_type="image/png",
|
||||
file_path="Image.png",
|
||||
permalink="image", # Required for Postgres NOT NULL constraint
|
||||
@@ -93,7 +93,7 @@ async def test_entities(entity_service, file_service):
|
||||
e8 = await entity_service.create_entity( # duplicate title
|
||||
EntitySchema(
|
||||
title="Core Service",
|
||||
entity_type="component",
|
||||
note_type="component",
|
||||
directory="components2",
|
||||
project=entity_service.repository.project_id,
|
||||
)
|
||||
@@ -186,7 +186,7 @@ async def test_resolve_file(link_resolver):
|
||||
# Basic new entity
|
||||
resolved = await link_resolver.resolve_link("Image.png")
|
||||
assert resolved is not None
|
||||
assert resolved.entity_type == "file"
|
||||
assert resolved.note_type == "file"
|
||||
assert resolved.title == "Image.png"
|
||||
|
||||
|
||||
@@ -392,7 +392,7 @@ async def test_cross_project_link_resolution(
|
||||
target = await other_entity_repo.add(
|
||||
EntityModel(
|
||||
title="Cross Project Note",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content_type="text/markdown",
|
||||
file_path="docs/Cross Project Note.md",
|
||||
permalink=f"{other_project.permalink}/docs/cross-project-note",
|
||||
@@ -440,7 +440,7 @@ async def context_aware_entities(entity_repository):
|
||||
e1 = await entity_repository.add(
|
||||
EntityModel(
|
||||
title="testing",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content_type="text/markdown",
|
||||
file_path="testing.md",
|
||||
permalink="testing",
|
||||
@@ -455,7 +455,7 @@ async def context_aware_entities(entity_repository):
|
||||
e2 = await entity_repository.add(
|
||||
EntityModel(
|
||||
title="testing",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content_type="text/markdown",
|
||||
file_path="main/testing/testing.md",
|
||||
permalink="main/testing/testing",
|
||||
@@ -470,7 +470,7 @@ async def context_aware_entities(entity_repository):
|
||||
e3 = await entity_repository.add(
|
||||
EntityModel(
|
||||
title="another-test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content_type="text/markdown",
|
||||
file_path="main/testing/another-test.md",
|
||||
permalink="main/testing/another-test",
|
||||
@@ -485,7 +485,7 @@ async def context_aware_entities(entity_repository):
|
||||
e4 = await entity_repository.add(
|
||||
EntityModel(
|
||||
title="testing",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content_type="text/markdown",
|
||||
file_path="other/testing.md",
|
||||
permalink="other/testing",
|
||||
@@ -500,7 +500,7 @@ async def context_aware_entities(entity_repository):
|
||||
e5 = await entity_repository.add(
|
||||
EntityModel(
|
||||
title="note",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content_type="text/markdown",
|
||||
file_path="deep/nested/folder/note.md",
|
||||
permalink="deep/nested/folder/note",
|
||||
@@ -515,7 +515,7 @@ async def context_aware_entities(entity_repository):
|
||||
e6 = await entity_repository.add(
|
||||
EntityModel(
|
||||
title="note",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content_type="text/markdown",
|
||||
file_path="deep/note.md",
|
||||
permalink="deep/note",
|
||||
@@ -530,7 +530,7 @@ async def context_aware_entities(entity_repository):
|
||||
e7 = await entity_repository.add(
|
||||
EntityModel(
|
||||
title="note",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content_type="text/markdown",
|
||||
file_path="note.md",
|
||||
permalink="note",
|
||||
@@ -739,7 +739,7 @@ async def relative_path_entities(entity_repository):
|
||||
e1 = await entity_repository.add(
|
||||
EntityModel(
|
||||
title="link-test",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content_type="text/markdown",
|
||||
file_path="testing/link-test.md",
|
||||
permalink="testing/link-test",
|
||||
@@ -754,7 +754,7 @@ async def relative_path_entities(entity_repository):
|
||||
e2 = await entity_repository.add(
|
||||
EntityModel(
|
||||
title="deep-note",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content_type="text/markdown",
|
||||
file_path="testing/nested/deep-note.md",
|
||||
permalink="testing/nested/deep-note",
|
||||
@@ -769,7 +769,7 @@ async def relative_path_entities(entity_repository):
|
||||
e3 = await entity_repository.add(
|
||||
EntityModel(
|
||||
title="deep-note",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content_type="text/markdown",
|
||||
file_path="nested/deep-note.md",
|
||||
permalink="nested/deep-note",
|
||||
@@ -784,7 +784,7 @@ async def relative_path_entities(entity_repository):
|
||||
e4 = await entity_repository.add(
|
||||
EntityModel(
|
||||
title="file",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content_type="text/markdown",
|
||||
file_path="other/file.md",
|
||||
permalink="other/file",
|
||||
|
||||
@@ -45,7 +45,7 @@ async def test_remove_project_with_related_entities(project_service: ProjectServ
|
||||
|
||||
entity_data = {
|
||||
"title": "Test Entity for Deletion",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"project_id": project.id,
|
||||
"permalink": "test-deletion-entity",
|
||||
|
||||
@@ -135,7 +135,7 @@ async def test_get_statistics(project_service: ProjectService, test_graph, test_
|
||||
# Assert it returns a valid ProjectStatistics object
|
||||
assert isinstance(statistics, ProjectStatistics)
|
||||
assert statistics.total_entities > 0
|
||||
assert "test" in statistics.entity_types
|
||||
assert "test" in statistics.note_types
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -152,12 +152,12 @@ async def test_filters(search_service, test_graph):
|
||||
"""Test search filters."""
|
||||
# Combined filters
|
||||
results = await search_service.search(
|
||||
SearchQuery(text="Deep", entity_types=[SearchItemType.ENTITY], types=["deep"])
|
||||
SearchQuery(text="Deep", entity_types=[SearchItemType.ENTITY], note_types=["deep"])
|
||||
)
|
||||
assert len(results) == 1
|
||||
for r in results:
|
||||
assert r.type == SearchItemType.ENTITY
|
||||
assert r.metadata.get("entity_type") == "deep"
|
||||
assert r.metadata.get("note_type") == "deep"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -197,7 +197,7 @@ async def test_search_type(search_service, test_graph):
|
||||
"""Test search filters."""
|
||||
|
||||
# Should find only type
|
||||
results = await search_service.search(SearchQuery(types=["test"]))
|
||||
results = await search_service.search(SearchQuery(note_types=["test"]))
|
||||
assert len(results) > 0
|
||||
for r in results:
|
||||
assert r.type == SearchItemType.ENTITY
|
||||
@@ -222,7 +222,7 @@ async def test_extract_entity_tags_exception_handling(search_service):
|
||||
# Create entity with string tags that will cause parsing to fail and fall back to single tag
|
||||
entity_with_invalid_tags = Entity(
|
||||
title="Test Entity",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
entity_metadata={"tags": "just a string"}, # This will fail ast.literal_eval
|
||||
content_type="text/markdown",
|
||||
file_path="test/test-entity.md",
|
||||
@@ -236,7 +236,7 @@ async def test_extract_entity_tags_exception_handling(search_service):
|
||||
# Test with empty string (should return empty list) - covers line 149
|
||||
entity_with_empty_tags = Entity(
|
||||
title="Test Entity Empty",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
entity_metadata={"tags": ""},
|
||||
content_type="text/markdown",
|
||||
file_path="test/test-entity-empty.md",
|
||||
@@ -429,7 +429,7 @@ async def test_plain_multiterm_fts_retries_with_relaxed_or_when_strict_empty(
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
permalink="test/fallback",
|
||||
metadata={"entity_type": "note"},
|
||||
metadata={"note_type": "note"},
|
||||
title="Fallback Match",
|
||||
score=1.0,
|
||||
)
|
||||
@@ -560,7 +560,7 @@ async def test_extract_entity_tags_list_format(search_service, session_maker):
|
||||
|
||||
entity = Entity(
|
||||
title="Test Entity",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
entity_metadata={"tags": ["business", "strategy", "planning"]},
|
||||
content_type="text/markdown",
|
||||
file_path="test/business-strategy.md",
|
||||
@@ -578,7 +578,7 @@ async def test_extract_entity_tags_string_format(search_service, session_maker):
|
||||
|
||||
entity = Entity(
|
||||
title="Test Entity",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
entity_metadata={"tags": "['documentation', 'tools', 'best-practices']"},
|
||||
content_type="text/markdown",
|
||||
file_path="test/docs.md",
|
||||
@@ -596,7 +596,7 @@ async def test_extract_entity_tags_empty_list(search_service, session_maker):
|
||||
|
||||
entity = Entity(
|
||||
title="Test Entity",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
entity_metadata={"tags": []},
|
||||
content_type="text/markdown",
|
||||
file_path="test/empty-tags.md",
|
||||
@@ -614,7 +614,7 @@ async def test_extract_entity_tags_empty_string(search_service, session_maker):
|
||||
|
||||
entity = Entity(
|
||||
title="Test Entity",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
entity_metadata={"tags": "[]"},
|
||||
content_type="text/markdown",
|
||||
file_path="test/empty-string-tags.md",
|
||||
@@ -632,7 +632,7 @@ async def test_extract_entity_tags_no_metadata(search_service, session_maker):
|
||||
|
||||
entity = Entity(
|
||||
title="Test Entity",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
entity_metadata=None,
|
||||
content_type="text/markdown",
|
||||
file_path="test/no-metadata.md",
|
||||
@@ -650,7 +650,7 @@ async def test_extract_entity_tags_no_tags_key(search_service, session_maker):
|
||||
|
||||
entity = Entity(
|
||||
title="Test Entity",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
entity_metadata={"title": "Some Title", "type": "note"},
|
||||
content_type="text/markdown",
|
||||
file_path="test/no-tags-key.md",
|
||||
@@ -670,7 +670,7 @@ async def test_search_tag_prefix_maps_to_tags_filter(search_service, entity_serv
|
||||
EntitySchema(
|
||||
title="Tagged Note Missing",
|
||||
directory="tags",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="# Tagged Note",
|
||||
entity_metadata={"tags": ["tier1", "alpha"]},
|
||||
)
|
||||
@@ -692,7 +692,7 @@ async def test_search_tag_prefix_with_nonexistent_tag_returns_empty(search_servi
|
||||
EntitySchema(
|
||||
title="Tagged Note",
|
||||
directory="tags",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="# Tagged Note",
|
||||
entity_metadata={"tags": ["tier1", "alpha"]},
|
||||
)
|
||||
@@ -714,7 +714,7 @@ async def test_search_tag_prefix_multiple_tags_requires_all(search_service, enti
|
||||
EntitySchema(
|
||||
title="Multi Tagged Note",
|
||||
directory="tags/multi",
|
||||
entity_type="note",
|
||||
note_type="note",
|
||||
content="# Tagged Note",
|
||||
entity_metadata={"tags": ["tier1", "alpha"]},
|
||||
)
|
||||
@@ -739,7 +739,7 @@ async def test_search_by_frontmatter_tags(search_service, session_maker, test_pr
|
||||
|
||||
entity_data = {
|
||||
"title": "Business Strategy Guide",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"entity_metadata": {"tags": ["business", "strategy", "planning", "organization"]},
|
||||
"content_type": "text/markdown",
|
||||
"file_path": "guides/business-strategy.md",
|
||||
@@ -791,7 +791,7 @@ async def test_search_by_frontmatter_tags_string_format(
|
||||
|
||||
entity_data = {
|
||||
"title": "Documentation Guidelines",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"entity_metadata": {"tags": "['documentation', 'tools', 'best-practices']"},
|
||||
"content_type": "text/markdown",
|
||||
"file_path": "guides/documentation.md",
|
||||
@@ -844,7 +844,7 @@ async def test_search_special_characters_in_title(search_service, session_maker,
|
||||
|
||||
entity_data = {
|
||||
"title": title,
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"entity_metadata": {"tags": ["special", "characters"]},
|
||||
"content_type": "text/markdown",
|
||||
"file_path": f"special/{title}.md",
|
||||
@@ -887,7 +887,7 @@ async def test_search_title_with_parentheses_specific(search_service, session_ma
|
||||
|
||||
entity_data = {
|
||||
"title": "Note (with parentheses)",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"entity_metadata": {"tags": ["test"]},
|
||||
"content_type": "text/markdown",
|
||||
"file_path": "special/Note (with parentheses).md",
|
||||
@@ -923,7 +923,7 @@ async def test_search_title_via_repository_direct(search_service, session_maker,
|
||||
|
||||
entity_data = {
|
||||
"title": "Note (with parentheses)",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"entity_metadata": {"tags": ["test"]},
|
||||
"content_type": "text/markdown",
|
||||
"file_path": "special/Note (with parentheses).md",
|
||||
@@ -971,7 +971,7 @@ async def test_index_entity_with_duplicate_observations(
|
||||
# Create entity
|
||||
entity_data = {
|
||||
"title": "Entity With Duplicate Observations",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"entity_metadata": {},
|
||||
"content_type": "text/markdown",
|
||||
"file_path": "test/duplicate-obs.md",
|
||||
@@ -1026,7 +1026,7 @@ async def test_index_entity_dedupes_observations_by_permalink(
|
||||
# Create entity
|
||||
entity_data = {
|
||||
"title": "Dedupe Test Entity",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"entity_metadata": {},
|
||||
"content_type": "text/markdown",
|
||||
"file_path": "test/dedupe-test.md",
|
||||
@@ -1084,7 +1084,7 @@ async def test_index_entity_multiple_categories_same_content(
|
||||
# Create entity
|
||||
entity_data = {
|
||||
"title": "Multi Category Entity",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"entity_metadata": {},
|
||||
"content_type": "text/markdown",
|
||||
"file_path": "test/multi-category.md",
|
||||
@@ -1145,7 +1145,7 @@ async def test_index_entity_markdown_strips_nul_bytes(search_service, session_ma
|
||||
|
||||
entity_data = {
|
||||
"title": "NUL Test Entity",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"entity_metadata": {},
|
||||
"content_type": "text/markdown",
|
||||
"file_path": "test/nul-test.md",
|
||||
@@ -1184,7 +1184,7 @@ async def test_reindex_vectors(search_service, session_maker, test_project):
|
||||
entity = await entity_repo.create(
|
||||
{
|
||||
"title": f"Vector Test Entity {i}",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"entity_metadata": {},
|
||||
"content_type": "text/markdown",
|
||||
"file_path": f"test/vector-test-{i}.md",
|
||||
@@ -1226,7 +1226,7 @@ async def test_reindex_vectors_no_callback(search_service, session_maker, test_p
|
||||
entity = await entity_repo.create(
|
||||
{
|
||||
"title": "No Callback Entity",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"entity_metadata": {},
|
||||
"content_type": "text/markdown",
|
||||
"file_path": "test/no-callback.md",
|
||||
|
||||
@@ -227,7 +227,7 @@ A test concept.
|
||||
other = Entity(
|
||||
permalink="concept/other",
|
||||
title="Other",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
file_path="concept/other.md",
|
||||
checksum="12345678",
|
||||
content_type="text/markdown",
|
||||
@@ -245,7 +245,7 @@ A test concept.
|
||||
|
||||
# Find new entity
|
||||
test_concept = next(e for e in entities if e.permalink == "concept/test-concept")
|
||||
assert test_concept.entity_type == "knowledge"
|
||||
assert test_concept.note_type == "knowledge"
|
||||
|
||||
# Verify relation was created
|
||||
# with forward link
|
||||
@@ -907,7 +907,7 @@ async def test_sync_null_checksum_cleanup(
|
||||
entity = Entity(
|
||||
permalink="concept/incomplete",
|
||||
title="Incomplete",
|
||||
entity_type="test",
|
||||
note_type="test",
|
||||
file_path="concept/incomplete.md",
|
||||
checksum=None, # Null checksum
|
||||
content_type="text/markdown",
|
||||
@@ -1417,7 +1417,7 @@ This is a test file for race condition handling.
|
||||
# on the "add" call (same effect as the race-condition branch).
|
||||
await sync_service.entity_repository.add(
|
||||
Entity(
|
||||
entity_type="file",
|
||||
note_type="file",
|
||||
file_path=rel_path,
|
||||
checksum="old_checksum",
|
||||
title="Test Race Condition",
|
||||
|
||||
@@ -123,9 +123,9 @@ class ProductionCascadeTest:
|
||||
|
||||
# Create test entity linked to project
|
||||
entity_sql = """
|
||||
INSERT INTO entity (title, entity_type, content_type, project_id, permalink, file_path,
|
||||
INSERT INTO entity (title, note_type, content_type, project_id, permalink, file_path,
|
||||
checksum, created_at, updated_at)
|
||||
VALUES (:title, :entity_type, :content_type, :project_id, :permalink, :file_path,
|
||||
VALUES (:title, :note_type, :content_type, :project_id, :permalink, :file_path,
|
||||
:checksum, :created_at, :updated_at)
|
||||
"""
|
||||
|
||||
@@ -133,7 +133,7 @@ class ProductionCascadeTest:
|
||||
text(entity_sql),
|
||||
{
|
||||
"title": "Cascade Test Entity",
|
||||
"entity_type": "note",
|
||||
"note_type": "note",
|
||||
"content_type": "text/markdown",
|
||||
"project_id": project_id,
|
||||
"permalink": "cascade-test-entity",
|
||||
|
||||
@@ -198,9 +198,11 @@ dependencies = [
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "cst-lsp" },
|
||||
{ name = "freezegun" },
|
||||
{ name = "gevent" },
|
||||
{ name = "icecream" },
|
||||
{ name = "libcst" },
|
||||
{ name = "psycopg" },
|
||||
{ name = "pyright" },
|
||||
{ name = "pytest" },
|
||||
@@ -258,9 +260,11 @@ requires-dist = [
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "cst-lsp", specifier = ">=0.1.3" },
|
||||
{ name = "freezegun", specifier = ">=1.5.5" },
|
||||
{ name = "gevent", specifier = ">=24.11.1" },
|
||||
{ name = "icecream", specifier = ">=2.1.3" },
|
||||
{ name = "libcst", specifier = ">=1.8.6" },
|
||||
{ name = "psycopg", specifier = ">=3.2.0" },
|
||||
{ name = "pyright", specifier = ">=1.1.408" },
|
||||
{ name = "pytest", specifier = ">=8.3.4" },
|
||||
@@ -307,6 +311,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cattrs"
|
||||
version = "26.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a0/ec/ba18945e7d6e55a58364d9fb2e46049c1c2998b3d805f19b703f14e81057/cattrs-26.1.0.tar.gz", hash = "sha256:fa239e0f0ec0715ba34852ce813986dfed1e12117e209b816ab87401271cdd40", size = 495672, upload-time = "2026-02-18T22:15:19.406Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/80/56/60547f7801b97c67e97491dc3d9ade9fbccbd0325058fd3dfcb2f5d98d90/cattrs-26.1.0-py3-none-any.whl", hash = "sha256:d1e0804c42639494d469d08d4f26d6b9de9b8ab26b446db7b5f8c2e97f7c3096", size = 73054, upload-time = "2026-02-18T22:15:17.958Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.1.4"
|
||||
@@ -581,6 +598,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cst-lsp"
|
||||
version = "0.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "libcst" },
|
||||
{ name = "pygls" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/00/7c2a1357e34e78fb18dc3905ea5a32a6ebe2aaa13b696eafce9c8b2fccdc/cst_lsp-0.1.3.tar.gz", hash = "sha256:43df05f53bf76932766d01cad89081a06e3ef7f74a0c7bfab7d37409a884b2c8", size = 13881, upload-time = "2024-09-04T00:53:48.299Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/3e/98423e2dcb855c21544fbb40600159d81d553f45991123b20b52ad23f696/cst_lsp-0.1.3-py3-none-any.whl", hash = "sha256:251d0958b9943d6031424e859740cdfd7e3f8b817943387e68c2c48d9bdc1bd0", size = 16339, upload-time = "2024-09-04T00:53:47.446Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cyclopts"
|
||||
version = "4.4.4"
|
||||
@@ -1365,6 +1395,58 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libcst"
|
||||
version = "1.8.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyyaml", marker = "python_full_version != '3.13.*'" },
|
||||
{ name = "pyyaml-ft", marker = "python_full_version == '3.13.*'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/de/cd/337df968b38d94c5aabd3e1b10630f047a2b345f6e1d4456bd9fe7417537/libcst-1.8.6.tar.gz", hash = "sha256:f729c37c9317126da9475bdd06a7208eb52fcbd180a6341648b45a56b4ba708b", size = 891354, upload-time = "2025-11-03T22:33:30.621Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/3c/93365c17da3d42b055a8edb0e1e99f1c60c776471db6c9b7f1ddf6a44b28/libcst-1.8.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0c13d5bd3d8414a129e9dccaf0e5785108a4441e9b266e1e5e9d1f82d1b943c9", size = 2206166, upload-time = "2025-11-03T22:32:16.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/cb/7530940e6ac50c6dd6022349721074e19309eb6aa296e942ede2213c1a19/libcst-1.8.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1472eeafd67cdb22544e59cf3bfc25d23dc94058a68cf41f6654ff4fcb92e09", size = 2083726, upload-time = "2025-11-03T22:32:17.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/cf/7e5eaa8c8f2c54913160671575351d129170db757bb5e4b7faffed022271/libcst-1.8.6-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:089c58e75cb142ec33738a1a4ea7760a28b40c078ab2fd26b270dac7d2633a4d", size = 2235755, upload-time = "2025-11-03T22:32:18.859Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/54/570ec2b0e9a3de0af9922e3bb1b69a5429beefbc753a7ea770a27ad308bd/libcst-1.8.6-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c9d7aeafb1b07d25a964b148c0dda9451efb47bbbf67756e16eeae65004b0eb5", size = 2301473, upload-time = "2025-11-03T22:32:20.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/4c/163457d1717cd12181c421a4cca493454bcabd143fc7e53313bc6a4ad82a/libcst-1.8.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:207481197afd328aa91d02670c15b48d0256e676ce1ad4bafb6dc2b593cc58f1", size = 2298899, upload-time = "2025-11-03T22:32:21.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/1d/317ddef3669883619ef3d3395ea583305f353ef4ad87d7a5ac1c39be38e3/libcst-1.8.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:375965f34cc6f09f5f809244d3ff9bd4f6cb6699f571121cebce53622e7e0b86", size = 2408239, upload-time = "2025-11-03T22:32:23.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/a1/f47d8cccf74e212dd6044b9d6dbc223636508da99acff1d54786653196bc/libcst-1.8.6-cp312-cp312-win_amd64.whl", hash = "sha256:da95b38693b989eaa8d32e452e8261cfa77fe5babfef1d8d2ac25af8c4aa7e6d", size = 2119660, upload-time = "2025-11-03T22:32:24.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/d0/dd313bf6a7942cdf951828f07ecc1a7695263f385065edc75ef3016a3cb5/libcst-1.8.6-cp312-cp312-win_arm64.whl", hash = "sha256:bff00e1c766658adbd09a175267f8b2f7616e5ee70ce45db3d7c4ce6d9f6bec7", size = 1999824, upload-time = "2025-11-03T22:32:26.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/01/723cd467ec267e712480c772aacc5aa73f82370c9665162fd12c41b0065b/libcst-1.8.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7445479ebe7d1aff0ee094ab5a1c7718e1ad78d33e3241e1a1ec65dcdbc22ffb", size = 2206386, upload-time = "2025-11-03T22:32:27.422Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/50/b944944f910f24c094f9b083f76f61e3985af5a376f5342a21e01e2d1a81/libcst-1.8.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4fc3fef8a2c983e7abf5d633e1884c5dd6fa0dcb8f6e32035abd3d3803a3a196", size = 2083945, upload-time = "2025-11-03T22:32:28.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/a1/bd1b2b2b7f153d82301cdaddba787f4a9fc781816df6bdb295ca5f88b7cf/libcst-1.8.6-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:1a3a5e4ee870907aa85a4076c914ae69066715a2741b821d9bf16f9579de1105", size = 2235818, upload-time = "2025-11-03T22:32:30.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/ab/f5433988acc3b4d188c4bb154e57837df9488cc9ab551267cdeabd3bb5e7/libcst-1.8.6-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6609291c41f7ad0bac570bfca5af8fea1f4a27987d30a1fa8b67fe5e67e6c78d", size = 2301289, upload-time = "2025-11-03T22:32:31.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/57/89f4ba7a6f1ac274eec9903a9e9174890d2198266eee8c00bc27eb45ecf7/libcst-1.8.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25eaeae6567091443b5374b4c7d33a33636a2d58f5eda02135e96fc6c8807786", size = 2299230, upload-time = "2025-11-03T22:32:33.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/36/0aa693bc24cce163a942df49d36bf47a7ed614a0cd5598eee2623bc31913/libcst-1.8.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04030ea4d39d69a65873b1d4d877def1c3951a7ada1824242539e399b8763d30", size = 2408519, upload-time = "2025-11-03T22:32:34.678Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/18/6dd055b5f15afa640fb3304b2ee9df8b7f72e79513814dbd0a78638f4a0e/libcst-1.8.6-cp313-cp313-win_amd64.whl", hash = "sha256:8066f1b70f21a2961e96bedf48649f27dfd5ea68be5cd1bed3742b047f14acde", size = 2119853, upload-time = "2025-11-03T22:32:36.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/ed/5ddb2a22f0b0abdd6dcffa40621ada1feaf252a15e5b2733a0a85dfd0429/libcst-1.8.6-cp313-cp313-win_arm64.whl", hash = "sha256:c188d06b583900e662cd791a3f962a8c96d3dfc9b36ea315be39e0a4c4792ebf", size = 1999808, upload-time = "2025-11-03T22:32:38.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/d3/72b2de2c40b97e1ef4a1a1db4e5e52163fc7e7740ffef3846d30bc0096b5/libcst-1.8.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c41c76e034a1094afed7057023b1d8967f968782433f7299cd170eaa01ec033e", size = 2190553, upload-time = "2025-11-03T22:32:39.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/20/983b7b210ccc3ad94a82db54230e92599c4a11b9cfc7ce3bc97c1d2df75c/libcst-1.8.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5432e785322aba3170352f6e72b32bea58d28abd141ac37cc9b0bf6b7c778f58", size = 2074717, upload-time = "2025-11-03T22:32:41.373Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/f2/9e01678fedc772e09672ed99930de7355757035780d65d59266fcee212b8/libcst-1.8.6-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:85b7025795b796dea5284d290ff69de5089fc8e989b25d6f6f15b6800be7167f", size = 2225834, upload-time = "2025-11-03T22:32:42.716Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/0d/7bed847b5c8c365e9f1953da274edc87577042bee5a5af21fba63276e756/libcst-1.8.6-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:536567441182a62fb706e7aa954aca034827b19746832205953b2c725d254a93", size = 2287107, upload-time = "2025-11-03T22:32:44.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/f0/7e51fa84ade26c518bfbe7e2e4758b56d86a114c72d60309ac0d350426c4/libcst-1.8.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f04d3672bde1704f383a19e8f8331521abdbc1ed13abb349325a02ac56e5012", size = 2288672, upload-time = "2025-11-03T22:32:45.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/cd/15762659a3f5799d36aab1bc2b7e732672722e249d7800e3c5f943b41250/libcst-1.8.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f04febcd70e1e67917be7de513c8d4749d2e09206798558d7fe632134426ea4", size = 2392661, upload-time = "2025-11-03T22:32:47.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/6b/b7f9246c323910fcbe021241500f82e357521495dcfe419004dbb272c7cb/libcst-1.8.6-cp313-cp313t-win_amd64.whl", hash = "sha256:1dc3b897c8b0f7323412da3f4ad12b16b909150efc42238e19cbf19b561cc330", size = 2105068, upload-time = "2025-11-03T22:32:49.145Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/0b/4fd40607bc4807ec2b93b054594373d7fa3d31bb983789901afcb9bcebe9/libcst-1.8.6-cp313-cp313t-win_arm64.whl", hash = "sha256:44f38139fa95e488db0f8976f9c7ca39a64d6bc09f2eceef260aa1f6da6a2e42", size = 1985181, upload-time = "2025-11-03T22:32:50.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/60/4105441989e321f7ad0fd28ffccb83eb6aac0b7cfb0366dab855dcccfbe5/libcst-1.8.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b188e626ce61de5ad1f95161b8557beb39253de4ec74fc9b1f25593324a0279c", size = 2204202, upload-time = "2025-11-03T22:32:52.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/2f/51a6f285c3a183e50cfe5269d4a533c21625aac2c8de5cdf2d41f079320d/libcst-1.8.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:87e74f7d7dfcba9efa91127081e22331d7c42515f0a0ac6e81d4cf2c3ed14661", size = 2083581, upload-time = "2025-11-03T22:32:54.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/64/921b1c19b638860af76cdb28bc81d430056592910b9478eea49e31a7f47a/libcst-1.8.6-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:3a926a4b42015ee24ddfc8ae940c97bd99483d286b315b3ce82f3bafd9f53474", size = 2236495, upload-time = "2025-11-03T22:32:55.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/a8/b00592f9bede618cbb3df6ffe802fc65f1d1c03d48a10d353b108057d09c/libcst-1.8.6-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:3f4fbb7f569e69fd9e89d9d9caa57ca42c577c28ed05062f96a8c207594e75b8", size = 2301466, upload-time = "2025-11-03T22:32:57.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/df/790d9002f31580fefd0aec2f373a0f5da99070e04c5e8b1c995d0104f303/libcst-1.8.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:08bd63a8ce674be431260649e70fca1d43f1554f1591eac657f403ff8ef82c7a", size = 2300264, upload-time = "2025-11-03T22:32:58.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/de/dc3f10e65bab461be5de57850d2910a02c24c3ddb0da28f0e6e4133c3487/libcst-1.8.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e00e275d4ba95d4963431ea3e409aa407566a74ee2bf309a402f84fc744abe47", size = 2408572, upload-time = "2025-11-03T22:33:00.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/3b/35645157a7590891038b077db170d6dd04335cd2e82a63bdaa78c3297dfe/libcst-1.8.6-cp314-cp314-win_amd64.whl", hash = "sha256:fea5c7fa26556eedf277d4f72779c5ede45ac3018650721edd77fd37ccd4a2d4", size = 2193917, upload-time = "2025-11-03T22:33:02.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/a2/1034a9ba7d3e82f2c2afaad84ba5180f601aed676d92b76325797ad60951/libcst-1.8.6-cp314-cp314-win_arm64.whl", hash = "sha256:bb9b4077bdf8857b2483879cbbf70f1073bc255b057ec5aac8a70d901bb838e9", size = 2078748, upload-time = "2025-11-03T22:33:03.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/a1/30bc61e8719f721a5562f77695e6154e9092d1bdf467aa35d0806dcd6cea/libcst-1.8.6-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:55ec021a296960c92e5a33b8d93e8ad4182b0eab657021f45262510a58223de1", size = 2188980, upload-time = "2025-11-03T22:33:05.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/14/c660204532407c5628e3b615015a902ed2d0b884b77714a6bdbe73350910/libcst-1.8.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ba9ab2b012fbd53b36cafd8f4440a6b60e7e487cd8b87428e57336b7f38409a4", size = 2074828, upload-time = "2025-11-03T22:33:06.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/e2/c497c354943dff644749f177ee9737b09ed811b8fc842b05709a40fe0d1b/libcst-1.8.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c0a0cc80aebd8aa15609dd4d330611cbc05e9b4216bcaeabba7189f99ef07c28", size = 2225568, upload-time = "2025-11-03T22:33:08.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/ef/45999676d07bd6d0eefa28109b4f97124db114e92f9e108de42ba46a8028/libcst-1.8.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:42a4f68121e2e9c29f49c97f6154e8527cd31021809cc4a941c7270aa64f41aa", size = 2286523, upload-time = "2025-11-03T22:33:10.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/6c/517d8bf57d9f811862f4125358caaf8cd3320a01291b3af08f7b50719db4/libcst-1.8.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a434c521fadaf9680788b50d5c21f4048fa85ed19d7d70bd40549fbaeeecab1", size = 2288044, upload-time = "2025-11-03T22:33:11.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/ce/24d7d49478ffb61207f229239879845da40a374965874f5ee60f96b02ddb/libcst-1.8.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6a65f844d813ab4ef351443badffa0ae358f98821561d19e18b3190f59e71996", size = 2392605, upload-time = "2025-11-03T22:33:12.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/c3/829092ead738b71e96a4e96896c96f276976e5a8a58b4473ed813d7c962b/libcst-1.8.6-cp314-cp314t-win_amd64.whl", hash = "sha256:bdb14bc4d4d83a57062fed2c5da93ecb426ff65b0dc02ddf3481040f5f074a82", size = 2181581, upload-time = "2025-11-03T22:33:14.514Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/6d/5d6a790a02eb0d9d36c4aed4f41b277497e6178900b2fa29c35353aa45ed/libcst-1.8.6-cp314-cp314t-win_arm64.whl", hash = "sha256:819c8081e2948635cab60c603e1bbdceccdfe19104a242530ad38a36222cb88f", size = 2065000, upload-time = "2025-11-03T22:33:16.257Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loguru"
|
||||
version = "0.7.3"
|
||||
@@ -1378,6 +1460,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lsprotocol"
|
||||
version = "2025.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
{ name = "cattrs" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e9/26/67b84e6ec1402f0e6764ef3d2a0aaf9a79522cc1d37738f4e5bb0b21521a/lsprotocol-2025.0.0.tar.gz", hash = "sha256:e879da2b9301e82cfc3e60d805630487ac2f7ab17492f4f5ba5aaba94fe56c29", size = 74896, upload-time = "2025-06-17T21:30:18.156Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/f0/92f2d609d6642b5f30cb50a885d2bf1483301c69d5786286500d15651ef2/lsprotocol-2025.0.0-py3-none-any.whl", hash = "sha256:f9d78f25221f2a60eaa4a96d3b4ffae011b107537facee61d3da3313880995c7", size = 76250, upload-time = "2025-06-17T21:30:19.455Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mako"
|
||||
version = "1.3.10"
|
||||
@@ -2125,6 +2220,20 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygls"
|
||||
version = "2.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
{ name = "cattrs" },
|
||||
{ name = "lsprotocol" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/94/cce3560f6c7296be43bd67ba342f8972b8adddfe407b62b25d1fb90c514b/pygls-2.0.1.tar.gz", hash = "sha256:2f774a669fbe2ece977d302786f01f9b0c5df7d0204ea0fa371ecb08288d6b86", size = 55796, upload-time = "2026-01-26T23:04:33.8Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/48/8b/d0d7e51f928ec6e555c943f7c07a712b992e9cf061d52cbf37fbf8622c05/pygls-2.0.1-py3-none-any.whl", hash = "sha256:d29748042cea5bedc98285eb3e2c0c60bf3fc73786319519001bf72bbe8f36cc", size = 69536, upload-time = "2026-01-26T23:04:35.197Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
@@ -2391,6 +2500,30 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml-ft"
|
||||
version = "8.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/eb/5a0d575de784f9a1f94e2b1288c6886f13f34185e13117ed530f32b6f8a8/pyyaml_ft-8.0.0.tar.gz", hash = "sha256:0c947dce03954c7b5d38869ed4878b2e6ff1d44b08a0d84dc83fdad205ae39ab", size = 141057, upload-time = "2025-06-10T15:32:15.613Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/68/ba/a067369fe61a2e57fb38732562927d5bae088c73cb9bb5438736a9555b29/pyyaml_ft-8.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c1306282bc958bfda31237f900eb52c9bedf9b93a11f82e1aab004c9a5657a6", size = 187027, upload-time = "2025-06-10T15:31:48.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/c5/a3d2020ce5ccfc6aede0d45bcb870298652ac0cf199f67714d250e0cdf39/pyyaml_ft-8.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30c5f1751625786c19de751e3130fc345ebcba6a86f6bddd6e1285342f4bbb69", size = 176146, upload-time = "2025-06-10T15:31:50.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/bb/23a9739291086ca0d3189eac7cd92b4d00e9fdc77d722ab610c35f9a82ba/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fa992481155ddda2e303fcc74c79c05eddcdbc907b888d3d9ce3ff3e2adcfb0", size = 746792, upload-time = "2025-06-10T15:31:52.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/c2/e8825f4ff725b7e560d62a3609e31d735318068e1079539ebfde397ea03e/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cec6c92b4207004b62dfad1f0be321c9f04725e0f271c16247d8b39c3bf3ea42", size = 786772, upload-time = "2025-06-10T15:31:54.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/be/58a4dcae8854f2fdca9b28d9495298fd5571a50d8430b1c3033ec95d2d0e/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06237267dbcab70d4c0e9436d8f719f04a51123f0ca2694c00dd4b68c338e40b", size = 778723, upload-time = "2025-06-10T15:31:56.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/ed/fed0da92b5d5d7340a082e3802d84c6dc9d5fa142954404c41a544c1cb92/pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a7f332bc565817644cdb38ffe4739e44c3e18c55793f75dddb87630f03fc254", size = 758478, upload-time = "2025-06-10T15:31:58.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/69/ac02afe286275980ecb2dcdc0156617389b7e0c0a3fcdedf155c67be2b80/pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d10175a746be65f6feb86224df5d6bc5c049ebf52b89a88cf1cd78af5a367a8", size = 799159, upload-time = "2025-06-10T15:31:59.675Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/ac/c492a9da2e39abdff4c3094ec54acac9747743f36428281fb186a03fab76/pyyaml_ft-8.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:58e1015098cf8d8aec82f360789c16283b88ca670fe4275ef6c48c5e30b22a96", size = 158779, upload-time = "2025-06-10T15:32:01.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/9b/41998df3298960d7c67653669f37710fa2d568a5fc933ea24a6df60acaf6/pyyaml_ft-8.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5f3e2ceb790d50602b2fd4ec37abbd760a8c778e46354df647e7c5a4ebb", size = 191331, upload-time = "2025-06-10T15:32:02.602Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/16/2710c252ee04cbd74d9562ebba709e5a284faeb8ada88fcda548c9191b47/pyyaml_ft-8.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d445bf6ea16bb93c37b42fdacfb2f94c8e92a79ba9e12768c96ecde867046d1", size = 182879, upload-time = "2025-06-10T15:32:04.466Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/40/ae8163519d937fa7bfa457b6f78439cc6831a7c2b170e4f612f7eda71815/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c56bb46b4fda34cbb92a9446a841da3982cdde6ea13de3fbd80db7eeeab8b49", size = 811277, upload-time = "2025-06-10T15:32:06.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/66/28d82dbff7f87b96f0eeac79b7d972a96b4980c1e445eb6a857ba91eda00/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab0abb46eb1780da486f022dce034b952c8ae40753627b27a626d803926483b", size = 831650, upload-time = "2025-06-10T15:32:08.076Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/df/161c4566facac7d75a9e182295c223060373d4116dead9cc53a265de60b9/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd48d639cab5ca50ad957b6dd632c7dd3ac02a1abe0e8196a3c24a52f5db3f7a", size = 815755, upload-time = "2025-06-10T15:32:09.435Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/10/f42c48fa5153204f42eaa945e8d1fd7c10d6296841dcb2447bf7da1be5c4/pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:052561b89d5b2a8e1289f326d060e794c21fa068aa11255fe71d65baf18a632e", size = 810403, upload-time = "2025-06-10T15:32:11.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/d2/e369064aa51009eb9245399fd8ad2c562bd0bcd392a00be44b2a824ded7c/pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3bb4b927929b0cb162fb1605392a321e3333e48ce616cdcfa04a839271373255", size = 835581, upload-time = "2025-06-10T15:32:12.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/28/26534bed77109632a956977f60d8519049f545abc39215d086e33a61f1f2/pyyaml_ft-8.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793", size = 171579, upload-time = "2025-06-10T15:32:14.34Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "referencing"
|
||||
version = "0.36.2"
|
||||
|
||||
Reference in New Issue
Block a user