415 lines
20 KiB
Python
415 lines
20 KiB
Python
#!/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())
|