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

47 lines
1.6 KiB
Python

"""Tests for the automated KB build pipeline (issue #122, epic #35)."""
from __future__ import annotations
from pathlib import Path
from scripts import build_knowledge_base as pipe
from scripts import generate_kb_from_vocab as gen
def test_dry_run_pipeline_succeeds(monkeypatch) -> None:
# Mirror CI: no external vocab source, so the committed snapshot is used.
monkeypatch.setattr(pipe.build_vocab, "_SOURCE", Path("/nonexistent/source.txt"))
assert pipe.run_pipeline(write=False) == 0 # clean: no rejections, no conflicts
def test_candidate_generation_is_deterministic() -> None:
assert gen.build_generated_entries() == gen.build_generated_entries()
def test_validation_rejects_bad_candidates() -> None:
by_category = {
"objects": [
{"id": "real_tag", "tags": ["real tag"], "category": "objects"},
{"id": "bad_category", "tags": ["bad"], "category": "not_a_category"},
]
}
rejections = pipe.validate_candidates(
by_category,
curated_ids=set(),
curated_aliases=set(),
source_vocab={"real_tag", "bad_category"},
)
ids = {r["id"] for r in rejections}
assert ids == {"bad_category"} # only the malformed candidate is rejected
def test_collision_with_curated_is_rejected() -> None:
by_category = {"objects": [{"id": "sword", "tags": ["sword"], "category": "objects"}]}
rejections = pipe.validate_candidates(
by_category,
curated_ids={"sword"}, # curated already owns "sword"
curated_aliases=set(),
source_vocab={"sword"},
)
assert [r["id"] for r in rejections] == ["sword"]