Files
50e7b0a8a4 feat(kb): automatic KB builder, ingestion & validation tooling (#35) (#139)
* feat(kb): automatic KB builder, ingestion & validation tooling (#35)

Closes #118, #119, #120, #121, #122.

- Ingest committed Danbooru alias/implication snapshots during generation:
  synonyms reach canonicals (long black stockings -> thighhighs), implications
  become expand relations. Curated-wins preserved.
- Automatic candidate validator (structural rules reused from kb_validation +
  confidence heuristic, no approval gate; SC0022).
- Duplicate/conflict detection tool (scripts/detect_kb_conflicts.py) + reusable
  detect_conflicts; clean reference KB reports zero.
- KB coverage benchmark harness (scripts/coverage_benchmark.py) + JSON summary.
- Single fully-automated, CI-runnable build pipeline (scripts/build_knowledge_base.py)
  that fails fast on rejections/conflicts and never prompts. Docs added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(kb): use pytest.approx for confidence float assertions (SonarCloud reliability)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: kunail0804 <kunail0804@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 18:10:17 +02:00

56 lines
2.2 KiB
Python

"""Tests for Danbooru alias/implication ingestion (issue #118, epic #35)."""
from __future__ import annotations
from pathlib import Path
from compiler.common.knowledge_base import load_knowledge_base
from scripts import generate_kb_from_vocab as gen
KB_DIR = Path(__file__).resolve().parent.parent / "knowledge_base"
# --- dataset level: the shipped, regenerated KB ----------------------------
def test_synonym_reaches_canonical_via_ingested_alias() -> None:
kb = load_knowledge_base(KB_DIR)
# The known V1 gap: "long black stockings" must reach "thighhighs".
assert kb.lookup("stockings") is not None
assert kb.lookup("stockings").id == "thighhighs"
# An alias attached to a *generated* canonical (exercises ingestion on gen_*).
assert kb.lookup("tights") is not None
assert kb.lookup("tights").id == "pantyhose"
def test_implication_becomes_expand_relation() -> None:
kb = load_knowledge_base(KB_DIR)
assert "gloves" in kb.get("elbow_gloves").expand
assert "weapon" in kb.get("katana").expand
# --- unit level: the ingestion function ------------------------------------
def test_ingestion_attaches_alias_and_skips_reserved(monkeypatch) -> None:
monkeypatch.setattr(
gen, "_load_aliases_by_canonical", lambda: {"pantyhose": ["tights", "curated_word"]}
)
monkeypatch.setattr(gen, "_load_implications", lambda: {})
by_category = {"clothing": [{"id": "pantyhose", "tags": ["pantyhose"], "category": "clothing"}]}
# "curated_word" is reserved by a curated entry, so curated wins → skipped.
gen._ingest_aliases_and_implications(by_category, {"curated_word"}, curated_ids=set())
assert by_category["clothing"][0]["aliases"] == ["tights"]
def test_ingestion_adds_expand_only_for_known_targets(monkeypatch) -> None:
monkeypatch.setattr(gen, "_load_aliases_by_canonical", lambda: {})
monkeypatch.setattr(
gen, "_load_implications", lambda: {"elbow_gloves": ["gloves", "does_not_exist"]}
)
by_category = {
"clothing": [{"id": "elbow_gloves", "tags": ["elbow gloves"], "category": "clothing"}]
}
gen._ingest_aliases_and_implications(by_category, set(), curated_ids={"gloves"})
assert by_category["clothing"][0]["expand"] == ["gloves"] # unknown target dropped