* 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>
51 lines
2.2 KiB
Python
51 lines
2.2 KiB
Python
"""Tests for Knowledge Base conflict detection (issue #120, epic #35)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from compiler.common.kb_conflicts import detect_conflicts
|
|
from scripts.detect_kb_conflicts import _load
|
|
|
|
KB_DIR = Path(__file__).resolve().parent.parent / "knowledge_base"
|
|
|
|
|
|
def test_clean_reference_kb_has_no_conflicts() -> None:
|
|
entries, curated_ids = _load(KB_DIR)
|
|
report = detect_conflicts(entries, curated_ids)
|
|
assert report.total == 0, report.to_json()
|
|
|
|
|
|
def test_fixture_reports_each_conflict_type_and_only_those() -> None:
|
|
entries = [
|
|
# duplicate id
|
|
{"id": "dup", "tags": ["dup a"], "category": "objects"},
|
|
{"id": "dup", "tags": ["dup b"], "category": "objects"},
|
|
# duplicate alias across two entries
|
|
{"id": "a1", "tags": ["a1"], "category": "objects", "aliases": ["shared_alias"]},
|
|
{"id": "a2", "tags": ["a2"], "category": "objects", "aliases": ["shared_alias"]},
|
|
# same tag owned by multiple NON-curated entries
|
|
{"id": "g1", "tags": ["overlap tag"], "category": "objects"},
|
|
{"id": "g2", "tags": ["overlap tag"], "category": "objects"},
|
|
# contradictory (mutual) expansion
|
|
{"id": "x", "tags": ["x"], "category": "objects", "expand": ["y"]},
|
|
{"id": "y", "tags": ["y"], "category": "objects", "expand": ["x"]},
|
|
]
|
|
report = detect_conflicts(entries, curated_ids=set())
|
|
assert [d["id"] for d in report.duplicate_ids] == ["dup"]
|
|
assert [d["alias"] for d in report.duplicate_aliases] == ["shared_alias"]
|
|
assert [d["tag"] for d in report.tag_multiple_owners] == ["overlap tag"]
|
|
assert report.contradictory_expansions == [{"pair": ["x", "y"]}]
|
|
assert report.total == 4
|
|
|
|
|
|
def test_curated_owner_disambiguates_shared_tag() -> None:
|
|
# A tag shared between a curated entry and a generated one is NOT a conflict
|
|
# (curated wins). Same tag between two generated entries IS.
|
|
entries = [
|
|
{"id": "curated_one", "tags": ["t"], "category": "objects"},
|
|
{"id": "generated_one", "tags": ["t"], "category": "objects"},
|
|
]
|
|
assert detect_conflicts(entries, curated_ids={"curated_one"}).tag_multiple_owners == []
|
|
assert detect_conflicts(entries, curated_ids=set()).tag_multiple_owners != []
|