fix(tolaria): C5F Phase B — search-source indexability path-independent (valid C3 object_id suffices; LEGACY kept; out-of-scope dirs no longer auto-exclude indexable knowledge)

This commit is contained in:
Red Queen 2026-08-26 13:05:13 +00:00
parent 5e4191578a
commit 12635e8672
2 changed files with 86 additions and 12 deletions

View file

@ -81,6 +81,7 @@ from rq_c5b import (
content_hash,
detect_secret,
KnowledgeScope,
LEGACY_SPECIAL_PATHS,
SCOPE_IN_SCOPE,
SCOPE_LEGACY_SPECIAL,
)
@ -281,6 +282,17 @@ class SearchSourceBuilder:
Liest den Vault (read-only) und klassifiziert die indexierbaren Objekte.
Rueckgabe: {"indexable": [{path, id, content, fm, body}],
"excluded": [...], "vault_object_count": int}
INDEXIERBARKEIT ist PFAD-INDEPENDENT (C5F Phase B):
Ein Vault-Objekt ist indexierbar, wenn es
* eine gueltige C3 object/<FULL_UUID>-ID im Frontmatter traegt
(extract_object_id) ODER
* ein expliziter LEGACY_SPECIAL-Sonderfall ist (README.md x2, vps.md;
ohne C3-ID, KEEP_DISTINCT / DEFERRED).
Der Pfad (z.B. notes/trading/second-brain/, notes/ai-agents/) ist dabei
NICHT mehr ausschlaggebend jedes Markdown mit gueltiger object_id ist
ein Knowledge-Objekt und wird indexiert. Objekte OHNE gueltige object_id
(ausser Legacy) sind nicht indexierbar -> excluded (HUMAN_REVIEW/Orphan).
"""
entries = self.tolaria.list(VAULT_PREFIX) or []
indexable: List[Dict[str, Any]] = []
@ -294,18 +306,15 @@ class SearchSourceBuilder:
if content is None:
excluded.append({"path": rel, "reason": "unreadable"})
continue
cls = self.scope.classify_with_content(rel, content)
sc = cls.get("scope")
if sc == SCOPE_IN_SCOPE:
oid = cls.get("object_id")
# Pfad-unabhaengige Indexierbarkeit
if rel in LEGACY_SPECIAL_PATHS:
indexable.append({"path": rel, "id": None, "content": content})
continue
oid = extract_object_id(content)
if oid:
indexable.append({"path": rel, "id": oid, "content": content})
continue
excluded.append({"path": rel, "reason": "no_object_id"})
elif sc == SCOPE_LEGACY_SPECIAL:
indexable.append({"path": rel, "id": None, "content": content})
else:
excluded.append({"path": rel, "reason": sc})
return {"indexable": indexable, "excluded": excluded,
"vault_object_count": len(entries)}

View file

@ -678,5 +678,70 @@ class TestRealisticIntegration(unittest.TestCase):
self.assertGreater(base["total"], 0)
class TestPhaseB_PathIndependentScope(unittest.TestCase):
"""C5F Phase B: Indexierbarkeit ist pfad-unabhaengig (gültige C3-ID reicht)."""
def _collect(self, fake, src=None):
import tempfile as _tf
if src is None:
src = os.path.join(_tf.mkdtemp(prefix="c5fb_"), "source.json")
builder = _make_builder(fake, src)
return builder._collect_indexable()
def test_object_under_second_brain_with_valid_id_indexed(self):
# Objekt unter notes/trading/second-brain/ (vorher OUT_OF_SCOPE via Pfad)
# mit gültiger C3-ID muss jetzt indexiert werden.
fake = FakeTolaria()
fake.vault[_vault_path("notes/trading/second-brain/regelwerk.md")] = (
"---\nkid: 1\nid: " + UUID_A + "\ntitle: Regelwerk\n---\nbody")
r = self._collect(fake)
paths = [o["path"] for o in r["indexable"]]
ids = [o["id"] for o in r["indexable"]]
self.assertIn("notes/trading/second-brain/regelwerk.md", paths)
self.assertIn(UUID_A, ids)
def test_object_under_ai_agents_with_valid_id_indexed(self):
fake = FakeTolaria()
fake.vault[_vault_path("notes/ai-agents/reference.md")] = (
"---\nkid: 1\nid: " + UUID_B + "\ntitle: Ref\n---\nbody")
r = self._collect(fake)
paths = [o["path"] for o in r["indexable"]]
self.assertIn("notes/ai-agents/reference.md", paths)
def test_unknown_file_without_id_excluded(self):
# Unbekannte Nicht-Knowledge-Datei OHNE gültige C3-ID -> ausgeschlossen,
# auch wenn sie .md ist.
fake = FakeTolaria()
fake.vault[_vault_path("notes/inbox/notizen.md")] = "kein frontmatter id"
r = self._collect(fake)
paths = [o["path"] for o in r["indexable"]]
excl = [e["path"] for e in r["excluded"]]
self.assertNotIn("notes/inbox/notizen.md", paths)
self.assertIn("notes/inbox/notizen.md", excl)
def test_legacy_special_indexed_without_id(self):
# LEGACY_SPECIAL (z.B. vps.md) bleibt ohne C3-ID indexierbar.
fake = FakeTolaria()
fake.vault[_vault_path("vps.md")] = "---\ntitle: VPS\n---\nbody"
r = self._collect(fake)
paths = [o["path"] for o in r["indexable"]]
legacy = [o for o in r["indexable"] if o["id"] is None]
self.assertIn("vps.md", paths)
self.assertTrue(any(o["path"] == "vps.md" for o in legacy))
def test_mixed_scope_total_count(self):
# Mixed: Root mit ID + second-brain mit ID + Notiz ohne ID + Legacy.
fake = FakeTolaria()
fake.vault[_vault_path("modul-09.md")] = (
"---\nkid: 1\nid: " + UUID_A + "\ntitle: M\n---\nbody")
fake.vault[_vault_path("notes/trading/second-brain/x.md")] = (
"---\nkid: 1\nid: " + UUID_B + "\ntitle: X\n---\nbody")
fake.vault[_vault_path("notes/inbox/notiz.md")] = "kein fm id"
fake.vault[_vault_path("README.md")] = "---\ntitle: R\n---\nbody"
r = self._collect(fake)
self.assertEqual(len(r["indexable"]), 3) # 2 mit ID + 1 Legacy
self.assertEqual(len(r["excluded"]), 1) # Notiz ohne ID
if __name__ == "__main__":
unittest.main()