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:
parent
5e4191578a
commit
12635e8672
2 changed files with 86 additions and 12 deletions
|
|
@ -81,6 +81,7 @@ from rq_c5b import (
|
||||||
content_hash,
|
content_hash,
|
||||||
detect_secret,
|
detect_secret,
|
||||||
KnowledgeScope,
|
KnowledgeScope,
|
||||||
|
LEGACY_SPECIAL_PATHS,
|
||||||
SCOPE_IN_SCOPE,
|
SCOPE_IN_SCOPE,
|
||||||
SCOPE_LEGACY_SPECIAL,
|
SCOPE_LEGACY_SPECIAL,
|
||||||
)
|
)
|
||||||
|
|
@ -281,6 +282,17 @@ class SearchSourceBuilder:
|
||||||
Liest den Vault (read-only) und klassifiziert die indexierbaren Objekte.
|
Liest den Vault (read-only) und klassifiziert die indexierbaren Objekte.
|
||||||
Rueckgabe: {"indexable": [{path, id, content, fm, body}],
|
Rueckgabe: {"indexable": [{path, id, content, fm, body}],
|
||||||
"excluded": [...], "vault_object_count": int}
|
"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 []
|
entries = self.tolaria.list(VAULT_PREFIX) or []
|
||||||
indexable: List[Dict[str, Any]] = []
|
indexable: List[Dict[str, Any]] = []
|
||||||
|
|
@ -294,18 +306,15 @@ class SearchSourceBuilder:
|
||||||
if content is None:
|
if content is None:
|
||||||
excluded.append({"path": rel, "reason": "unreadable"})
|
excluded.append({"path": rel, "reason": "unreadable"})
|
||||||
continue
|
continue
|
||||||
cls = self.scope.classify_with_content(rel, content)
|
# Pfad-unabhaengige Indexierbarkeit
|
||||||
sc = cls.get("scope")
|
if rel in LEGACY_SPECIAL_PATHS:
|
||||||
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:
|
|
||||||
indexable.append({"path": rel, "id": None, "content": content})
|
indexable.append({"path": rel, "id": None, "content": content})
|
||||||
else:
|
continue
|
||||||
excluded.append({"path": rel, "reason": sc})
|
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,
|
return {"indexable": indexable, "excluded": excluded,
|
||||||
"vault_object_count": len(entries)}
|
"vault_object_count": len(entries)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -678,5 +678,70 @@ class TestRealisticIntegration(unittest.TestCase):
|
||||||
self.assertGreater(base["total"], 0)
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
Loading…
Reference in a new issue