fix(file-index): prune removed active assets on rebuild
setup / install-leaves-tree-clean (push) Canceled after 0s
setup / install-leaves-tree-clean (push) Canceled after 0s
Treat the source registry as authoritative during file-index rebuilds while preserving staged uploads. This prevents deleted archived sources from remaining searchable. Tests: python3 -m unittest tests.test_file_assets tests.test_business_facts
This commit is contained in:
@@ -185,9 +185,8 @@ def rebuild_from_registry(vault_path: str | Path, *, db_path: Path | None = None
|
||||
if not registry_path.is_file():
|
||||
raise FileNotFoundError(f"source registry not found: {registry_path}")
|
||||
|
||||
indexed = 0
|
||||
records: list[dict[str, Any]] = []
|
||||
skipped = 0
|
||||
with connect(db_path) as conn:
|
||||
for raw in registry_path.read_text(encoding="utf-8").splitlines():
|
||||
if not raw.strip():
|
||||
continue
|
||||
@@ -196,10 +195,21 @@ def rebuild_from_registry(vault_path: str | Path, *, db_path: Path | None = None
|
||||
except json.JSONDecodeError:
|
||||
skipped += 1
|
||||
continue
|
||||
if record.get("record_type") != "source_archive":
|
||||
continue
|
||||
if record.get("record_type") == "source_archive":
|
||||
records.append(record)
|
||||
|
||||
with connect(db_path) as conn:
|
||||
# A rebuild is authoritative for archived sources. Preserve uploaded
|
||||
# staging rows, but discard active rows that no longer have a registry
|
||||
# record so deleted sources cannot remain searchable.
|
||||
conn.execute(
|
||||
"DELETE FROM file_assets_fts WHERE file_id IN (SELECT file_id FROM file_assets WHERE status = 'active')"
|
||||
)
|
||||
conn.execute("DELETE FROM file_assets WHERE status = 'active'")
|
||||
for record in records:
|
||||
upsert_asset(conn, asset_from_registry_record(record), status="active")
|
||||
indexed += 1
|
||||
conn.execute("DELETE FROM file_assets_fts WHERE file_id NOT IN (SELECT file_id FROM file_assets)")
|
||||
indexed = len(records)
|
||||
return RebuildResult(db_path=db_path, registry_path=registry_path, indexed=indexed, skipped=skipped)
|
||||
|
||||
|
||||
|
||||
@@ -55,6 +55,32 @@ class FileAssetIndexTests(unittest.TestCase):
|
||||
self.assertEqual(json.loads(rows[0]["pitch"]), ["0.5"])
|
||||
self.assertEqual(json.loads(rows[0]["pin_count"]), ["18"])
|
||||
|
||||
def test_rebuild_removes_active_records_removed_from_registry(self):
|
||||
retained_id = "sha256:" + "c" * 64
|
||||
removed_id = "sha256:" + "d" * 64
|
||||
self._write_registry_record(
|
||||
source_id=retained_id,
|
||||
archive_path="_sources/archive/bjd/pdf/retained.pdf",
|
||||
original_name="retained.pdf",
|
||||
title="Retained source",
|
||||
)
|
||||
self._write_registry_record(
|
||||
source_id=removed_id,
|
||||
archive_path="_sources/archive/bjd/xlsx/removed.xlsx",
|
||||
original_name="removed.xlsx",
|
||||
title="Removed source",
|
||||
)
|
||||
rebuild_from_registry(self.vault, db_path=self.db_path)
|
||||
|
||||
retained_record = json.loads(self.registry_path.read_text(encoding="utf-8").splitlines()[0])
|
||||
self.registry_path.write_text(json.dumps(retained_record) + "\n", encoding="utf-8")
|
||||
rebuild_from_registry(self.vault, db_path=self.db_path)
|
||||
|
||||
remaining = search_assets(self.vault, "retained", db_path=self.db_path, limit=5, read_only=True)
|
||||
removed = search_assets(self.vault, "removed", db_path=self.db_path, limit=5, read_only=True)
|
||||
self.assertEqual([row["file_id"] for row in remaining], [retained_id])
|
||||
self.assertEqual(removed, [])
|
||||
|
||||
def _write_registry_record(self, **fields):
|
||||
record = {
|
||||
"record_type": "source_archive",
|
||||
|
||||
Reference in New Issue
Block a user