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" "\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()