#!/usr/bin/env python3 """Issue and validate byte-bound semantic certificates for design-bearing documents.""" from __future__ import annotations import argparse import hashlib import json import os from pathlib import Path import re import sys from typing import Any, Iterable, Mapping import semantic_audit import semantic_candidate_builder import semantic_surface_extractor import typed_contract_check DEFAULT_ROOT = Path(__file__).resolve().parents[2] DEFAULT_STATE = Path("harness/state/semantic-certificates") DEFAULT_AGENT_METADATA = Path("harness/source/agents/wiki-semantic-coherence-auditor.json") DEFAULT_AGENT_BODY = Path("harness/source/agents/bodies/wiki-semantic-coherence-auditor.md") SCHEMA_VERSION = "semantic-certificate/v1" RESULT_SCHEMA = "semantic-certificate-result/v1" HEX_SHA256 = re.compile(r"^[0-9a-f]{64}$") class SemanticCertificateError(ValueError): def __init__(self, code: str, message: str, path: str = "") -> None: self.code = code self.path = path super().__init__(message) def _source_path(root: Path, value: Path) -> Path: return value if value.is_absolute() else root / value def _file_sha(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() def _canonical_sha(value: Any) -> str: return hashlib.sha256(semantic_audit.canonical_json_bytes(value)).hexdigest() def document_id(relative: str) -> str: candidate = Path(relative) if not relative or candidate.is_absolute() or ".." in candidate.parts or "\\" in relative: raise SemanticCertificateError("INVALID_CERTIFICATE_SUBJECT", "subject must be a canonical repo-relative POSIX path", relative) return hashlib.sha256(relative.encode("utf-8")).hexdigest() def certificate_path(root: Path, subject: str, document_sha256: str, state_path: Path = DEFAULT_STATE) -> Path: if not HEX_SHA256.fullmatch(document_sha256): raise SemanticCertificateError("INVALID_DOCUMENT_SHA256", "document sha256 is invalid", subject) state = _source_path(root, state_path) return state / document_id(subject) / f"{document_sha256}.json" def logical_subject(root: Path, document: Path) -> str: """Return the stable pre-cutover identity for an active document path.""" relative = document.resolve().relative_to(root.resolve()).as_posix() layout_path = root / "harness/source/vault-layout.json" if not layout_path.is_file(): return relative try: layout = json.loads(layout_path.read_text(encoding="utf-8")) except (OSError, UnicodeError, json.JSONDecodeError): return relative if layout.get("mode") != "canonical": return relative migration = layout.get("migration_manifest") entries = migration.get("entries") if isinstance(migration, Mapping) else None if not isinstance(entries, list): return relative matches = [ str(item.get("legacy_path")) for item in entries if isinstance(item, Mapping) and item.get("canonical_path") == relative ] if len(matches) != 1: return relative return matches[0] def _policy_hashes( root: Path, *, policy_path: Path = semantic_surface_extractor.DEFAULT_POLICY, ontology_path: Path = semantic_candidate_builder.DEFAULT_ONTOLOGY, agent_metadata_path: Path = DEFAULT_AGENT_METADATA, agent_body_path: Path = DEFAULT_AGENT_BODY, ) -> dict[str, str]: policy = _source_path(root, policy_path) ontology = _source_path(root, ontology_path) metadata = _source_path(root, agent_metadata_path) body = _source_path(root, agent_body_path) for source in (policy, ontology, metadata, body): if not source.is_file(): raise SemanticCertificateError("SEMANTIC_POLICY_SOURCE_MISSING", "certificate binding source is missing", source.as_posix()) contract_payload = metadata.read_bytes() + b"\0" + body.read_bytes() return { "policy_sha256": _file_sha(policy), "ontology_sha256": _file_sha(ontology), "auditor_contract_sha256": hashlib.sha256(contract_payload).hexdigest(), } def _typed_hash(root: Path) -> str: result = typed_contract_check.check(root) if result.get("status") != "PASS": codes = sorted({str(item.get("code", "UNKNOWN")) for item in result.get("findings", [])}) raise SemanticCertificateError("TYPED_CONTRACT_FAILED", ",".join(codes)) return str(result["typed_contract_graph_sha256"]) def build_certificate( root: Path, validated_audit: Mapping[str, Any], *, audit_request: Mapping[str, Any], audit_result: Mapping[str, Any], run_root: Path | None = None, policy_path: Path = semantic_surface_extractor.DEFAULT_POLICY, ontology_path: Path = semantic_candidate_builder.DEFAULT_ONTOLOGY, agent_metadata_path: Path = DEFAULT_AGENT_METADATA, agent_body_path: Path = DEFAULT_AGENT_BODY, ) -> dict[str, Any]: root = root.resolve(strict=True) try: revalidated = semantic_audit.validate_result(root, audit_request, audit_result, run_root=run_root) except semantic_audit.SemanticAuditError as exc: raise SemanticCertificateError("INVALID_SEMANTIC_AUDIT", str(exc)) from exc if semantic_audit.canonical_json_bytes(revalidated) != semantic_audit.canonical_json_bytes(validated_audit): raise SemanticCertificateError("INVALID_SEMANTIC_AUDIT", "validated audit differs from request/result replay") if validated_audit.get("schema_version") != semantic_audit.VALIDATED_RESULT_SCHEMA: raise SemanticCertificateError("INVALID_SEMANTIC_AUDIT", "validated semantic audit schema mismatch") subject = str(validated_audit.get("subject", "")) doc_id = document_id(subject) document = Path(os.path.abspath(root / subject)) try: document.relative_to(root) document.resolve(strict=True).relative_to(root) except ValueError as exc: raise SemanticCertificateError("INVALID_CERTIFICATE_SUBJECT", "subject escapes repository", subject) from exc if not document.is_file(): raise SemanticCertificateError("CERTIFICATE_SUBJECT_MISSING", "subject document does not exist", subject) document_sha = _file_sha(document) if document_sha != validated_audit.get("document_sha256"): raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "audit is not bound to current document bytes", subject) coverage = validated_audit.get("coverage") counts = validated_audit.get("counts") if not isinstance(coverage, Mapping) or not isinstance(counts, Mapping): raise SemanticCertificateError("INVALID_SEMANTIC_AUDIT", "audit coverage/counts are missing", subject) if coverage.get("eligible_surfaces") != coverage.get("processed_surfaces"): raise SemanticCertificateError("SEMANTIC_SURFACE_UNCOVERED", "audit did not process every eligible surface", subject) if coverage.get("candidate_pairs") != coverage.get("processed_pairs"): raise SemanticCertificateError("SEMANTIC_PAIR_UNCOVERED", "audit did not process every candidate pair", subject) mode = validated_audit.get("mode") if mode not in {"local", "hub"}: raise SemanticCertificateError("INVALID_SEMANTIC_AUDIT", "audit mode is invalid", subject) proof_hashes = sorted({str(item["proof_manifest_sha256"]) for item in validated_audit.get("findings", [])}) hashes = _policy_hashes( root, policy_path=policy_path, ontology_path=ontology_path, agent_metadata_path=agent_metadata_path, agent_body_path=agent_body_path, ) verdict = "PASS" if validated_audit.get("status") == "PASS" else "FAIL" return { "schema_version": SCHEMA_VERSION, "subject": subject, "document_id": doc_id, "document_sha256": document_sha, **hashes, "typed_contract_graph_sha256": _typed_hash(root), "mode": mode, "verdict": verdict, "coverage": { "eligible_surfaces": int(coverage["eligible_surfaces"]), "processed_surfaces": int(coverage["processed_surfaces"]), "candidate_pairs": int(coverage["candidate_pairs"]), "processed_pairs": int(coverage["processed_pairs"]), "dropped_pairs": int(coverage["dropped_pairs"]), }, "findings": { "blocking": int(counts["blocking"]), "readiness_blocking": int(counts["readiness_blocking"]), "verified": int(counts["verified_findings"]), }, "proof_manifest_sha256": _canonical_sha(proof_hashes), "audit_request_sha256": str(validated_audit["request_sha256"]), "semantic_audit_sha256": _canonical_sha(validated_audit), "audit_request": dict(audit_request), "audit_result": dict(audit_result), "auditor": dict(validated_audit["auditor"]), } def prepare_certificate( root: Path, validated_audit: Mapping[str, Any], *, state_path: Path = DEFAULT_STATE, **kwargs: Any, ) -> tuple[Path, bytes, dict[str, Any]]: certificate = build_certificate(root, validated_audit, **kwargs) path = certificate_path(root, certificate["subject"], certificate["document_sha256"], state_path) return path, semantic_audit.canonical_json_bytes(certificate), certificate def _load_certificate(path: Path) -> Mapping[str, Any]: try: value = json.loads(path.read_text(encoding="utf-8")) except (OSError, UnicodeError, json.JSONDecodeError) as exc: raise SemanticCertificateError("INVALID_SEMANTIC_CERTIFICATE", str(exc), path.as_posix()) from exc required = { "schema_version", "subject", "document_id", "document_sha256", "policy_sha256", "ontology_sha256", "auditor_contract_sha256", "typed_contract_graph_sha256", "mode", "verdict", "coverage", "findings", "proof_manifest_sha256", "audit_request_sha256", "semantic_audit_sha256", "audit_request", "audit_result", "auditor", } if not isinstance(value, dict) or set(value) != required or value.get("schema_version") != SCHEMA_VERSION: raise SemanticCertificateError("INVALID_SEMANTIC_CERTIFICATE", "certificate has missing or unknown fields", path.as_posix()) return value def validate_certificate( root: Path, path: Path, *, state_path: Path = DEFAULT_STATE, policy_path: Path = semantic_surface_extractor.DEFAULT_POLICY, ontology_path: Path = semantic_candidate_builder.DEFAULT_ONTOLOGY, agent_metadata_path: Path = DEFAULT_AGENT_METADATA, agent_body_path: Path = DEFAULT_AGENT_BODY, ) -> dict[str, Any]: root = root.resolve(strict=True) path = path.resolve(strict=True) certificate = _load_certificate(path) subject = str(certificate["subject"]) expected_path = certificate_path(root, subject, str(certificate["document_sha256"]), state_path).resolve() if path != expected_path: raise SemanticCertificateError("INVALID_CERTIFICATE_PATH", "certificate path does not match document-id/document sha", path.as_posix()) if certificate["document_id"] != document_id(subject): raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "document id mismatch", subject) audit_request = certificate.get("audit_request") audit_result = certificate.get("audit_result") if not isinstance(audit_request, Mapping) or not isinstance(audit_result, Mapping): raise SemanticCertificateError("INVALID_SEMANTIC_CERTIFICATE", "embedded audit artifacts must be objects", subject) try: replayed = semantic_audit.validate_result(root, audit_request, audit_result) except semantic_audit.SemanticAuditError as exc: raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", f"embedded audit replay failed: {exc}", subject) from exc if certificate.get("audit_request_sha256") != _canonical_sha(audit_request): raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "audit request hash mismatch", subject) if certificate.get("semantic_audit_sha256") != _canonical_sha(replayed): raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "semantic audit hash mismatch", subject) replayed_hashes = sorted({str(item["proof_manifest_sha256"]) for item in replayed.get("findings", [])}) if certificate.get("proof_manifest_sha256") != _canonical_sha(replayed_hashes): raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "proof manifest binding changed", subject) document = Path(os.path.abspath(root / subject)) try: document.relative_to(root) document.resolve(strict=True).relative_to(root) except (ValueError, FileNotFoundError) as exc: raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "document subject is missing or escapes repository", subject) from exc if not document.is_file() or _file_sha(document) != certificate["document_sha256"]: raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "document bytes changed", subject) hashes = _policy_hashes( root, policy_path=policy_path, ontology_path=ontology_path, agent_metadata_path=agent_metadata_path, agent_body_path=agent_body_path, ) for key, expected in hashes.items(): if certificate.get(key) != expected: raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", f"{key} changed", subject) if certificate.get("typed_contract_graph_sha256") != _typed_hash(root): raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "typed contract graph changed", subject) expected_verdict = "PASS" if replayed.get("status") == "PASS" else "FAIL" if certificate.get("verdict") != expected_verdict: raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "audit verdict changed", subject) if certificate.get("auditor") != replayed.get("auditor"): raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "auditor identity changed", subject) if certificate.get("mode") != replayed.get("mode") or certificate.get("document_sha256") != replayed.get("document_sha256"): raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "audit subject binding changed", subject) policy = semantic_surface_extractor.load_policy(root, policy_path) extraction = semantic_surface_extractor.extract_document(root, document, policy) if extraction["mode"] != certificate.get("mode"): raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "semantic mode changed", subject) coverage = certificate.get("coverage") findings = certificate.get("findings") if not isinstance(coverage, Mapping) or not isinstance(findings, Mapping): raise SemanticCertificateError("INVALID_SEMANTIC_CERTIFICATE", "coverage/findings must be objects", subject) if coverage.get("eligible_surfaces") != extraction["coverage"]["eligible_surface_blocks"]: raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "eligible surface count changed", subject) replayed_coverage = replayed.get("coverage", {}) replayed_counts = replayed.get("counts", {}) expected_coverage = { "eligible_surfaces": replayed_coverage.get("eligible_surfaces"), "processed_surfaces": replayed_coverage.get("processed_surfaces"), "candidate_pairs": replayed_coverage.get("candidate_pairs"), "processed_pairs": replayed_coverage.get("processed_pairs"), "dropped_pairs": replayed_coverage.get("dropped_pairs"), } expected_findings = { "blocking": replayed_counts.get("blocking"), "readiness_blocking": replayed_counts.get("readiness_blocking"), "verified": replayed_counts.get("verified_findings"), } if dict(coverage) != expected_coverage or dict(findings) != expected_findings: raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "audit coverage or finding counts changed", subject) if coverage.get("eligible_surfaces") != coverage.get("processed_surfaces"): raise SemanticCertificateError("SEMANTIC_SURFACE_UNCOVERED", "certificate surface coverage is incomplete", subject) if coverage.get("candidate_pairs") != coverage.get("processed_pairs"): raise SemanticCertificateError("SEMANTIC_PAIR_UNCOVERED", "certificate pair coverage is incomplete", subject) if certificate.get("verdict") != "PASS" or findings.get("blocking") != 0 or findings.get("readiness_blocking") != 0: raise SemanticCertificateError("SEMANTIC_BLOCKING_VERDICT", "certificate contains a blocking semantic result", subject) if certificate["mode"] == "hub" and coverage.get("dropped_pairs") != 0: raise SemanticCertificateError("SEMANTIC_PAIR_DROPPED", "hub certificate contains dropped candidates", subject) return dict(certificate) def check( root: Path, *, mode: str | None = None, paths: Iterable[Path] | None = None, state_path: Path = DEFAULT_STATE, ) -> dict[str, Any]: root = root.resolve(strict=True) policy = semantic_surface_extractor.load_policy(root) selected = tuple(paths) if paths is not None else semantic_surface_extractor.eligible_documents(root, policy, required_only=True, mode=mode) findings: list[dict[str, Any]] = [] current: list[dict[str, Any]] = [] # 자동 탐색이 0건이면 "요구 문서가 없어 전부 최신" 처럼 PASS 로 보이지만, 실제로는 # 탐색이 깨져 인증 검사를 *조용히 건너뛴* 상태일 수 있다. 명시적 paths 는 호출자 책임이나, # 자동 탐색의 0건은 loud FAIL. 단 mode 필터(local/hub)의 0건은 "그 모드 문서가 없을 뿐" # 이라 정상일 수 있으므로(예: local 만 있고 hub 는 없음), 필터 없는 전수 탐색에만 적용한다. if paths is None and mode is None and not selected: findings.append({ "code": "NO_REQUIRED_DOCUMENTS", "path": "", "line": 0, "message": "자동 탐색이 인증 대상(설계-보유) 문서를 0건 발견 — 탐색이 깨졌을 수 있음.", }) for raw in selected: document = raw if raw.is_absolute() else root / raw extraction = semantic_surface_extractor.extract_document(root, document, policy) if mode is not None and extraction["mode"] != mode: continue subject = logical_subject(root, document) path = certificate_path(root, subject, extraction["document_sha256"], state_path) if not path.is_file(): findings.append({ "code": "SEMANTIC_CERTIFICATE_MISSING", "path": extraction["path"], "line": 0, "message": f"current {extraction['mode']} certificate is missing", }) continue try: certificate = validate_certificate(root, path, state_path=state_path) current.append({"subject": certificate["subject"], "mode": certificate["mode"], "path": path.relative_to(root).as_posix()}) except SemanticCertificateError as exc: findings.append({"code": exc.code, "path": exc.path or extraction["path"], "line": 0, "message": str(exc)}) return { "schema_version": RESULT_SCHEMA, "status": "PASS" if not findings else "FAIL", "mode": mode or "all", "required_documents": len(selected), "current_certificates": len(current), "coverage": { "required": len(selected), "current": len(current), "missing_or_stale": len(findings), }, "certificates": current, "findings": sorted(findings, key=lambda item: (item["path"], item["code"])), } def quality_extension(root: Path, paths: Iterable[Path]) -> dict[str, Any]: """QualityExtension-compatible current-certificate validation.""" return check(root, paths=paths) def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--root", type=Path, default=DEFAULT_ROOT) parser.add_argument("--mode", choices=("local", "hub")) parser.add_argument("--path", type=Path, action="append") parser.add_argument("--check", action="store_true") args = parser.parse_args(argv) try: result = check(args.root, mode=args.mode, paths=args.path) exit_code = 0 if result["status"] == "PASS" else 1 except (SemanticCertificateError, semantic_surface_extractor.SemanticSurfaceError, OSError, UnicodeError, json.JSONDecodeError) as exc: result = {"schema_version": RESULT_SCHEMA, "status": "ERROR", "errors": [{"code": getattr(exc, "code", "SEMANTIC_CERTIFICATE_ERROR"), "message": str(exc)}]} exit_code = 2 json.dump(result, sys.stdout, ensure_ascii=False, indent=2, sort_keys=True) sys.stdout.write("\n") return exit_code if __name__ == "__main__": raise SystemExit(main())