Files
llm-wiki/harness/tests/test_semantic_candidate_builder.py
T

147 lines
6.3 KiB
Python

from __future__ import annotations
import hashlib
import json
from pathlib import Path
import shutil
import sys
import tempfile
import unittest
RUNTIME = Path(__file__).resolve().parents[1] / "runtime"
if str(RUNTIME) not in sys.path:
sys.path.insert(0, str(RUNTIME))
import semantic_candidate_builder as builder # noqa: E402
import semantic_surface_extractor as extractor # noqa: E402
REPO_ROOT = Path(__file__).resolve().parents[2]
class SemanticCandidateBuilderTest(unittest.TestCase):
def setUp(self) -> None:
self.tempdir = tempfile.TemporaryDirectory()
self.root = Path(self.tempdir.name)
(self.root / "harness/source").mkdir(parents=True)
shutil.copyfile(REPO_ROOT / builder.DEFAULT_ONTOLOGY, self.root / builder.DEFAULT_ONTOLOGY)
shutil.copyfile(REPO_ROOT / extractor.DEFAULT_POLICY, self.root / extractor.DEFAULT_POLICY)
(self.root / "raw/branch-notes").mkdir(parents=True)
self.path = self.root / "raw/branch-notes/feature-candidates.md"
body_lines = [
"stage-7 owns mapper",
"stage-7 produces bundle",
"stage-7 consumes input",
"build-gate requires `vite build` FE-GATE-BUILD-001@2",
"build-gate forbids `vite build` FE-GATE-BUILD-001@2",
"component returns model",
"component validates model",
]
self.path.write_text(
"---\n"
"title: candidate fixture\n"
"source_type: branch-note\n"
"status: verified\n"
"semantic_surface_exclusions:\n"
" - branch-contract-packet|fixture\n"
" - scope|fixture\n"
" - decision-evidence|fixture\n"
" - edge-failure-dependency|fixture\n"
" - claims-to-verify|fixture\n"
"---\n\n"
"<!-- section-id: implementation -->\n"
"## 구현\n"
+ "\n".join(body_lines)
+ "\n",
encoding="utf-8",
)
policy = extractor.load_policy(self.root)
self.extraction = extractor.extract_document(self.root, self.path, policy)
def tearDown(self) -> None:
self.tempdir.cleanup()
def _assertion_result(self) -> dict[str, object]:
surface = self.extraction["surfaces"][0]
lines = self.path.read_text(encoding="utf-8").splitlines()
quote_lines = {line: number for number, line in enumerate(lines, 1) if number >= surface["line_start"]}
specs = [
("A1", "stage-7", "owns", "mapper", "must", "stage-7 owns mapper"),
("A2", "stage-7", "produces", "bundle", "must", "stage-7 produces bundle"),
("A3", "consumer", "consumes", "bundle", "must", "stage-7 consumes input"),
("A4", "build-gate", "requires", "vite build", "must", "build-gate requires `vite build` FE-GATE-BUILD-001@2"),
("A5", "build-gate", "forbids", "vite build", "must_not", "build-gate forbids `vite build` FE-GATE-BUILD-001@2"),
("A6", "component", "returns", "model", "must", "component returns model"),
("A7", "component", "validates", "model", "must", "component validates model"),
]
assertions = []
for identifier, subject, predicate, obj, modality, quote in specs:
line = quote_lines[quote]
assertions.append({
"assertion_id": identifier,
"source_surface": surface["surface_id"],
"subject": subject,
"predicate": predicate,
"object": obj,
"condition": "normal-path",
"modality": modality,
"scope": "branch",
"quote": quote,
"line_start": line,
"line_end": line,
})
return {
"schema_version": builder.ASSERTION_SCHEMA,
"subject": self.extraction["path"],
"mode": "local",
"surface_manifest_sha256": hashlib.sha256(extractor.canonical_json_bytes(self.extraction)).hexdigest(),
"assertions": assertions,
}
def test_exact_ontology_and_seven_candidate_rules(self) -> None:
result = builder.build(self.root, self.extraction, self._assertion_result())
observed = {rule for item in result["candidates"] for rule in item["rule_ids"]}
self.assertTrue({"C1", "C2", "C3", "C4", "C5", "C6", "C7"}.issubset(observed))
self.assertEqual(result["coverage"]["processed_surfaces"], result["coverage"]["eligible_surfaces"])
def test_unknown_predicate_and_unverified_quote_fail(self) -> None:
assertions = self._assertion_result()
assertions["assertions"][0]["predicate"] = "owner"
with self.assertRaises(builder.SemanticCandidateError):
builder.build(self.root, self.extraction, assertions)
assertions = self._assertion_result()
assertions["assertions"][0]["quote"] = "not the source bytes"
with self.assertRaises(builder.SemanticCandidateError):
builder.build(self.root, self.extraction, assertions)
def test_surface_manifest_binding_blocks_stale_assertions(self) -> None:
assertions = self._assertion_result()
assertions["surface_manifest_sha256"] = "0" * 64
with self.assertRaises(builder.SemanticCandidateError):
builder.build(self.root, self.extraction, assertions)
def test_unquoted_path_literal_does_not_raise_stop_iteration(self) -> None:
assertions = self._assertion_result()
first = assertions["assertions"][0]
line = first["line_start"]
document_lines = self.path.read_text(encoding="utf-8").splitlines()
document_lines[line - 1] = "stage-7 owns mapper at src/main/example"
self.path.write_text("\n".join(document_lines) + "\n", encoding="utf-8")
policy = extractor.load_policy(self.root)
extraction = extractor.extract_document(self.root, self.path, policy)
first["quote"] = document_lines[line - 1]
assertions["surface_manifest_sha256"] = hashlib.sha256(
extractor.canonical_json_bytes(extraction)
).hexdigest()
result = builder.build(self.root, extraction, assertions)
self.assertEqual(
result["coverage"]["processed_surfaces"],
result["coverage"]["eligible_surfaces"],
)
if __name__ == "__main__":
unittest.main()