Files
583350034e feat(knowledge-base): add the Knowledge Base loader (#7) (#53)
Implement Knowledge Base loading and indexing per MASTER_SPEC §15.11, §27.3 in
compiler/common/knowledge_base.py.

- KnowledgeBaseEntry: frozen model of a canonical concept.
- KnowledgeBase: immutable, with id->entry and alias->id indexes and
  get / resolve_alias / lookup accessors (read-only mappings).
- load_knowledge_base: reads every *.json (array of entries) from a directory,
  delegates full validation to validate_knowledge_base (#9), and on any finding
  raises Fatal SC0004 KnowledgeBaseError with the findings attached -- indexes
  are built only after validation passes, so a failed load leaves no partial
  state.
- KnowledgeBaseLoader: caches the Knowledge Base after first get(); reload() is
  explicit and side-effect-free on failure (the previous KB is kept). No
  automatic file watching. Optional structured logger.

Tests: multi-file load, id/alias lookups, expand/deprecated preservation, empty
dir, invalid KB / duplicate id / invalid JSON -> SC0004, caching identity,
reload picks up changes, reload failure keeps the previous KB, and immutability.

Co-authored-by: kunail0804 <kunail0804@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 19:36:31 +02:00

154 lines
4.9 KiB
Python

"""Tests for the Knowledge Base loader (issue #7, §15.11, §27.3)."""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from compiler.common.knowledge_base import (
KnowledgeBase,
KnowledgeBaseError,
KnowledgeBaseLoader,
load_knowledge_base,
)
from compiler.common.result import Severity
def write_kb(directory: Path, **files: list[dict]) -> Path:
for name, entries in files.items():
(directory / f"{name}.json").write_text(json.dumps(entries), encoding="utf-8")
return directory
def valid_files() -> dict[str, list[dict]]:
return {
"hair": [
{
"id": "long_hair",
"aliases": ["lengthy hair"],
"tags": ["long hair"],
"category": "hair",
},
{
"id": "twin_braids",
"aliases": ["double braids"],
"tags": ["twin braids"],
"category": "hair",
"expand": ["long_hair"],
},
],
"eyes": [
{
"id": "blue_eyes",
"aliases": ["azure eyes"],
"tags": ["blue eyes"],
"category": "eyes",
},
],
}
# --- successful load --------------------------------------------------------
def test_loads_all_files(tmp_path) -> None:
kb = load_knowledge_base(write_kb(tmp_path, **valid_files()))
assert isinstance(kb, KnowledgeBase)
assert len(kb) == 3
assert kb.get("long_hair").tags == ("long hair",)
assert kb.get("blue_eyes").category == "eyes"
def test_id_and_alias_lookups(tmp_path) -> None:
kb = load_knowledge_base(write_kb(tmp_path, **valid_files()))
assert kb.resolve_alias("lengthy hair") == "long_hair"
assert kb.lookup("lengthy hair").id == "long_hair" # via alias
assert kb.lookup("long_hair").id == "long_hair" # via id
assert kb.lookup("unknown") is None
def test_expand_and_deprecated_preserved(tmp_path) -> None:
kb = load_knowledge_base(write_kb(tmp_path, **valid_files()))
assert kb.get("twin_braids").expand == ("long_hair",)
assert kb.get("long_hair").deprecated is False
def test_empty_directory_is_valid_empty_kb(tmp_path) -> None:
kb = load_knowledge_base(tmp_path)
assert len(kb) == 0
assert kb.get("anything") is None
# --- invalid load -> SC0004 -------------------------------------------------
def test_invalid_kb_raises_sc0004(tmp_path) -> None:
write_kb(tmp_path, bad=[{"id": "x", "tags": ["x"], "category": "nope"}])
with pytest.raises(KnowledgeBaseError) as exc:
load_knowledge_base(tmp_path)
assert exc.value.message.code == "SC0004"
assert exc.value.message.severity is Severity.FATAL
assert exc.value.findings # the underlying problems are attached
def test_duplicate_id_across_files_fails(tmp_path) -> None:
write_kb(
tmp_path,
a=[{"id": "dup", "tags": ["a"], "category": "hair"}],
b=[{"id": "dup", "tags": ["b"], "category": "hair"}],
)
with pytest.raises(KnowledgeBaseError):
load_knowledge_base(tmp_path)
def test_invalid_json_file_fails(tmp_path) -> None:
(tmp_path / "broken.json").write_text("{not json", encoding="utf-8")
with pytest.raises(KnowledgeBaseError) as exc:
load_knowledge_base(tmp_path)
assert exc.value.message.code == "SC0004"
# --- caching and reload -----------------------------------------------------
def test_loader_caches_after_first_get(tmp_path) -> None:
loader = KnowledgeBaseLoader(write_kb(tmp_path, **valid_files()))
first = loader.get()
second = loader.get()
assert first is second # loaded once, reused
def test_explicit_reload_picks_up_changes(tmp_path) -> None:
loader = KnowledgeBaseLoader(write_kb(tmp_path, **valid_files()))
assert loader.get().get("blue_eyes") is not None
# Replace the KB on disk.
for existing in tmp_path.glob("*.json"):
existing.unlink()
write_kb(tmp_path, hair=[{"id": "short_hair", "tags": ["short hair"], "category": "hair"}])
reloaded = loader.reload()
assert reloaded.get("short_hair") is not None
assert reloaded.get("blue_eyes") is None
def test_reload_failure_is_side_effect_free(tmp_path) -> None:
loader = KnowledgeBaseLoader(write_kb(tmp_path, **valid_files()))
good = loader.get()
# Corrupt the KB on disk, then reload.
write_kb(tmp_path, bad=[{"id": "x", "tags": ["x"], "category": "nope"}])
with pytest.raises(KnowledgeBaseError):
loader.reload()
# The previously loaded KB is still intact and returned.
assert loader.get() is good
assert loader.get().get("long_hair") is not None
# --- immutability -----------------------------------------------------------
def test_knowledge_base_is_read_only(tmp_path) -> None:
kb = load_knowledge_base(write_kb(tmp_path, **valid_files()))
with pytest.raises(TypeError):
kb.by_id["x"] = None # type: ignore[index]