* 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>
47 lines
1.6 KiB
Python
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"]
|