From 12635e8672307d69fb73fbe9ad140607a3694d48 Mon Sep 17 00:00:00 2001 From: Red Queen Date: Wed, 26 Aug 2026 13:05:13 +0000 Subject: [PATCH] =?UTF-8?q?fix(tolaria):=20C5F=20Phase=20B=20=E2=80=94=20s?= =?UTF-8?q?earch-source=20indexability=20path-independent=20(valid=20C3=20?= =?UTF-8?q?object=5Fid=20suffices;=20LEGACY=20kept;=20out-of-scope=20dirs?= =?UTF-8?q?=20no=20longer=20auto-exclude=20indexable=20knowledge)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tolaria/c5-sync-service/rq_c5d.py | 31 ++++++--- .../test_c5d_source_pipeline.py | 67 ++++++++++++++++++- 2 files changed, 86 insertions(+), 12 deletions(-) diff --git a/tolaria/c5-sync-service/rq_c5d.py b/tolaria/c5-sync-service/rq_c5d.py index bc0f019..d7cfb0e 100644 --- a/tolaria/c5-sync-service/rq_c5d.py +++ b/tolaria/c5-sync-service/rq_c5d.py @@ -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/-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") - if oid: - indexable.append({"path": rel, "id": oid, "content": content}) - continue - excluded.append({"path": rel, "reason": "no_object_id"}) - elif sc == SCOPE_LEGACY_SPECIAL: + # Pfad-unabhaengige Indexierbarkeit + if rel in LEGACY_SPECIAL_PATHS: indexable.append({"path": rel, "id": None, "content": content}) - else: - excluded.append({"path": rel, "reason": sc}) + 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"}) return {"indexable": indexable, "excluded": excluded, "vault_object_count": len(entries)} diff --git a/tolaria/c5-sync-service/test_c5d_source_pipeline.py b/tolaria/c5-sync-service/test_c5d_source_pipeline.py index 390dee1..f2bec80 100644 --- a/tolaria/c5-sync-service/test_c5d_source_pipeline.py +++ b/tolaria/c5-sync-service/test_c5d_source_pipeline.py @@ -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() + unittest.main() \ No newline at end of file