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

91 lines
2.9 KiB
Python

"""Tests for the automatic candidate validator (issue #119, epic #35)."""
from __future__ import annotations
import pytest
from compiler.common.kb_autovalidate import (
DEFAULT_CONFIDENCE_THRESHOLD,
compute_confidence,
validate_candidate,
)
VOCAB = {"pantyhose", "elbow_gloves", "gloves", "katana"}
def decide(entry, **overrides):
kwargs = dict(
curated_ids={"glasses", "thighhighs"},
curated_aliases={"spectacles", "thigh highs"},
known_ids={"pantyhose", "elbow_gloves", "gloves", "katana", "weapon"},
source_vocab=VOCAB,
)
kwargs.update(overrides)
return validate_candidate(entry, **kwargs)
def codes(decision):
return [m.code for m in decision.reasons]
def test_well_formed_candidate_is_accepted() -> None:
decision = decide({"id": "pantyhose", "tags": ["pantyhose"], "category": "clothing"})
assert decision.accepted
assert decision.confidence == pytest.approx(1.0)
assert decision.reasons == []
def test_id_collision_with_curated_is_rejected() -> None:
decision = decide({"id": "thighhighs", "tags": ["thighhighs"], "category": "clothing"})
assert not decision.accepted
assert "SC0005" in codes(decision)
def test_alias_collision_with_curated_is_rejected() -> None:
decision = decide(
{
"id": "pantyhose",
"tags": ["pantyhose"],
"category": "clothing",
"aliases": ["spectacles"],
}
)
assert not decision.accepted
assert "SC0004" in codes(decision)
def test_unknown_category_is_rejected() -> None:
decision = decide({"id": "pantyhose", "tags": ["pantyhose"], "category": "not_a_category"})
assert not decision.accepted
assert "SC0008" in codes(decision)
def test_self_cycle_expansion_is_rejected() -> None:
decision = decide(
{"id": "katana", "tags": ["katana"], "category": "objects", "expand": ["katana"]}
)
assert not decision.accepted
assert "SC0003" in codes(decision)
def test_unknown_expand_target_is_rejected() -> None:
decision = decide(
{"id": "katana", "tags": ["katana"], "category": "objects", "expand": ["nonexistent"]}
)
assert not decision.accepted
assert "SC0004" in codes(decision)
def test_below_confidence_threshold_is_rejected() -> None:
# A tag not in the source vocab scores below threshold (no invention).
decision = decide({"id": "invented_tag", "tags": ["invented tag"], "category": "clothing"})
assert not decision.accepted
assert "SC0022" in codes(decision)
assert decision.confidence < DEFAULT_CONFIDENCE_THRESHOLD
def test_confidence_scoring() -> None:
assert compute_confidence({"id": "katana", "category": "objects"}, VOCAB) == pytest.approx(1.0)
assert compute_confidence({"id": "katana", "category": "bogus"}, VOCAB) == pytest.approx(0.7)
assert compute_confidence({"id": "unknown", "category": "objects"}, VOCAB) == pytest.approx(0.3)