mirror of
https://github.com/muratcankoylan/Agent-Skills-for-Context-Engineering.git
synced 2026-09-14 20:36:54 +08:00
2900 lines
124 KiB
Python
2900 lines
124 KiB
Python
#!/usr/bin/env python3
|
|
"""Build and reconcile the repository's deterministic corpus inventory.
|
|
|
|
The inventory is a derived view, never an authority over its inputs. It has no
|
|
wall-clock timestamp or embedded Git SHA. Instead it binds to exact canonical
|
|
input bytes through ``source_tree_digest`` and lists every contributing path
|
|
and digest. This avoids self-referential commit hashes in a committed output.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import stat
|
|
import sys
|
|
import tempfile
|
|
from dataclasses import asdict, dataclass
|
|
from datetime import date
|
|
from pathlib import Path, PurePosixPath
|
|
from typing import Any, Iterable
|
|
|
|
try:
|
|
from skill_frontmatter import parse_frontmatter # type: ignore[import-not-found]
|
|
except ModuleNotFoundError: # Imported as researcher.scripts.build_inventory.
|
|
from researcher.scripts.skill_frontmatter import parse_frontmatter
|
|
|
|
try:
|
|
from validate_spec_lifecycle import ( # type: ignore[import-not-found]
|
|
AUTHORITY_CONFORMANCE_RECEIPT_PATH,
|
|
AUTHORITY_CONSTITUTION_POLICY_PATH,
|
|
AUTHORITY_FIXTURE_MANIFEST_PATH,
|
|
AUTHORITY_IMPLEMENTED_STATUSES,
|
|
AUTHORITY_VALIDATOR_PATH,
|
|
AUTHORITY_VOCABULARY_MIN_REVISION,
|
|
AUTHORITY_VOCABULARY_PATH,
|
|
AUTHORITY_VOCABULARY_READY_STATUSES,
|
|
AUTHORITY_VOCABULARY_SPEC,
|
|
AuthorityVocabularyBinding,
|
|
SpecRevision,
|
|
load_candidate_authority_vocabulary,
|
|
parse_spec_revision,
|
|
)
|
|
except ModuleNotFoundError: # Imported as researcher.scripts.build_inventory.
|
|
from researcher.scripts.validate_spec_lifecycle import (
|
|
AUTHORITY_CONFORMANCE_RECEIPT_PATH,
|
|
AUTHORITY_CONSTITUTION_POLICY_PATH,
|
|
AUTHORITY_FIXTURE_MANIFEST_PATH,
|
|
AUTHORITY_IMPLEMENTED_STATUSES,
|
|
AUTHORITY_VALIDATOR_PATH,
|
|
AUTHORITY_VOCABULARY_MIN_REVISION,
|
|
AUTHORITY_VOCABULARY_PATH,
|
|
AUTHORITY_VOCABULARY_READY_STATUSES,
|
|
AUTHORITY_VOCABULARY_SPEC,
|
|
AuthorityVocabularyBinding,
|
|
SpecRevision,
|
|
load_candidate_authority_vocabulary,
|
|
parse_spec_revision,
|
|
)
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
DEFAULT_INVENTORY = ROOT / "researcher" / "corpus" / "inventory.json"
|
|
DEFAULT_SUMMARY = ROOT / "researcher" / "generated" / "corpus-summary.md"
|
|
SCHEMA_VERSION = "1.0.0"
|
|
|
|
SPEC_FILENAME_RE = re.compile(r"^SPEC-(?P<number>[0-9]{3})-(?P<slug>[a-z0-9-]+)\.md$")
|
|
SPEC_HEADING_RE = re.compile(r"^# SPEC-(?P<number>[0-9]{3}): (?P<title>.+)$")
|
|
SPEC_INDEX_ROW_RE = re.compile(
|
|
r"^\|\s*\[SPEC-(?P<number>[0-9]{3})\]\((?P<path>SPEC-[^)]+\.md)\)"
|
|
r"\s*\|[^|\n]+\|[^|\n]+\|\s*$"
|
|
)
|
|
SPEC_ID_RE = re.compile(r"^SPEC-[0-9]{3}$")
|
|
SPEC_GRAPH_EDGE_RE = re.compile(
|
|
r'^\s*S(?P<source>[0-9]{3})(?:\["(?P<source_label>[^"\]]*)"\])?\s*-->\s*'
|
|
r'S(?P<target>[0-9]{3})(?:\["(?P<target_label>[^"\]]*)"\])?\s*$'
|
|
)
|
|
SPEC_STATUSES = {
|
|
"draft",
|
|
"architecture_reviewed",
|
|
"accepted",
|
|
"implemented",
|
|
"verified",
|
|
"operational",
|
|
"amended",
|
|
"superseded",
|
|
"retired",
|
|
}
|
|
SPEC_CLASSIFICATIONS = {"public", "private", "split"}
|
|
SPEC_REQUIRED_METADATA = (
|
|
"Status",
|
|
"Revision",
|
|
"Revises",
|
|
"Wave",
|
|
"Classification",
|
|
"Owners",
|
|
"Depends on",
|
|
)
|
|
SPEC_OPTIONAL_METADATA = {
|
|
"Activation",
|
|
"Adoption decision",
|
|
"Authority vocabulary",
|
|
"Authority vocabulary digest",
|
|
"Authority vocabulary version",
|
|
"Dependency revisions",
|
|
"Lifecycle decision",
|
|
"Replacement",
|
|
}
|
|
SPEC_METADATA_ORDER = (
|
|
"Status",
|
|
"Revision",
|
|
"Revises",
|
|
"Activation",
|
|
"Wave",
|
|
"Classification",
|
|
"Owners",
|
|
"Depends on",
|
|
"Dependency revisions",
|
|
"Authority vocabulary",
|
|
"Authority vocabulary digest",
|
|
"Authority vocabulary version",
|
|
"Adoption decision",
|
|
"Lifecycle decision",
|
|
"Replacement",
|
|
)
|
|
SPEC_REVISION_DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
SPEC_LIFECYCLE_DECISION_RE = re.compile(r"^ADR-[0-9]{4}$")
|
|
SPEC_REPLACEMENT_RE = re.compile(r"^SPEC-[0-9]{3}@[1-9][0-9]*$")
|
|
SPEC_DEPENDENCY_REVISION_RE = re.compile(r"^(SPEC-[0-9]{3})@([1-9][0-9]*)$")
|
|
SPEC_TERMINAL_STATUSES = {"amended", "superseded", "retired"}
|
|
AUTHORITY_VOCABULARY_METADATA_KEYS = frozenset(
|
|
{
|
|
"Authority vocabulary",
|
|
"Authority vocabulary digest",
|
|
"Authority vocabulary version",
|
|
}
|
|
)
|
|
ADR_FILENAME_RE = re.compile(r"^(?P<number>[0-9]{4})-(?P<slug>[a-z0-9-]+)\.md$")
|
|
ADR_HEADING_RE = re.compile(r"^# ADR-(?P<number>[0-9]{4}): (?P<title>.+)$")
|
|
ADR_INDEX_ROW_RE = re.compile(
|
|
r"^- \[ADR-(?P<number>[0-9]{4}): (?P<title>[^\]]+)\]"
|
|
r"\((?P<path>[0-9]{4}-[^)]+\.md)\)$"
|
|
)
|
|
ADR_STATUSES = {"accepted", "deprecated", "proposed", "superseded"}
|
|
ADR_ALLOWED_METADATA = {
|
|
"Status",
|
|
"Date",
|
|
"Spec",
|
|
"Specs",
|
|
"Supersedes",
|
|
"Lifecycle transition",
|
|
}
|
|
ADR_ID_RE = re.compile(r"^ADR-[0-9]{4}$")
|
|
ADR_LIFECYCLE_TRANSITION_RE = re.compile(
|
|
r"^(?P<spec>SPEC-[0-9]{3})@(?P<revision>[1-9][0-9]*) -> "
|
|
r"(?P<status>amended|superseded|retired) -> "
|
|
r"(?P<replacement>SPEC-[0-9]{3}@[1-9][0-9]*|none)$"
|
|
)
|
|
FENCE_OPEN_RE = re.compile(r"^ {0,3}(?P<fence>`{3,}|~{3,})(?P<info>.*)$")
|
|
LEVEL_TWO_HEADING_RE = re.compile(r"^## (?P<title>.+?)\s*$")
|
|
RAW_HTML_BLOCK_START_RE = re.compile(
|
|
r"^ {0,3}<(?:/?[A-Za-z][A-Za-z0-9-]*(?:[ \t/>]|$)|[!?])",
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
VALIDATOR_OWNERSHIP = (
|
|
{
|
|
"id": "governance-policy",
|
|
"path": "researcher/scripts/validate_governance.py",
|
|
"owns": [
|
|
"Constitution decision evaluation",
|
|
"constitutional invariants",
|
|
"generated authority view",
|
|
],
|
|
},
|
|
{
|
|
"id": "repository-inventory",
|
|
"path": "researcher/scripts/build_inventory.py",
|
|
"owns": [
|
|
"identifier uniqueness",
|
|
"cross-artifact references",
|
|
"specification graph",
|
|
"authority artifact parity and source binding",
|
|
"live counts",
|
|
"generated inventory",
|
|
],
|
|
},
|
|
{
|
|
"id": "specification-lifecycle",
|
|
"path": "researcher/scripts/validate_spec_lifecycle.py",
|
|
"owns": [
|
|
"base-aware specification status transitions",
|
|
"accepted contract revision identity",
|
|
"authority registry, fixture, and semantic profile validation",
|
|
"structural authority-policy closure",
|
|
"authority conformance receipt validation",
|
|
"terminal specification decisions",
|
|
],
|
|
},
|
|
{
|
|
"id": "export-policy",
|
|
"path": "researcher/scripts/validate_export.py",
|
|
"owns": ["classification export routes", "public projections", "staged export closure"],
|
|
},
|
|
{
|
|
"id": "public-repository-boundary",
|
|
"path": "researcher/scripts/validate_public_repo.py",
|
|
"owns": [
|
|
"tracked local paths",
|
|
"private runtime roots",
|
|
"credential filenames",
|
|
"private-key material",
|
|
],
|
|
},
|
|
{
|
|
"id": "schema-contract",
|
|
"path": "researcher/scripts/validate_schemas.py",
|
|
"owns": ["schema registry", "canonical records", "legacy adapters", "conformance evidence"],
|
|
},
|
|
{
|
|
"id": "platform-compatibility",
|
|
"path": "researcher/scripts/validate_platform_compat.py",
|
|
"owns": ["Agent Skills format", "platform install layouts", "reference validator"],
|
|
},
|
|
{
|
|
"id": "repository-structure",
|
|
"path": "researcher/scripts/validate_repo.py",
|
|
"owns": ["content invariants", "rubric math", "source evaluations", "run fixtures"],
|
|
},
|
|
{
|
|
"id": "skill-health",
|
|
"path": "researcher/scripts/skill_health.py",
|
|
"owns": ["skill body quality score"],
|
|
},
|
|
{
|
|
"id": "activation-cases",
|
|
"path": "researcher/scripts/check_activation_cases.py",
|
|
"owns": ["activation boundary smoke tests"],
|
|
},
|
|
{
|
|
"id": "adversarial-benchmarks",
|
|
"path": "researcher/scripts/run_benchmarks.py",
|
|
"owns": ["deterministic benchmark composition", "adversarial scenario execution"],
|
|
},
|
|
)
|
|
|
|
LIVE_DOCUMENT_LINKS = {
|
|
"README.md": "researcher/generated/corpus-summary.md",
|
|
"AGENTS.md": "researcher/generated/corpus-summary.md",
|
|
"CLAUDE.md": "researcher/generated/corpus-summary.md",
|
|
"researcher/README.md": "generated/corpus-summary.md",
|
|
}
|
|
|
|
|
|
class InventoryError(ValueError):
|
|
"""Raised for structural errors that prevent deterministic parsing."""
|
|
|
|
|
|
class DuplicateKeyError(InventoryError):
|
|
pass
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Finding:
|
|
code: str
|
|
path: str
|
|
artifact_id: str | None
|
|
message: str
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return asdict(self)
|
|
|
|
|
|
def canonical_json_bytes(value: Any) -> bytes:
|
|
"""Canonical bytes for deterministic repository views.
|
|
|
|
This is the repository's v1 sorted-key profile. SPEC-003 replaces it with
|
|
a registered cross-language canonicalization profile for durable records.
|
|
Inventory values are limited to strings, integers, booleans, null, lists,
|
|
and objects, so JSON number edge cases cannot enter this view.
|
|
"""
|
|
|
|
return json.dumps(
|
|
value,
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
allow_nan=False,
|
|
).encode("utf-8")
|
|
|
|
|
|
def pretty_json(value: Any) -> str:
|
|
return json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2, allow_nan=False) + "\n"
|
|
|
|
|
|
def sha256_bytes(value: bytes) -> str:
|
|
return f"sha256:{hashlib.sha256(value).hexdigest()}"
|
|
|
|
|
|
def record_digest(value: Any) -> str:
|
|
return sha256_bytes(canonical_json_bytes(value))
|
|
|
|
|
|
def _reject_duplicate_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
result: dict[str, Any] = {}
|
|
for key, value in pairs:
|
|
if key in result:
|
|
raise DuplicateKeyError(f"duplicate JSON key: {key}")
|
|
result[key] = value
|
|
return result
|
|
|
|
|
|
def parse_json_text(text: str) -> Any:
|
|
return json.loads(text, object_pairs_hook=_reject_duplicate_keys)
|
|
|
|
|
|
def _strip_html_comments(text: str) -> tuple[str, bool]:
|
|
"""Remove CommonMark HTML comments and report an unclosed comment."""
|
|
|
|
output: list[str] = []
|
|
cursor = 0
|
|
while True:
|
|
start = text.find("<!--", cursor)
|
|
if start < 0:
|
|
output.append(text[cursor:])
|
|
return "".join(output), False
|
|
output.append(text[cursor:start])
|
|
end = text.find("-->", start + 4)
|
|
if end < 0:
|
|
output.append("\n" * text[start:].count("\n"))
|
|
return "".join(output), True
|
|
comment = text[start : end + 3]
|
|
output.append("\n" * comment.count("\n"))
|
|
cursor = end + 3
|
|
|
|
|
|
def _is_fence_close(line: str, character: str, minimum_length: int) -> bool:
|
|
return re.fullmatch(
|
|
rf" {{0,3}}{re.escape(character)}{{{minimum_length},}}[ \t]*",
|
|
line,
|
|
) is not None
|
|
|
|
|
|
def contains_raw_html_block(text: str) -> bool:
|
|
"""Return whether visible Markdown contains a raw HTML block opener."""
|
|
|
|
visible_text, _ = _strip_html_comments(text)
|
|
return any(RAW_HTML_BLOCK_START_RE.match(line) for line in visible_text.splitlines())
|
|
|
|
|
|
def markdown_level_two_sections(text: str) -> tuple[dict[str, list[list[str]]], bool]:
|
|
"""Return visible level-two sections and malformed comment/fence status."""
|
|
|
|
sections: dict[str, list[list[str]]] = {}
|
|
current: list[str] | None = None
|
|
fence_character: str | None = None
|
|
fence_length = 0
|
|
visible_text, malformed_comment = _strip_html_comments(text)
|
|
for line in visible_text.splitlines():
|
|
if fence_character is not None:
|
|
if current is not None:
|
|
current.append(line)
|
|
if _is_fence_close(line, fence_character, fence_length):
|
|
fence_character = None
|
|
fence_length = 0
|
|
continue
|
|
fence_match = FENCE_OPEN_RE.fullmatch(line)
|
|
if fence_match is not None:
|
|
if current is not None:
|
|
current.append(line)
|
|
fence = fence_match.group("fence")
|
|
fence_character = fence[0]
|
|
fence_length = len(fence)
|
|
continue
|
|
heading_match = LEVEL_TWO_HEADING_RE.fullmatch(line)
|
|
if heading_match is not None:
|
|
current = []
|
|
sections.setdefault(heading_match.group("title"), []).append(current)
|
|
continue
|
|
if current is not None:
|
|
current.append(line)
|
|
return sections, malformed_comment or fence_character is not None
|
|
|
|
|
|
def visible_unfenced_lines(lines: Iterable[str]) -> list[str]:
|
|
"""Remove CommonMark fenced blocks from an already comment-free section."""
|
|
|
|
visible: list[str] = []
|
|
fence_character: str | None = None
|
|
fence_length = 0
|
|
for line in lines:
|
|
if fence_character is not None:
|
|
if _is_fence_close(line, fence_character, fence_length):
|
|
fence_character = None
|
|
fence_length = 0
|
|
continue
|
|
fence_match = FENCE_OPEN_RE.fullmatch(line)
|
|
if fence_match is not None:
|
|
fence = fence_match.group("fence")
|
|
fence_character = fence[0]
|
|
fence_length = len(fence)
|
|
continue
|
|
visible.append(line)
|
|
return visible
|
|
|
|
|
|
def fenced_blocks(lines: Iterable[str], language: str) -> tuple[list[list[str]], bool]:
|
|
"""Extract complete fenced blocks with one exact language identifier."""
|
|
|
|
blocks: list[list[str]] = []
|
|
current: list[str] | None = None
|
|
fence_character: str | None = None
|
|
fence_length = 0
|
|
selected = False
|
|
for line in lines:
|
|
if fence_character is not None:
|
|
if _is_fence_close(line, fence_character, fence_length):
|
|
if selected and current is not None:
|
|
blocks.append(current)
|
|
current = None
|
|
fence_character = None
|
|
fence_length = 0
|
|
selected = False
|
|
elif selected and current is not None:
|
|
current.append(line)
|
|
continue
|
|
fence_match = FENCE_OPEN_RE.fullmatch(line)
|
|
if fence_match is None:
|
|
continue
|
|
fence = fence_match.group("fence")
|
|
fence_character = fence[0]
|
|
fence_length = len(fence)
|
|
selected = fence_match.group("info").strip() == language
|
|
current = [] if selected else None
|
|
return blocks, fence_character is not None
|
|
|
|
|
|
def atomic_write_text(path: Path, text: str) -> None:
|
|
"""Flush and atomically replace one generated file in its target directory."""
|
|
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
descriptor, temporary_name = tempfile.mkstemp(
|
|
prefix=f".{path.name}.", suffix=".tmp", dir=str(path.parent)
|
|
)
|
|
temporary = Path(temporary_name)
|
|
try:
|
|
with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as handle:
|
|
handle.write(text)
|
|
handle.flush()
|
|
os.fsync(handle.fileno())
|
|
os.replace(temporary, path)
|
|
if hasattr(os, "O_DIRECTORY"):
|
|
directory_fd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
|
|
try:
|
|
os.fsync(directory_fd)
|
|
finally:
|
|
os.close(directory_fd)
|
|
except Exception:
|
|
try:
|
|
temporary.unlink()
|
|
except FileNotFoundError:
|
|
pass
|
|
raise
|
|
|
|
|
|
class InventoryBuilder:
|
|
def __init__(self, root: Path) -> None:
|
|
self.root = root.resolve()
|
|
self.findings: list[Finding] = []
|
|
self.sources: dict[str, dict[str, Any]] = {}
|
|
self.skill_names: set[str] = set()
|
|
self.mechanisms: dict[str, dict[str, Any]] = {}
|
|
self.claims: dict[str, dict[str, Any]] = {}
|
|
|
|
def add_finding(
|
|
self,
|
|
code: str,
|
|
path: Path | str,
|
|
message: str,
|
|
artifact_id: str | None = None,
|
|
) -> None:
|
|
self.findings.append(Finding(code, self.relative(path), artifact_id, message))
|
|
|
|
def relative(self, path: Path | str) -> str:
|
|
value = Path(path)
|
|
if not value.is_absolute():
|
|
normalized = PurePosixPath(str(value).replace("\\", "/"))
|
|
if normalized.is_absolute() or ".." in normalized.parts:
|
|
raise InventoryError(f"path escapes repository: {path}")
|
|
return normalized.as_posix()
|
|
try:
|
|
# Preserve the lexical repository path here. add_source resolves
|
|
# the target separately so a symlink can be reported against its
|
|
# in-repository name instead of escaping before a typed finding is
|
|
# emitted.
|
|
lexical = Path(os.path.abspath(value))
|
|
return lexical.relative_to(self.root).as_posix()
|
|
except ValueError as exc:
|
|
raise InventoryError(f"path escapes repository: {path}") from exc
|
|
|
|
def add_source(self, path: Path) -> tuple[str, int]:
|
|
relative = self.relative(path)
|
|
try:
|
|
resolved = path.resolve(strict=True)
|
|
resolved.relative_to(self.root)
|
|
except (OSError, ValueError) as exc:
|
|
self.add_finding("PATH_ESCAPE", relative, f"source is missing or escapes repository: {exc}")
|
|
return "sha256:" + "0" * 64, 0
|
|
if path.is_symlink() or not resolved.is_file():
|
|
self.add_finding("PATH_ESCAPE", relative, "canonical input must be a regular non-symlink file")
|
|
return "sha256:" + "0" * 64, 0
|
|
body = resolved.read_bytes()
|
|
digest = sha256_bytes(body)
|
|
self.sources[relative] = {"path": relative, "digest": digest, "size_bytes": len(body)}
|
|
return digest, len(body)
|
|
|
|
def load_json(self, path: Path) -> Any | None:
|
|
self.add_source(path)
|
|
try:
|
|
return parse_json_text(path.read_text(encoding="utf-8"))
|
|
except (OSError, UnicodeError, json.JSONDecodeError, DuplicateKeyError) as exc:
|
|
self.add_finding("PARSE_ERROR", path, str(exc))
|
|
return None
|
|
|
|
def load_jsonl(self, path: Path) -> list[tuple[int, dict[str, Any], str]]:
|
|
self.add_source(path)
|
|
try:
|
|
lines = path.read_text(encoding="utf-8").splitlines()
|
|
except (OSError, UnicodeError) as exc:
|
|
self.add_finding("PARSE_ERROR", path, str(exc))
|
|
return []
|
|
records: list[tuple[int, dict[str, Any], str]] = []
|
|
for line_number, raw in enumerate(lines, start=1):
|
|
if not raw.strip():
|
|
continue
|
|
try:
|
|
value = parse_json_text(raw)
|
|
except (json.JSONDecodeError, DuplicateKeyError) as exc:
|
|
self.add_finding("PARSE_ERROR", path, f"line {line_number}: {exc}")
|
|
continue
|
|
if not isinstance(value, dict):
|
|
self.add_finding("PARSE_ERROR", path, f"line {line_number}: expected object")
|
|
continue
|
|
records.append((line_number, value, record_digest(value)))
|
|
return records
|
|
|
|
@staticmethod
|
|
def _category(owner: str, records: Iterable[dict[str, Any]], **extra: Any) -> dict[str, Any]:
|
|
materialized = list(records)
|
|
return {"owner": owner, "count": len(materialized), "records": materialized, **extra}
|
|
|
|
def build(self) -> dict[str, Any]:
|
|
skills = self.build_skills()
|
|
mechanisms = self.build_mechanisms()
|
|
claims = self.build_claims()
|
|
ledgers = self.build_ledgers()
|
|
corpus, relationships = self.build_corpus_index()
|
|
activation = self.build_activation_cases()
|
|
relationships["skill_activation_cases"] = sorted(
|
|
(
|
|
{"skill_id": record["expected_primary_skill"], "case_id": record["id"]}
|
|
for record in activation["records"]
|
|
),
|
|
key=lambda item: (item["skill_id"], item["case_id"]),
|
|
)
|
|
router = self.build_router_prompts()
|
|
scenarios, goldens = self.build_adversarial()
|
|
effectiveness = self.build_effectiveness_tasks()
|
|
benchmark_runners = self.build_benchmark_runners()
|
|
examples = self.build_examples()
|
|
manifests = self.build_manifests()
|
|
validators = self.build_validators()
|
|
schemas = self.build_schemas()
|
|
export_contracts = self.build_export_contracts()
|
|
schema_contracts = self.build_schema_contracts()
|
|
specifications = self.build_specifications()
|
|
architecture_decisions = self.build_architecture_decisions(
|
|
{record["id"] for record in specifications["records"]}
|
|
)
|
|
decisions_by_id = {
|
|
record["id"]: record for record in architecture_decisions["records"]
|
|
}
|
|
for record in specifications["records"]:
|
|
adoption_decision = record.get("adoption_decision")
|
|
if adoption_decision is None:
|
|
continue
|
|
decision = decisions_by_id.get(adoption_decision)
|
|
if decision is None:
|
|
self.add_finding(
|
|
"DANGLING_SPEC_ADOPTION_DECISION",
|
|
self.root / record["path"],
|
|
f"adoption decision does not exist: {adoption_decision}",
|
|
record["id"],
|
|
)
|
|
continue
|
|
if decision["status"] != "accepted":
|
|
self.add_finding(
|
|
"UNACCEPTED_SPEC_ADOPTION_DECISION",
|
|
self.root / record["path"],
|
|
f"adoption decision must be accepted: {adoption_decision}",
|
|
record["id"],
|
|
)
|
|
if record["id"] not in decision["specifications"]:
|
|
self.add_finding(
|
|
"OUT_OF_SCOPE_SPEC_ADOPTION_DECISION",
|
|
self.root / record["path"],
|
|
f"{adoption_decision} does not scope {record['id']}",
|
|
record["id"],
|
|
)
|
|
for record in specifications["records"]:
|
|
lifecycle_decision = record.get("lifecycle_decision")
|
|
if lifecycle_decision is None:
|
|
continue
|
|
decision = decisions_by_id.get(lifecycle_decision)
|
|
if decision is None:
|
|
self.add_finding(
|
|
"DANGLING_SPEC_LIFECYCLE_DECISION",
|
|
self.root / record["path"],
|
|
f"lifecycle decision does not exist: {lifecycle_decision}",
|
|
record["id"],
|
|
)
|
|
continue
|
|
if decision["status"] != "accepted":
|
|
self.add_finding(
|
|
"UNACCEPTED_SPEC_LIFECYCLE_DECISION",
|
|
self.root / record["path"],
|
|
f"lifecycle decision must be accepted: {lifecycle_decision}",
|
|
record["id"],
|
|
)
|
|
if record["id"] not in decision["specifications"]:
|
|
self.add_finding(
|
|
"OUT_OF_SCOPE_SPEC_LIFECYCLE_DECISION",
|
|
self.root / record["path"],
|
|
f"{lifecycle_decision} does not scope {record['id']}",
|
|
record["id"],
|
|
)
|
|
if record["status"] in SPEC_TERMINAL_STATUSES:
|
|
expected_target = (
|
|
record.get("replacement")
|
|
if record["status"] in {"amended", "superseded"}
|
|
else "none"
|
|
)
|
|
expected_transition = (
|
|
f"{record['id']}@{record['revision']} -> "
|
|
f"{record['status']} -> {expected_target}"
|
|
)
|
|
if decision.get("lifecycle_transition") != expected_transition:
|
|
self.add_finding(
|
|
"SPEC_LIFECYCLE_DECISION_MISMATCH",
|
|
self.root / record["path"],
|
|
f"{lifecycle_decision} must bind exact transition "
|
|
f"{expected_transition!r}",
|
|
record["id"],
|
|
)
|
|
self.validate_live_document_links()
|
|
|
|
artifacts = {
|
|
"skills": skills,
|
|
"mechanisms": mechanisms,
|
|
"claims": claims,
|
|
"mechanism_ledgers": ledgers,
|
|
"corpus_index": corpus,
|
|
"activation_cases": activation,
|
|
"router_prompts": router,
|
|
"adversarial_scenarios": scenarios,
|
|
"adversarial_goldens": goldens,
|
|
"effectiveness_tasks": effectiveness,
|
|
"benchmark_runners": benchmark_runners,
|
|
"examples": examples,
|
|
"manifests": manifests,
|
|
"validators": validators,
|
|
"schemas": schemas,
|
|
"export_contracts": export_contracts,
|
|
"schema_contracts": schema_contracts,
|
|
"specifications": specifications,
|
|
"architecture_decisions": architecture_decisions,
|
|
}
|
|
source_records = sorted(self.sources.values(), key=lambda item: item["path"])
|
|
source_tree_digest = sha256_bytes(
|
|
canonical_json_bytes({"schema_version": SCHEMA_VERSION, "sources": source_records})
|
|
)
|
|
unresolved = sorted(
|
|
(finding.to_dict() for finding in self.findings),
|
|
key=lambda item: (item["code"], item["path"], item["artifact_id"] or "", item["message"]),
|
|
)
|
|
return {
|
|
"schema_version": SCHEMA_VERSION,
|
|
"repository_revision": {
|
|
"kind": "canonical_source_tree",
|
|
"digest": source_tree_digest,
|
|
"git_commit_excluded_to_avoid_self_reference": True,
|
|
},
|
|
"source_tree_digest": source_tree_digest,
|
|
"sources": source_records,
|
|
"artifacts": artifacts,
|
|
"relationships": relationships,
|
|
"compatibility": {
|
|
"plugin_versions": manifests["versions"],
|
|
"manifest_skill_sets": manifests["skill_sets"],
|
|
"benchmark_runner": {
|
|
"router": benchmark_runners["status"]["router"],
|
|
"effectiveness": benchmark_runners["status"]["effectiveness"],
|
|
"paid_results": "runtime_or_published_only",
|
|
},
|
|
"historical_reports_are_snapshots": True,
|
|
},
|
|
"validator_ownership": validators["records"],
|
|
"unresolved_references": unresolved,
|
|
}
|
|
|
|
def build_skills(self) -> dict[str, Any]:
|
|
records: list[dict[str, Any]] = []
|
|
skills_dir = self.root / "skills"
|
|
if not skills_dir.exists():
|
|
self.add_finding("PARSE_ERROR", skills_dir, "skills directory is missing")
|
|
return self._category("skills/*/SKILL.md", records)
|
|
for skill_dir in sorted(path for path in skills_dir.iterdir() if path.is_dir()):
|
|
skill_file = skill_dir / "SKILL.md"
|
|
if not skill_file.exists():
|
|
self.add_finding("PARSE_ERROR", skill_file, "SKILL.md is missing", skill_dir.name)
|
|
continue
|
|
digest, _ = self.add_source(skill_file)
|
|
try:
|
|
frontmatter, issues = parse_frontmatter(skill_file.read_text(encoding="utf-8"))
|
|
except (OSError, UnicodeError) as exc:
|
|
self.add_finding("PARSE_ERROR", skill_file, str(exc), skill_dir.name)
|
|
continue
|
|
for issue in issues:
|
|
self.add_finding("PARSE_ERROR", skill_file, issue, skill_dir.name)
|
|
name = frontmatter.get("name")
|
|
if not isinstance(name, str) or not name:
|
|
name = skill_dir.name
|
|
if name != skill_dir.name:
|
|
self.add_finding(
|
|
"SKILL_PATH_MISMATCH",
|
|
skill_file,
|
|
f"frontmatter name {name!r} does not match directory {skill_dir.name!r}",
|
|
str(name),
|
|
)
|
|
if name in self.skill_names:
|
|
self.add_finding("DUPLICATE_SKILL_ID", skill_file, "duplicate skill identifier", name)
|
|
self.skill_names.add(name)
|
|
records.append({"id": name, "path": self.relative(skill_file), "digest": digest})
|
|
return self._category("skills/*/SKILL.md", sorted(records, key=lambda item: item["id"]))
|
|
|
|
def build_mechanisms(self) -> dict[str, Any]:
|
|
path = self.root / "researcher" / "mechanisms" / "registry.jsonl"
|
|
records: list[dict[str, Any]] = []
|
|
for line_number, value, digest in self.load_jsonl(path):
|
|
mechanism_id = value.get("mechanism_id")
|
|
if not isinstance(mechanism_id, str) or not mechanism_id:
|
|
self.add_finding("PARSE_ERROR", path, f"line {line_number}: mechanism_id is required")
|
|
continue
|
|
if mechanism_id in self.mechanisms:
|
|
self.add_finding(
|
|
"DUPLICATE_MECHANISM_ID", path, f"duplicate at line {line_number}", mechanism_id
|
|
)
|
|
owner = value.get("owning_skill")
|
|
if owner not in self.skill_names:
|
|
self.add_finding(
|
|
"DANGLING_SKILL", path, f"unknown owning_skill {owner!r}", mechanism_id
|
|
)
|
|
self.mechanisms[mechanism_id] = value
|
|
records.append(
|
|
{
|
|
"id": mechanism_id,
|
|
"owning_skill": owner,
|
|
"status": value.get("status"),
|
|
"line": line_number,
|
|
"digest": digest,
|
|
}
|
|
)
|
|
return self._category(
|
|
"researcher/mechanisms/registry.jsonl", sorted(records, key=lambda item: item["id"])
|
|
)
|
|
|
|
def build_claims(self) -> dict[str, Any]:
|
|
path = self.root / "researcher" / "claims" / "index.jsonl"
|
|
records: list[dict[str, Any]] = []
|
|
for line_number, value, digest in self.load_jsonl(path):
|
|
claim_id = value.get("claim_id")
|
|
if not isinstance(claim_id, str) or not claim_id:
|
|
self.add_finding("PARSE_ERROR", path, f"line {line_number}: claim_id is required")
|
|
continue
|
|
if claim_id in self.claims:
|
|
self.add_finding("DUPLICATE_CLAIM_ID", path, f"duplicate at line {line_number}", claim_id)
|
|
owner = value.get("owning_skill")
|
|
if owner not in self.skill_names:
|
|
self.add_finding("DANGLING_SKILL", path, f"unknown owning_skill {owner!r}", claim_id)
|
|
self.claims[claim_id] = value
|
|
records.append(
|
|
{
|
|
"id": claim_id,
|
|
"owning_skill": owner,
|
|
"volatility": value.get("volatility"),
|
|
"line": line_number,
|
|
"digest": digest,
|
|
}
|
|
)
|
|
return self._category(
|
|
"researcher/claims/index.jsonl", sorted(records, key=lambda item: item["id"])
|
|
)
|
|
|
|
def build_ledgers(self) -> dict[str, Any]:
|
|
ledger_dir = self.root / "researcher" / "mechanisms" / "ledgers"
|
|
accepted_path = ledger_dir / "accepted.jsonl"
|
|
rejected_path = ledger_dir / "rejected.jsonl"
|
|
accepted = self.load_jsonl(accepted_path)
|
|
rejected = self.load_jsonl(rejected_path)
|
|
records: list[dict[str, Any]] = []
|
|
accepted_ids: set[str] = set()
|
|
durable_provenance: set[str] = set()
|
|
superseded_lines = {
|
|
value.get("supersedes_legacy_line")
|
|
for _, value, _ in accepted
|
|
if isinstance(value.get("supersedes_legacy_line"), int)
|
|
}
|
|
seen_event_ids: set[str] = set()
|
|
for ledger_name, path, entries in (
|
|
("accepted", accepted_path, accepted),
|
|
("rejected", rejected_path, rejected),
|
|
):
|
|
for line_number, value, digest in entries:
|
|
mechanism_id = value.get("mechanism_id")
|
|
event_id = value.get("event_id")
|
|
if isinstance(event_id, str):
|
|
if event_id in seen_event_ids:
|
|
self.add_finding(
|
|
"DUPLICATE_LEDGER_EVENT_ID", path, f"duplicate event at line {line_number}", event_id
|
|
)
|
|
seen_event_ids.add(event_id)
|
|
if not isinstance(mechanism_id, str) or not mechanism_id:
|
|
self.add_finding("PARSE_ERROR", path, f"line {line_number}: mechanism_id is required")
|
|
continue
|
|
if ledger_name == "accepted":
|
|
accepted_ids.add(mechanism_id)
|
|
if mechanism_id not in self.mechanisms:
|
|
self.add_finding(
|
|
"ORPHAN_ACCEPTED_LEDGER_EVENT", path, "mechanism is absent from registry", mechanism_id
|
|
)
|
|
elif self.mechanisms[mechanism_id].get("status") != "accepted":
|
|
self.add_finding(
|
|
"LEDGER_STATUS_MISMATCH", path, "accepted ledger disagrees with registry", mechanism_id
|
|
)
|
|
source = value.get("source") or value.get("source_path")
|
|
source_exists = isinstance(source, str) and (self.root / source).exists()
|
|
has_commit = isinstance(value.get("source_commit"), str) and bool(value["source_commit"])
|
|
if source_exists and (value.get("event_type") is None or has_commit):
|
|
durable_provenance.add(mechanism_id)
|
|
if isinstance(value.get("run_dir"), str) and line_number not in superseded_lines:
|
|
run_path = self.root / value["run_dir"]
|
|
if not run_path.exists():
|
|
self.add_finding(
|
|
"DANGLING_LEDGER_RUN",
|
|
path,
|
|
f"line {line_number}: run_dir is unavailable",
|
|
mechanism_id,
|
|
)
|
|
records.append(
|
|
{
|
|
"ledger": ledger_name,
|
|
"line": line_number,
|
|
"event_id": event_id,
|
|
"mechanism_id": mechanism_id,
|
|
"event_type": value.get("event_type", "legacy"),
|
|
"digest": digest,
|
|
}
|
|
)
|
|
for mechanism_id, mechanism in sorted(self.mechanisms.items()):
|
|
if mechanism.get("status") == "accepted" and mechanism_id not in accepted_ids:
|
|
self.add_finding(
|
|
"MISSING_ACCEPTED_LEDGER_EVENT",
|
|
accepted_path,
|
|
"accepted registry mechanism has no accepted-ledger event",
|
|
mechanism_id,
|
|
)
|
|
if mechanism.get("status") == "accepted" and mechanism_id not in durable_provenance:
|
|
self.add_finding(
|
|
"DANGLING_LEDGER_SOURCE",
|
|
accepted_path,
|
|
"accepted mechanism has no durable public provenance event",
|
|
mechanism_id,
|
|
)
|
|
return self._category(
|
|
"researcher/mechanisms/ledgers/*.jsonl",
|
|
records,
|
|
accepted_event_count=len(accepted),
|
|
rejected_event_count=len(rejected),
|
|
accepted_mechanism_count=len(accepted_ids),
|
|
)
|
|
|
|
def build_corpus_index(self) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
path = self.root / "researcher" / "corpus" / "index.json"
|
|
value = self.load_json(path)
|
|
records: list[dict[str, Any]] = []
|
|
mechanism_relations: list[dict[str, str]] = []
|
|
claim_relations: list[dict[str, str]] = []
|
|
indexed_skills: set[str] = set()
|
|
indexed_mechanisms: set[str] = set()
|
|
indexed_claims: set[str] = set()
|
|
if not isinstance(value, dict) or not isinstance(value.get("skills"), list):
|
|
self.add_finding("UNKNOWN_SCHEMA", path, "corpus index must contain a skills list")
|
|
return self._category("researcher/corpus/index.json", records), {
|
|
"skill_mechanisms": [],
|
|
"skill_claims": [],
|
|
"skill_activation_cases": [],
|
|
}
|
|
for item in value["skills"]:
|
|
if not isinstance(item, dict):
|
|
self.add_finding("UNKNOWN_SCHEMA", path, "skill entry must be an object")
|
|
continue
|
|
name = item.get("name")
|
|
if not isinstance(name, str) or not name:
|
|
self.add_finding("UNKNOWN_SCHEMA", path, "skill entry is missing name")
|
|
continue
|
|
if name in indexed_skills:
|
|
self.add_finding("DUPLICATE_SKILL_ID", path, "duplicate corpus skill", name)
|
|
indexed_skills.add(name)
|
|
if name not in self.skill_names:
|
|
self.add_finding("DANGLING_SKILL", path, "indexed skill is not published", name)
|
|
expected_path = f"skills/{name}/SKILL.md"
|
|
if item.get("path") != expected_path:
|
|
self.add_finding(
|
|
"SKILL_PATH_MISMATCH", path, f"expected {expected_path}, got {item.get('path')}", name
|
|
)
|
|
mechanism_ids = item.get("mechanism_ids", [])
|
|
claim_ids = item.get("claim_ids", [])
|
|
if not isinstance(mechanism_ids, list) or not isinstance(claim_ids, list):
|
|
self.add_finding("UNKNOWN_SCHEMA", path, "mechanism_ids and claim_ids must be lists", name)
|
|
continue
|
|
for mechanism_id in mechanism_ids:
|
|
if mechanism_id not in self.mechanisms:
|
|
self.add_finding(
|
|
"DANGLING_MECHANISM", path, "unknown mechanism reference", str(mechanism_id)
|
|
)
|
|
continue
|
|
indexed_mechanisms.add(mechanism_id)
|
|
owner = self.mechanisms[mechanism_id].get("owning_skill")
|
|
if owner != name:
|
|
self.add_finding(
|
|
"MECHANISM_OWNER_MISMATCH",
|
|
path,
|
|
f"registry owner is {owner!r}, index consumer is {name!r}",
|
|
mechanism_id,
|
|
)
|
|
mechanism_relations.append({"skill_id": name, "mechanism_id": mechanism_id})
|
|
for claim_id in claim_ids:
|
|
if claim_id not in self.claims:
|
|
self.add_finding("DANGLING_CLAIM", path, "unknown claim reference", str(claim_id))
|
|
continue
|
|
indexed_claims.add(claim_id)
|
|
relation = "owned" if self.claims[claim_id].get("owning_skill") == name else "related"
|
|
claim_relations.append(
|
|
{"skill_id": name, "claim_id": claim_id, "relationship": relation}
|
|
)
|
|
skill_path = self.root / expected_path
|
|
body_claim_ids: list[str] = []
|
|
if skill_path.exists():
|
|
text = skill_path.read_text(encoding="utf-8")
|
|
body_claim_ids = sorted(claim_id for claim_id in self.claims if claim_id in text)
|
|
records.append(
|
|
{
|
|
"id": name,
|
|
"path": item.get("path"),
|
|
"activation_scenarios": item.get("activation_scenarios", []),
|
|
"mechanism_ids": sorted(mechanism_ids),
|
|
"claim_ids": sorted(claim_ids),
|
|
"explicit_body_claim_ids": body_claim_ids,
|
|
}
|
|
)
|
|
for skill_id in sorted(self.skill_names - indexed_skills):
|
|
self.add_finding("UNINDEXED_SKILL", path, "published skill is absent from corpus index", skill_id)
|
|
for mechanism_id in sorted(set(self.mechanisms) - indexed_mechanisms):
|
|
self.add_finding(
|
|
"UNINDEXED_MECHANISM", path, "registry mechanism is absent from corpus relationships", mechanism_id
|
|
)
|
|
for claim_id in sorted(set(self.claims) - indexed_claims):
|
|
self.add_finding("UNINDEXED_CLAIM", path, "claim is absent from corpus relationships", claim_id)
|
|
return (
|
|
self._category(
|
|
"researcher/corpus/index.json",
|
|
sorted(records, key=lambda item: item["id"]),
|
|
semantics="curated_relationship_source",
|
|
),
|
|
{
|
|
"skill_mechanisms": sorted(
|
|
mechanism_relations, key=lambda item: (item["skill_id"], item["mechanism_id"])
|
|
),
|
|
"skill_claims": sorted(
|
|
claim_relations, key=lambda item: (item["skill_id"], item["claim_id"])
|
|
),
|
|
"skill_activation_cases": [],
|
|
},
|
|
)
|
|
|
|
def build_activation_cases(self) -> dict[str, Any]:
|
|
path = self.root / "researcher" / "fixtures" / "activation-cases.jsonl"
|
|
records: list[dict[str, Any]] = []
|
|
seen: set[str] = set()
|
|
for line_number, value, digest in self.load_jsonl(path):
|
|
case_id = value.get("case_id")
|
|
if not isinstance(case_id, str) or not case_id:
|
|
self.add_finding("PARSE_ERROR", path, f"line {line_number}: case_id is required")
|
|
continue
|
|
if case_id in seen:
|
|
self.add_finding(
|
|
"DUPLICATE_ACTIVATION_CASE_ID", path, f"duplicate at line {line_number}", case_id
|
|
)
|
|
seen.add(case_id)
|
|
referenced = [value.get("expected_primary_skill")]
|
|
referenced.extend(value.get("acceptable_secondary_skills", []))
|
|
referenced.extend(value.get("rejected_skills", []))
|
|
for skill_id in referenced:
|
|
if skill_id not in self.skill_names:
|
|
self.add_finding(
|
|
"DANGLING_SKILL", path, f"activation case references {skill_id!r}", case_id
|
|
)
|
|
records.append(
|
|
{
|
|
"id": case_id,
|
|
"expected_primary_skill": value.get("expected_primary_skill"),
|
|
"line": line_number,
|
|
"digest": digest,
|
|
}
|
|
)
|
|
return self._category(
|
|
"researcher/fixtures/activation-cases.jsonl", sorted(records, key=lambda item: item["id"])
|
|
)
|
|
|
|
def build_router_prompts(self) -> dict[str, Any]:
|
|
path = self.root / "researcher" / "benchmarks" / "router" / "prompts.jsonl"
|
|
records: list[dict[str, Any]] = []
|
|
seen: set[str] = set()
|
|
for line_number, value, digest in self.load_jsonl(path):
|
|
prompt_id = value.get("prompt_id")
|
|
if not isinstance(prompt_id, str) or not prompt_id:
|
|
self.add_finding("PARSE_ERROR", path, f"line {line_number}: prompt_id is required")
|
|
continue
|
|
if prompt_id in seen:
|
|
self.add_finding(
|
|
"DUPLICATE_ROUTER_PROMPT_ID", path, f"duplicate at line {line_number}", prompt_id
|
|
)
|
|
seen.add(prompt_id)
|
|
referenced = [value.get("expected_primary_skill")]
|
|
referenced.extend(value.get("acceptable_secondary_skills", []))
|
|
referenced.extend(value.get("rejected_skills", []))
|
|
for skill_id in referenced:
|
|
if skill_id not in self.skill_names:
|
|
self.add_finding("DANGLING_SKILL", path, f"router prompt references {skill_id!r}", prompt_id)
|
|
if not isinstance(value.get("prompt"), str) or not value["prompt"].strip():
|
|
self.add_finding("UNKNOWN_SCHEMA", path, "router prompt text is required", prompt_id)
|
|
records.append(
|
|
{
|
|
"id": prompt_id,
|
|
"expected_primary_skill": value.get("expected_primary_skill"),
|
|
"line": line_number,
|
|
"digest": digest,
|
|
}
|
|
)
|
|
return self._category(
|
|
"researcher/benchmarks/router/prompts.jsonl", sorted(records, key=lambda item: item["id"])
|
|
)
|
|
|
|
def build_adversarial(self) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
scenario_dir = self.root / "researcher" / "benchmarks" / "scenarios"
|
|
scenario_records: list[dict[str, Any]] = []
|
|
scenarios: dict[str, dict[str, Any]] = {}
|
|
for path in sorted(scenario_dir.glob("*.jsonl")):
|
|
for line_number, value, digest in self.load_jsonl(path):
|
|
scenario_id = value.get("scenario_id")
|
|
if not isinstance(scenario_id, str) or not scenario_id:
|
|
self.add_finding("PARSE_ERROR", path, f"line {line_number}: scenario_id is required")
|
|
continue
|
|
if scenario_id in scenarios:
|
|
self.add_finding(
|
|
"DUPLICATE_SCENARIO_ID", path, f"duplicate at line {line_number}", scenario_id
|
|
)
|
|
scenarios[scenario_id] = value
|
|
scenario_records.append(
|
|
{
|
|
"id": scenario_id,
|
|
"expected_gate": value.get("expected_gate"),
|
|
"path": self.relative(path),
|
|
"line": line_number,
|
|
"digest": digest,
|
|
}
|
|
)
|
|
golden_path = self.root / "researcher" / "benchmarks" / "goldens" / "adversarial-goldens.json"
|
|
golden_value = self.load_json(golden_path)
|
|
golden_records: list[dict[str, Any]] = []
|
|
if not isinstance(golden_value, dict):
|
|
self.add_finding("UNKNOWN_SCHEMA", golden_path, "golden file must be an object")
|
|
golden_value = {}
|
|
for scenario_id in sorted(set(scenarios) - set(golden_value)):
|
|
self.add_finding("MISSING_GOLDEN", golden_path, "scenario has no golden", scenario_id)
|
|
for scenario_id in sorted(set(golden_value) - set(scenarios)):
|
|
self.add_finding("ORPHAN_GOLDEN", golden_path, "golden has no scenario", scenario_id)
|
|
for scenario_id, golden in sorted(golden_value.items()):
|
|
expected_gate = golden.get("expected_gate") if isinstance(golden, dict) else None
|
|
if scenario_id in scenarios and expected_gate != scenarios[scenario_id].get("expected_gate"):
|
|
self.add_finding(
|
|
"GOLDEN_GATE_MISMATCH", golden_path, "golden and scenario expected_gate differ", scenario_id
|
|
)
|
|
golden_records.append(
|
|
{"id": scenario_id, "expected_gate": expected_gate, "digest": record_digest(golden)}
|
|
)
|
|
return (
|
|
self._category(
|
|
"researcher/benchmarks/scenarios/*.jsonl",
|
|
sorted(scenario_records, key=lambda item: item["id"]),
|
|
),
|
|
self._category("researcher/benchmarks/goldens/adversarial-goldens.json", golden_records),
|
|
)
|
|
|
|
def build_effectiveness_tasks(self) -> dict[str, Any]:
|
|
tasks_dir = self.root / "researcher" / "benchmarks" / "effectiveness" / "tasks"
|
|
records: list[dict[str, Any]] = []
|
|
seen: set[str] = set()
|
|
if not tasks_dir.exists():
|
|
self.add_finding("INVALID_EFFECTIVENESS_TASK", tasks_dir, "tasks directory is missing")
|
|
return self._category("researcher/benchmarks/effectiveness/tasks/*", records)
|
|
for task_dir in sorted(path for path in tasks_dir.iterdir() if path.is_dir()):
|
|
metadata_path = task_dir / "metadata.json"
|
|
metadata = self.load_json(metadata_path)
|
|
if not isinstance(metadata, dict):
|
|
self.add_finding("INVALID_EFFECTIVENESS_TASK", metadata_path, "metadata must be an object")
|
|
continue
|
|
task_id = metadata.get("id")
|
|
slug = metadata.get("slug")
|
|
if not isinstance(task_id, str) or not isinstance(slug, str):
|
|
self.add_finding("INVALID_EFFECTIVENESS_TASK", metadata_path, "id and slug are required")
|
|
continue
|
|
if task_id in seen:
|
|
self.add_finding(
|
|
"DUPLICATE_EFFECTIVENESS_TASK_ID", metadata_path, "duplicate task id", task_id
|
|
)
|
|
seen.add(task_id)
|
|
expected_dir = f"{task_id}-{slug}"
|
|
if task_dir.name != expected_dir:
|
|
self.add_finding(
|
|
"INVALID_EFFECTIVENESS_TASK",
|
|
task_dir,
|
|
f"directory must be named {expected_dir}",
|
|
task_id,
|
|
)
|
|
required = [task_dir / "README.md", task_dir / "task.md", task_dir / "verify.sh"]
|
|
starting = task_dir / "starting"
|
|
for required_path in required:
|
|
if not required_path.is_file():
|
|
self.add_finding(
|
|
"INVALID_EFFECTIVENESS_TASK", required_path, "required task file is missing", task_id
|
|
)
|
|
else:
|
|
self.add_source(required_path)
|
|
if not starting.is_dir() or not any(path.is_file() for path in starting.rglob("*")):
|
|
self.add_finding(
|
|
"INVALID_EFFECTIVENESS_TASK", starting, "starting directory must contain a file", task_id
|
|
)
|
|
else:
|
|
for starting_file in sorted(path for path in starting.rglob("*") if path.is_file()):
|
|
self.add_source(starting_file)
|
|
verify = task_dir / "verify.sh"
|
|
if verify.exists() and not (verify.stat().st_mode & stat.S_IXUSR):
|
|
self.add_finding(
|
|
"INVALID_EFFECTIVENESS_TASK", verify, "verify.sh must be executable", task_id
|
|
)
|
|
for key in ("target_skill", "irrelevant_skill"):
|
|
skill_id = metadata.get(key)
|
|
if skill_id != "none" and skill_id not in self.skill_names:
|
|
self.add_finding(
|
|
"DANGLING_SKILL", metadata_path, f"{key} references {skill_id!r}", task_id
|
|
)
|
|
records.append(
|
|
{
|
|
"id": task_id,
|
|
"slug": slug,
|
|
"path": self.relative(task_dir),
|
|
"target_skill": metadata.get("target_skill"),
|
|
"irrelevant_skill": metadata.get("irrelevant_skill"),
|
|
"metadata_digest": record_digest(metadata),
|
|
}
|
|
)
|
|
return self._category(
|
|
"researcher/benchmarks/effectiveness/tasks/*", sorted(records, key=lambda item: item["id"])
|
|
)
|
|
|
|
def build_examples(self) -> dict[str, Any]:
|
|
examples_dir = self.root / "examples"
|
|
records: list[dict[str, Any]] = []
|
|
for example_dir in sorted(path for path in examples_dir.iterdir() if path.is_dir()):
|
|
readme = example_dir / "README.md"
|
|
if not readme.exists():
|
|
self.add_finding("PARSE_ERROR", example_dir, "example directory is missing README.md")
|
|
continue
|
|
digest, _ = self.add_source(readme)
|
|
records.append({"id": example_dir.name, "path": self.relative(example_dir), "digest": digest})
|
|
return self._category("examples/*/README.md", records)
|
|
|
|
def build_benchmark_runners(self) -> dict[str, Any]:
|
|
runner_dir = self.root / "researcher" / "benchmarks" / "sdk-runner"
|
|
records: list[dict[str, Any]] = []
|
|
for relative in [
|
|
"package.json",
|
|
"package-lock.json",
|
|
"tsconfig.json",
|
|
"src/common.ts",
|
|
"src/runRouter.ts",
|
|
"src/runEffectiveness.ts",
|
|
]:
|
|
path = runner_dir / relative
|
|
if not path.exists():
|
|
self.add_finding("PARSE_ERROR", path, "benchmark runner contract file is missing", relative)
|
|
continue
|
|
digest, size = self.add_source(path)
|
|
records.append(
|
|
{
|
|
"id": relative.replace("/", ":"),
|
|
"path": self.relative(path),
|
|
"digest": digest,
|
|
"size_bytes": size,
|
|
}
|
|
)
|
|
return self._category(
|
|
"researcher/benchmarks/sdk-runner",
|
|
sorted(records, key=lambda item: item["id"]),
|
|
status={"router": "operational", "effectiveness": "scaffold"},
|
|
)
|
|
|
|
def build_manifests(self) -> dict[str, Any]:
|
|
marketplace_path = self.root / ".claude-plugin" / "marketplace.json"
|
|
plugin_path = self.root / ".plugin" / "plugin.json"
|
|
root_skill_path = self.root / "SKILL.md"
|
|
marketplace = self.load_json(marketplace_path)
|
|
plugin = self.load_json(plugin_path)
|
|
root_skill_digest, _ = self.add_source(root_skill_path)
|
|
root_version: str | None = None
|
|
try:
|
|
version_lines = [
|
|
line.partition(":")[2].strip()
|
|
for line in root_skill_path.read_text(encoding="utf-8").splitlines()
|
|
if line.startswith("**Version**:")
|
|
]
|
|
if len(version_lines) == 1:
|
|
root_version = version_lines[0]
|
|
else:
|
|
self.add_finding("UNKNOWN_SCHEMA", root_skill_path, "expected exactly one **Version** line")
|
|
except (OSError, UnicodeError) as exc:
|
|
self.add_finding("PARSE_ERROR", root_skill_path, str(exc))
|
|
marketplace_version = None
|
|
marketplace_skills: list[str] = []
|
|
if isinstance(marketplace, dict):
|
|
marketplace_version = marketplace.get("metadata", {}).get("version")
|
|
plugins = marketplace.get("plugins")
|
|
if isinstance(plugins, list) and len(plugins) == 1 and isinstance(plugins[0], dict):
|
|
raw_paths = plugins[0].get("skills", [])
|
|
if isinstance(raw_paths, list):
|
|
marketplace_skills = sorted(Path(value).name for value in raw_paths if isinstance(value, str))
|
|
plugin_version = plugin.get("version") if isinstance(plugin, dict) else None
|
|
versions = {
|
|
"marketplace": marketplace_version,
|
|
"open_plugin": plugin_version,
|
|
"collection_skill": root_version,
|
|
}
|
|
if len(set(versions.values())) != 1:
|
|
self.add_finding("PLUGIN_VERSION_MISMATCH", plugin_path, f"version surfaces differ: {versions}")
|
|
actual_skills = sorted(self.skill_names)
|
|
if marketplace_skills != actual_skills:
|
|
self.add_finding(
|
|
"MANIFEST_SKILL_MISMATCH",
|
|
marketplace_path,
|
|
f"manifest={marketplace_skills}, published={actual_skills}",
|
|
)
|
|
records = [
|
|
{"id": "claude-marketplace", "path": self.relative(marketplace_path), "version": marketplace_version},
|
|
{"id": "open-plugin", "path": self.relative(plugin_path), "version": plugin_version},
|
|
{
|
|
"id": "collection-skill",
|
|
"path": self.relative(root_skill_path),
|
|
"version": root_version,
|
|
"digest": root_skill_digest,
|
|
},
|
|
]
|
|
return self._category(
|
|
"publication manifests",
|
|
records,
|
|
versions=versions,
|
|
skill_sets={"published": actual_skills, "marketplace": marketplace_skills},
|
|
)
|
|
|
|
def build_validators(self) -> dict[str, Any]:
|
|
records: list[dict[str, Any]] = []
|
|
for descriptor in VALIDATOR_OWNERSHIP:
|
|
descriptor_path = descriptor["path"]
|
|
descriptor_id = descriptor["id"]
|
|
assert isinstance(descriptor_path, str)
|
|
assert isinstance(descriptor_id, str)
|
|
path = self.root / descriptor_path
|
|
if not path.exists():
|
|
self.add_finding("PARSE_ERROR", path, "declared validator is missing", descriptor_id)
|
|
continue
|
|
digest, _ = self.add_source(path)
|
|
records.append({**descriptor, "digest": digest})
|
|
support_records: list[dict[str, Any]] = []
|
|
for relative in (
|
|
".github/workflows/validate.yml",
|
|
"requirements-dev.in",
|
|
"requirements-dev.txt",
|
|
):
|
|
path = self.root / relative
|
|
if not path.is_file():
|
|
self.add_finding("PARSE_ERROR", path, "validation support file is missing", relative)
|
|
continue
|
|
digest, size = self.add_source(path)
|
|
support_records.append({"path": relative, "digest": digest, "size_bytes": size})
|
|
return self._category(
|
|
"declared validator ownership",
|
|
records,
|
|
support_files=support_records,
|
|
)
|
|
|
|
def build_schemas(self) -> dict[str, Any]:
|
|
path = self.root / "researcher" / "corpus" / "inventory.schema.json"
|
|
value = self.load_json(path)
|
|
records: list[dict[str, Any]] = []
|
|
if isinstance(value, dict):
|
|
records.append(
|
|
{
|
|
"id": "repository-inventory-v1",
|
|
"path": self.relative(path),
|
|
"schema_id": value.get("$id"),
|
|
"digest": record_digest(value),
|
|
}
|
|
)
|
|
return self._category("researcher/corpus/inventory.schema.json", records)
|
|
|
|
def build_export_contracts(self) -> dict[str, Any]:
|
|
paths = [
|
|
"governance/export-policy.yaml",
|
|
"governance/export-policy.schema.json",
|
|
"researcher/exports/schemas/export-records.schema.json",
|
|
"researcher/fixtures/export/restricted-request.json",
|
|
"researcher/fixtures/export/private-root/restricted-source.json",
|
|
"researcher/exports/examples/restricted-citation-v1/export-manifest.json",
|
|
"researcher/exports/examples/restricted-citation-v1/citation/restricted-fixture.json",
|
|
"researcher/scripts/export_policy.py",
|
|
]
|
|
records: list[dict[str, Any]] = []
|
|
for relative in paths:
|
|
path = self.root / relative
|
|
if not path.exists():
|
|
self.add_finding("PARSE_ERROR", path, "export contract artifact is missing", relative)
|
|
continue
|
|
digest, size = self.add_source(path)
|
|
records.append({"id": relative, "path": relative, "digest": digest, "size_bytes": size})
|
|
return self._category("governance/export-policy.yaml and export fixtures", records)
|
|
|
|
def build_schema_contracts(self) -> dict[str, Any]:
|
|
registry_path = self.root / "researcher/schemas/registry.json"
|
|
registry = self.load_json(registry_path)
|
|
records: list[dict[str, Any]] = []
|
|
if not isinstance(registry, dict) or not isinstance(registry.get("entries"), list):
|
|
self.add_finding("UNKNOWN_SCHEMA", registry_path, "schema registry has no entries array")
|
|
return self._category("researcher/schemas/registry.json", records)
|
|
seen: set[tuple[str, str]] = set()
|
|
id_prefixes = registry.get("id_prefixes")
|
|
registered_prefixes: dict[str, str] = {}
|
|
for index, entry in enumerate(registry["entries"]):
|
|
if not isinstance(entry, dict):
|
|
self.add_finding("UNKNOWN_SCHEMA", registry_path, f"entry {index} is not an object")
|
|
continue
|
|
kind = entry.get("kind")
|
|
version = entry.get("version")
|
|
if not isinstance(kind, str) or not isinstance(version, str):
|
|
self.add_finding("UNKNOWN_SCHEMA", registry_path, f"entry {index} lacks kind/version")
|
|
continue
|
|
key = (kind, version)
|
|
if key in seen:
|
|
self.add_finding("DUPLICATE_SCHEMA_ID", registry_path, "kind/version is duplicated", kind)
|
|
seen.add(key)
|
|
prefix = entry.get("id_prefix")
|
|
if isinstance(prefix, str):
|
|
registered_prefixes[prefix] = kind
|
|
schema_relative = entry.get("schema_path")
|
|
expected_digest = entry.get("schema_digest")
|
|
if not isinstance(schema_relative, str) or not isinstance(expected_digest, str):
|
|
self.add_finding("UNKNOWN_SCHEMA", registry_path, "schema path or digest is missing", kind)
|
|
continue
|
|
schema_path = self.root / schema_relative
|
|
if not schema_path.is_file():
|
|
self.add_finding("PARSE_ERROR", schema_path, "registered schema file is missing", kind)
|
|
continue
|
|
digest, _ = self.add_source(schema_path)
|
|
if digest != expected_digest:
|
|
self.add_finding(
|
|
"SCHEMA_DIGEST_MISMATCH",
|
|
schema_path,
|
|
f"registry={expected_digest}, actual={digest}",
|
|
kind,
|
|
)
|
|
golden_paths = entry.get("golden_paths")
|
|
if not isinstance(golden_paths, list) or not golden_paths:
|
|
self.add_finding("UNKNOWN_SCHEMA", registry_path, "schema has no golden", kind)
|
|
else:
|
|
for golden_relative in golden_paths:
|
|
if not isinstance(golden_relative, str):
|
|
self.add_finding("UNKNOWN_SCHEMA", registry_path, "golden path is not text", kind)
|
|
continue
|
|
golden_path = self.root / golden_relative
|
|
if not golden_path.is_file():
|
|
self.add_finding("PARSE_ERROR", golden_path, "registered golden is missing", kind)
|
|
else:
|
|
self.add_source(golden_path)
|
|
records.append(
|
|
{
|
|
"id": f"{kind}@{version}",
|
|
"kind": kind,
|
|
"version": version,
|
|
"schema_path": schema_relative,
|
|
"schema_digest": digest,
|
|
"owner_spec": entry.get("owner_spec"),
|
|
"status": entry.get("status"),
|
|
"compatibility": entry.get("compatibility"),
|
|
"id_prefix": prefix,
|
|
}
|
|
)
|
|
if id_prefixes != registered_prefixes:
|
|
self.add_finding(
|
|
"SCHEMA_PREFIX_MISMATCH",
|
|
registry_path,
|
|
"registry id_prefixes disagree with schema entries",
|
|
)
|
|
supporting_paths = [
|
|
"researcher/schemas/registry.schema.json",
|
|
"researcher/schemas/fixtures/canonicalization-v1.json",
|
|
"researcher/schemas/fixtures/editable-surface-policy-v1.json",
|
|
"researcher/schemas/public-legacy-sources.json",
|
|
"researcher/schemas/generated/conformance-report.json",
|
|
"researcher/schemas/generated/current-artifacts-report.json",
|
|
"researcher/schemas/generated/migration-dry-run.json",
|
|
"researcher/schemas/generated/compatibility-matrix.md",
|
|
"researcher/schemas/README.md",
|
|
"researcher/artifacts/README.md",
|
|
"researcher/runbooks/schema-migration.md",
|
|
"researcher/scripts/schema_contract.py",
|
|
"researcher/scripts/artifact_store.py",
|
|
"researcher/scripts/migrate_legacy.py",
|
|
"researcher/scripts/tests/test_schema_contract.py",
|
|
"researcher/scripts/tests/test_artifact_store.py",
|
|
"researcher/schemas/typescript/.gitignore",
|
|
"researcher/schemas/typescript/package.json",
|
|
"researcher/schemas/typescript/package-lock.json",
|
|
"researcher/schemas/typescript/tsconfig.json",
|
|
"researcher/schemas/typescript/src/canonicalize.ts",
|
|
"researcher/schemas/typescript/src/conformance.ts",
|
|
"researcher/schemas/typescript/src/errors.ts",
|
|
"researcher/schemas/typescript/src/index.ts",
|
|
"researcher/schemas/typescript/src/json.ts",
|
|
"researcher/schemas/typescript/src/registry.ts",
|
|
"researcher/schemas/typescript/src/semantics.ts",
|
|
"researcher/schemas/typescript/test/conformance.test.ts",
|
|
"researcher/schemas/typescript/test/runtime-registry.test.ts",
|
|
]
|
|
for relative in supporting_paths:
|
|
path = self.root / relative
|
|
if not path.is_file():
|
|
self.add_finding("PARSE_ERROR", path, "schema contract artifact is missing", relative)
|
|
else:
|
|
self.add_source(path)
|
|
return self._category(
|
|
"researcher/schemas/registry.json",
|
|
sorted(records, key=lambda item: item["id"]),
|
|
canonicalization_profile=registry.get("canonicalization_profile"),
|
|
id_prefix_count=len(registered_prefixes),
|
|
)
|
|
|
|
def build_specifications(self) -> dict[str, Any]:
|
|
"""Build and validate the canonical specification dependency graph."""
|
|
|
|
specs_dir = self.root / "docs" / "specs"
|
|
index_path = specs_dir / "README.md"
|
|
template_path = specs_dir / "SPEC-TEMPLATE.md"
|
|
lifecycle_validator_path = self.root / "researcher" / "scripts" / "validate_spec_lifecycle.py"
|
|
lifecycle_test_path = (
|
|
self.root / "researcher" / "scripts" / "tests" / "test_spec_lifecycle.py"
|
|
)
|
|
for supporting_path in (
|
|
index_path,
|
|
template_path,
|
|
lifecycle_validator_path,
|
|
lifecycle_test_path,
|
|
):
|
|
if supporting_path.is_file():
|
|
self.add_source(supporting_path)
|
|
else:
|
|
self.add_finding(
|
|
"PARSE_ERROR",
|
|
supporting_path,
|
|
"specification program supporting document is missing",
|
|
)
|
|
|
|
try:
|
|
index_text = index_path.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeError):
|
|
index_text = ""
|
|
|
|
roadmap_sections, malformed_roadmap = markdown_level_two_sections(index_text)
|
|
if malformed_roadmap:
|
|
self.add_finding(
|
|
"INVALID_SPEC_INDEX",
|
|
index_path,
|
|
"roadmap contains an unclosed HTML comment or fenced block",
|
|
)
|
|
if contains_raw_html_block(index_text):
|
|
self.add_finding(
|
|
"INVALID_SPEC_INDEX",
|
|
index_path,
|
|
"canonical roadmap cannot contain raw HTML blocks",
|
|
)
|
|
index_sections = roadmap_sections.get("Specification index", [])
|
|
if len(index_sections) != 1:
|
|
self.add_finding(
|
|
"INVALID_SPEC_INDEX",
|
|
index_path,
|
|
f"expected one visible Specification index section, found {len(index_sections)}",
|
|
)
|
|
visible_index_lines: list[str] = []
|
|
else:
|
|
visible_index_lines = visible_unfenced_lines(index_sections[0])
|
|
|
|
index_link_counts: dict[str, int] = {}
|
|
index_link_numbers: dict[str, str] = {}
|
|
for line in visible_index_lines:
|
|
index_match = SPEC_INDEX_ROW_RE.fullmatch(line)
|
|
if index_match is None:
|
|
continue
|
|
target = index_match.group("path")
|
|
index_link_counts[target] = index_link_counts.get(target, 0) + 1
|
|
index_link_numbers[target] = index_match.group("number")
|
|
|
|
records: list[dict[str, Any]] = []
|
|
specs_by_id: dict[str, dict[str, Any]] = {}
|
|
paths_by_id: dict[str, Path] = {}
|
|
lifecycle_specs: dict[str, SpecRevision] = {}
|
|
candidate_paths = sorted(
|
|
path
|
|
for path in specs_dir.rglob("*")
|
|
if path.is_file()
|
|
and path not in {index_path, template_path}
|
|
)
|
|
specification_paths: list[Path] = []
|
|
for path in candidate_paths:
|
|
if path.parent != specs_dir:
|
|
self.add_source(path)
|
|
self.add_finding(
|
|
"INVALID_SPEC_PATH",
|
|
path,
|
|
"canonical specifications must be direct children of docs/specs",
|
|
)
|
|
continue
|
|
specification_paths.append(path)
|
|
for path in specification_paths:
|
|
digest, _ = self.add_source(path)
|
|
filename_match = SPEC_FILENAME_RE.fullmatch(path.name)
|
|
if filename_match is None:
|
|
self.add_finding(
|
|
"INVALID_SPEC_FILENAME",
|
|
path,
|
|
"specification filename must match SPEC-NNN-lowercase-slug.md",
|
|
)
|
|
continue
|
|
filename_number = filename_match.group("number")
|
|
filename_id = f"SPEC-{filename_number}"
|
|
try:
|
|
exact_bytes = path.read_bytes()
|
|
text = exact_bytes.decode("utf-8")
|
|
except (OSError, UnicodeError) as exc:
|
|
self.add_finding("PARSE_ERROR", path, str(exc), filename_id)
|
|
continue
|
|
if b"\r" in exact_bytes:
|
|
self.add_finding(
|
|
"INVALID_SPEC_ENCODING",
|
|
path,
|
|
"canonical specifications must use LF line endings",
|
|
filename_id,
|
|
)
|
|
lines = text.splitlines()
|
|
heading_match = SPEC_HEADING_RE.fullmatch(lines[0] if lines else "")
|
|
if heading_match is None:
|
|
self.add_finding(
|
|
"INVALID_SPEC_HEADING",
|
|
path,
|
|
"first line must be '# SPEC-NNN: Title'",
|
|
filename_id,
|
|
)
|
|
continue
|
|
heading_number = heading_match.group("number")
|
|
spec_id = f"SPEC-{heading_number}"
|
|
if heading_number != filename_number:
|
|
self.add_finding(
|
|
"SPEC_FILENAME_MISMATCH",
|
|
path,
|
|
f"heading {spec_id} does not match filename {filename_id}",
|
|
spec_id,
|
|
)
|
|
|
|
try:
|
|
lifecycle_record = parse_spec_revision(
|
|
self.relative(path),
|
|
exact_bytes,
|
|
allow_legacy=False,
|
|
)
|
|
except ValueError:
|
|
lifecycle_record = None
|
|
if lifecycle_record is not None and lifecycle_record.spec_id not in lifecycle_specs:
|
|
lifecycle_specs[lifecycle_record.spec_id] = lifecycle_record
|
|
|
|
metadata: dict[str, str] = {}
|
|
metadata_order: list[str] = []
|
|
for raw_line in lines[1:]:
|
|
stripped = raw_line.strip()
|
|
if stripped.startswith("## "):
|
|
break
|
|
if not stripped:
|
|
continue
|
|
if ":" not in stripped:
|
|
self.add_finding(
|
|
"INVALID_SPEC_METADATA",
|
|
path,
|
|
f"metadata line has no colon: {stripped!r}",
|
|
spec_id,
|
|
)
|
|
continue
|
|
key, value = (part.strip() for part in stripped.split(":", 1))
|
|
if raw_line != f"{key}: {value}":
|
|
self.add_finding(
|
|
"INVALID_SPEC_METADATA_FORMAT",
|
|
path,
|
|
"metadata must use canonical 'Key: value' serialization without extra whitespace",
|
|
spec_id,
|
|
)
|
|
if key in metadata:
|
|
self.add_finding(
|
|
"INVALID_SPEC_METADATA",
|
|
path,
|
|
f"duplicate metadata key: {key}",
|
|
spec_id,
|
|
)
|
|
else:
|
|
metadata_order.append(key)
|
|
metadata[key] = value
|
|
|
|
order_index = {key: index for index, key in enumerate(SPEC_METADATA_ORDER)}
|
|
known_order = [key for key in metadata_order if key in order_index]
|
|
if known_order != sorted(known_order, key=order_index.__getitem__):
|
|
self.add_finding(
|
|
"INVALID_SPEC_METADATA_ORDER",
|
|
path,
|
|
"specification metadata must use canonical header order",
|
|
spec_id,
|
|
)
|
|
|
|
missing = [key for key in SPEC_REQUIRED_METADATA if not metadata.get(key)]
|
|
unknown = sorted(
|
|
set(metadata) - set(SPEC_REQUIRED_METADATA) - SPEC_OPTIONAL_METADATA
|
|
)
|
|
if missing:
|
|
self.add_finding(
|
|
"INVALID_SPEC_METADATA",
|
|
path,
|
|
f"missing metadata: {', '.join(missing)}",
|
|
spec_id,
|
|
)
|
|
if unknown:
|
|
self.add_finding(
|
|
"INVALID_SPEC_METADATA",
|
|
path,
|
|
f"unknown metadata: {', '.join(unknown)}",
|
|
spec_id,
|
|
)
|
|
if (
|
|
spec_id != AUTHORITY_VOCABULARY_SPEC
|
|
and AUTHORITY_VOCABULARY_METADATA_KEYS.intersection(metadata)
|
|
):
|
|
self.add_finding(
|
|
"INVALID_SPEC_METADATA",
|
|
path,
|
|
"Authority vocabulary metadata is valid only on SPEC-000",
|
|
spec_id,
|
|
)
|
|
|
|
status_value = metadata.get("Status", "")
|
|
if status_value not in SPEC_STATUSES:
|
|
self.add_finding(
|
|
"INVALID_SPEC_STATUS",
|
|
path,
|
|
f"unsupported status: {status_value!r}",
|
|
spec_id,
|
|
)
|
|
try:
|
|
revision = int(metadata.get("Revision", ""))
|
|
except ValueError:
|
|
revision = -1
|
|
if revision < 1:
|
|
self.add_finding(
|
|
"INVALID_SPEC_REVISION",
|
|
path,
|
|
f"Revision must be a positive integer: {metadata.get('Revision')!r}",
|
|
spec_id,
|
|
)
|
|
revises = metadata.get("Revises", "")
|
|
if revision == 1 and revises != "none":
|
|
self.add_finding(
|
|
"INVALID_SPEC_REVISION",
|
|
path,
|
|
"revision 1 must declare Revises: none",
|
|
spec_id,
|
|
)
|
|
elif revision > 1 and SPEC_REVISION_DIGEST_RE.fullmatch(revises) is None:
|
|
self.add_finding(
|
|
"INVALID_SPEC_REVISION",
|
|
path,
|
|
"revision greater than 1 must bind the prior file digest",
|
|
spec_id,
|
|
)
|
|
if spec_id == "SPEC-026" and revision == 1 and metadata.get("Activation") != "deferred":
|
|
self.add_finding(
|
|
"INVALID_SPEC_ACTIVATION",
|
|
path,
|
|
"SPEC-026 revision 1 must remain Activation: deferred",
|
|
spec_id,
|
|
)
|
|
lifecycle_decision = metadata.get("Lifecycle decision")
|
|
adoption_decision = metadata.get("Adoption decision")
|
|
if adoption_decision is not None and (
|
|
SPEC_LIFECYCLE_DECISION_RE.fullmatch(adoption_decision) is None
|
|
):
|
|
self.add_finding(
|
|
"INVALID_SPEC_ADOPTION_DECISION",
|
|
path,
|
|
f"Adoption decision must be ADR-NNNN: {adoption_decision!r}",
|
|
spec_id,
|
|
)
|
|
if (
|
|
lifecycle_decision is not None
|
|
and SPEC_LIFECYCLE_DECISION_RE.fullmatch(lifecycle_decision) is None
|
|
):
|
|
self.add_finding(
|
|
"INVALID_SPEC_LIFECYCLE_DECISION",
|
|
path,
|
|
f"Lifecycle decision must be ADR-NNNN: {lifecycle_decision!r}",
|
|
spec_id,
|
|
)
|
|
if status_value not in SPEC_TERMINAL_STATUSES and lifecycle_decision is not None:
|
|
self.add_finding(
|
|
"INVALID_SPEC_LIFECYCLE_DECISION",
|
|
path,
|
|
"Lifecycle decision is valid only on a terminal specification revision",
|
|
spec_id,
|
|
)
|
|
replacement = metadata.get("Replacement")
|
|
if replacement is not None and replacement != "none" and (
|
|
SPEC_REPLACEMENT_RE.fullmatch(replacement) is None
|
|
):
|
|
self.add_finding(
|
|
"INVALID_SPEC_REPLACEMENT",
|
|
path,
|
|
f"Replacement must be none or SPEC-NNN@revision: {replacement!r}",
|
|
spec_id,
|
|
)
|
|
if status_value in SPEC_TERMINAL_STATUSES and lifecycle_decision is None:
|
|
self.add_finding(
|
|
"MISSING_SPEC_LIFECYCLE_DECISION",
|
|
path,
|
|
f"{status_value} specifications require a Lifecycle decision",
|
|
spec_id,
|
|
)
|
|
if status_value in {"amended", "superseded"} and (
|
|
replacement is None or replacement == "none"
|
|
):
|
|
self.add_finding(
|
|
"MISSING_SPEC_REPLACEMENT",
|
|
path,
|
|
f"{status_value} specifications require a replacement revision",
|
|
spec_id,
|
|
)
|
|
elif status_value in {"amended", "superseded"}:
|
|
expected_replacement = f"{spec_id}@{revision + 1}"
|
|
if replacement != expected_replacement:
|
|
self.add_finding(
|
|
"INVALID_SPEC_REPLACEMENT",
|
|
path,
|
|
f"{status_value} must name {expected_replacement}",
|
|
spec_id,
|
|
)
|
|
if status_value == "retired" and replacement not in {None, "none"}:
|
|
self.add_finding(
|
|
"INVALID_SPEC_REPLACEMENT",
|
|
path,
|
|
"retired specifications cannot name a replacement revision",
|
|
spec_id,
|
|
)
|
|
if status_value not in SPEC_TERMINAL_STATUSES and replacement is not None:
|
|
self.add_finding(
|
|
"INVALID_SPEC_REPLACEMENT",
|
|
path,
|
|
"Replacement is valid only on a terminal specification revision",
|
|
spec_id,
|
|
)
|
|
try:
|
|
wave = int(metadata.get("Wave", ""))
|
|
except ValueError:
|
|
wave = -1
|
|
if wave < 0 or wave > 6:
|
|
self.add_finding(
|
|
"INVALID_SPEC_WAVE",
|
|
path,
|
|
f"wave must be an integer from 0 through 6: {metadata.get('Wave')!r}",
|
|
spec_id,
|
|
)
|
|
classification = metadata.get("Classification", "")
|
|
if classification not in SPEC_CLASSIFICATIONS:
|
|
self.add_finding(
|
|
"INVALID_SPEC_CLASSIFICATION",
|
|
path,
|
|
f"unsupported classification: {classification!r}",
|
|
spec_id,
|
|
)
|
|
owners = [owner.strip() for owner in metadata.get("Owners", "").split(";") if owner.strip()]
|
|
if not owners:
|
|
self.add_finding(
|
|
"INVALID_SPEC_METADATA",
|
|
path,
|
|
"Owners must contain at least one role",
|
|
spec_id,
|
|
)
|
|
depends_value = metadata.get("Depends on", "")
|
|
dependencies = [] if depends_value == "none" else [
|
|
dependency.strip() for dependency in depends_value.split(",") if dependency.strip()
|
|
]
|
|
duplicate_dependencies = sorted(
|
|
dependency for dependency in set(dependencies) if dependencies.count(dependency) > 1
|
|
)
|
|
if duplicate_dependencies:
|
|
self.add_finding(
|
|
"DUPLICATE_SPEC_DEPENDENCY",
|
|
path,
|
|
f"duplicate dependency identifiers: {', '.join(duplicate_dependencies)}",
|
|
spec_id,
|
|
)
|
|
if depends_value != "none" and depends_value != ", ".join(dependencies):
|
|
self.add_finding(
|
|
"INVALID_SPEC_DEPENDENCY",
|
|
path,
|
|
"dependencies must be comma-and-space separated in canonical order",
|
|
spec_id,
|
|
)
|
|
invalid_dependencies = [dependency for dependency in dependencies if not SPEC_ID_RE.fullmatch(dependency)]
|
|
if invalid_dependencies:
|
|
self.add_finding(
|
|
"INVALID_SPEC_DEPENDENCY",
|
|
path,
|
|
f"invalid dependency identifiers: {', '.join(invalid_dependencies)}",
|
|
spec_id,
|
|
)
|
|
|
|
record = {
|
|
"id": spec_id,
|
|
"title": heading_match.group("title"),
|
|
"status": status_value,
|
|
"revision": revision,
|
|
"revises": revises,
|
|
"wave": wave,
|
|
"classification": classification,
|
|
"owners": owners,
|
|
"dependencies": dependencies,
|
|
"path": self.relative(path),
|
|
"digest": digest,
|
|
}
|
|
if "Activation" in metadata:
|
|
record["activation"] = metadata["Activation"]
|
|
if adoption_decision is not None:
|
|
record["adoption_decision"] = adoption_decision
|
|
if "Dependency revisions" in metadata:
|
|
record["dependency_revisions"] = metadata["Dependency revisions"]
|
|
if lifecycle_decision is not None:
|
|
record["lifecycle_decision"] = lifecycle_decision
|
|
if replacement is not None:
|
|
record["replacement"] = replacement
|
|
if spec_id in specs_by_id:
|
|
self.add_finding(
|
|
"DUPLICATE_SPEC_ID",
|
|
path,
|
|
f"identifier already declared by {self.relative(paths_by_id[spec_id])}",
|
|
spec_id,
|
|
)
|
|
else:
|
|
specs_by_id[spec_id] = record
|
|
paths_by_id[spec_id] = path
|
|
records.append(record)
|
|
|
|
authority_binding: AuthorityVocabularyBinding | None = None
|
|
authority_binding_error = False
|
|
canonical_authority_paths = (
|
|
AUTHORITY_VOCABULARY_PATH,
|
|
AUTHORITY_FIXTURE_MANIFEST_PATH,
|
|
AUTHORITY_CONFORMANCE_RECEIPT_PATH,
|
|
)
|
|
present_authority_paths: list[str] = []
|
|
for relative in canonical_authority_paths:
|
|
authority_path = self.root / relative
|
|
if os.path.lexists(authority_path):
|
|
present_authority_paths.append(relative)
|
|
self.add_source(authority_path)
|
|
conformance_path = self.root / AUTHORITY_CONFORMANCE_RECEIPT_PATH
|
|
if os.path.lexists(conformance_path):
|
|
for relative in (
|
|
AUTHORITY_CONSTITUTION_POLICY_PATH,
|
|
AUTHORITY_VALIDATOR_PATH,
|
|
):
|
|
authority_input = self.root / relative
|
|
if os.path.lexists(authority_input):
|
|
self.add_source(authority_input)
|
|
try:
|
|
authority_binding = load_candidate_authority_vocabulary(
|
|
self.root,
|
|
lifecycle_specs,
|
|
)
|
|
except (OSError, UnicodeError, ValueError) as exc:
|
|
authority_binding_error = True
|
|
authority_spec_path = paths_by_id.get(
|
|
AUTHORITY_VOCABULARY_SPEC,
|
|
self.root / AUTHORITY_VOCABULARY_PATH,
|
|
)
|
|
self.add_finding(
|
|
"SPEC_AUTHORITY_VOCABULARY_INVALID",
|
|
authority_spec_path,
|
|
str(exc),
|
|
AUTHORITY_VOCABULARY_SPEC,
|
|
)
|
|
|
|
authority_spec_record = specs_by_id.get(AUTHORITY_VOCABULARY_SPEC)
|
|
authority_spec_revision = lifecycle_specs.get(AUTHORITY_VOCABULARY_SPEC)
|
|
authority_artifacts_declared = (
|
|
authority_spec_revision is not None
|
|
and authority_spec_revision.revision >= AUTHORITY_VOCABULARY_MIN_REVISION
|
|
and AUTHORITY_VOCABULARY_METADATA_KEYS.issubset(
|
|
authority_spec_revision.metadata
|
|
)
|
|
)
|
|
if not authority_artifacts_declared:
|
|
for relative in present_authority_paths:
|
|
self.add_finding(
|
|
"UNEXPECTED_AUTHORITY_ARTIFACT",
|
|
self.root / relative,
|
|
"authority artifacts require the complete canonical metadata binding "
|
|
"on SPEC-000 revision 2 or later",
|
|
AUTHORITY_VOCABULARY_SPEC,
|
|
)
|
|
if (
|
|
AUTHORITY_CONFORMANCE_RECEIPT_PATH in present_authority_paths
|
|
and authority_spec_record is not None
|
|
and authority_spec_record["status"]
|
|
in {"draft", "architecture_reviewed", "accepted"}
|
|
):
|
|
self.add_finding(
|
|
"UNEXPECTED_AUTHORITY_ARTIFACT",
|
|
self.root / AUTHORITY_CONFORMANCE_RECEIPT_PATH,
|
|
"authority-policy conformance is an implementation-stage artifact and "
|
|
"must be absent before SPEC-000 is implemented",
|
|
AUTHORITY_VOCABULARY_SPEC,
|
|
)
|
|
if authority_binding is not None and authority_spec_record is not None:
|
|
authority_record: dict[str, Any] = {
|
|
"path": authority_binding.path,
|
|
"digest": authority_binding.digest,
|
|
"schema_version": authority_binding.schema_version,
|
|
"constitution_revision": authority_binding.constitution_revision,
|
|
"registry_version": authority_binding.registry_version,
|
|
"fixture_manifest": {
|
|
"path": authority_binding.fixture_manifest_path,
|
|
"digest": authority_binding.fixture_manifest_digest,
|
|
"version": authority_binding.fixture_manifest_version,
|
|
},
|
|
}
|
|
conformance = authority_binding.policy_conformance
|
|
if conformance is not None:
|
|
authority_record["policy_conformance"] = {
|
|
"path": conformance.path,
|
|
"digest": conformance.digest,
|
|
"schema_version": conformance.schema_version,
|
|
"constitution_policy_digest": conformance.constitution_policy_digest,
|
|
"registry_digest": conformance.registry_digest,
|
|
"fixture_manifest_digest": conformance.fixture_manifest_digest,
|
|
"validator_digest": conformance.validator_digest,
|
|
"validator_version": conformance.validator_version,
|
|
"case_count": conformance.case_count,
|
|
}
|
|
authority_spec_record["authority_vocabulary"] = authority_record
|
|
|
|
if (
|
|
authority_spec_record is not None
|
|
and authority_spec_record["revision"] >= AUTHORITY_VOCABULARY_MIN_REVISION
|
|
and authority_binding is None
|
|
and not authority_binding_error
|
|
):
|
|
self.add_finding(
|
|
"SPEC_AUTHORITY_VOCABULARY_INVALID",
|
|
paths_by_id[AUTHORITY_VOCABULARY_SPEC],
|
|
"SPEC-000 revision 2 or later requires the exact revision-bound, "
|
|
"digest-pinned AuthorityVocabularyRegistry and fixture manifest",
|
|
AUTHORITY_VOCABULARY_SPEC,
|
|
)
|
|
if (
|
|
authority_spec_record is not None
|
|
and authority_spec_record["revision"] >= AUTHORITY_VOCABULARY_MIN_REVISION
|
|
and authority_spec_record["status"] in AUTHORITY_IMPLEMENTED_STATUSES
|
|
and (
|
|
authority_binding is None
|
|
or authority_binding.policy_conformance is None
|
|
)
|
|
):
|
|
self.add_finding(
|
|
"SPEC_AUTHORITY_POLICY_CONFORMANCE_REQUIRED",
|
|
paths_by_id[AUTHORITY_VOCABULARY_SPEC],
|
|
"implemented, verified, and operational SPEC-000 revisions require "
|
|
"the exact validated authority-policy conformance receipt",
|
|
AUTHORITY_VOCABULARY_SPEC,
|
|
)
|
|
|
|
for spec_id, record in sorted(specs_by_id.items()):
|
|
for dependency in record["dependencies"]:
|
|
target = specs_by_id.get(dependency)
|
|
if target is None:
|
|
self.add_finding(
|
|
"DANGLING_SPEC_DEPENDENCY",
|
|
paths_by_id[spec_id],
|
|
f"dependency does not exist: {dependency}",
|
|
spec_id,
|
|
)
|
|
continue
|
|
if dependency == spec_id:
|
|
self.add_finding(
|
|
"SPEC_DEPENDENCY_CYCLE",
|
|
paths_by_id[spec_id],
|
|
"specification depends on itself",
|
|
spec_id,
|
|
)
|
|
if target["wave"] > record["wave"]:
|
|
self.add_finding(
|
|
"INVALID_SPEC_WAVE",
|
|
paths_by_id[spec_id],
|
|
f"wave {record['wave']} depends on later wave {target['wave']} ({dependency})",
|
|
spec_id,
|
|
)
|
|
|
|
visit_state: dict[str, int] = {}
|
|
cycle_reports: set[tuple[str, ...]] = set()
|
|
|
|
def visit(spec_id: str, stack: tuple[str, ...]) -> None:
|
|
state = visit_state.get(spec_id, 0)
|
|
if state == 2:
|
|
return
|
|
if state == 1:
|
|
start = stack.index(spec_id)
|
|
cycle = stack[start:] + (spec_id,)
|
|
normalized = tuple(sorted(set(cycle)))
|
|
if normalized not in cycle_reports:
|
|
cycle_reports.add(normalized)
|
|
self.add_finding(
|
|
"SPEC_DEPENDENCY_CYCLE",
|
|
paths_by_id[spec_id],
|
|
f"dependency cycle: {' -> '.join(cycle)}",
|
|
spec_id,
|
|
)
|
|
return
|
|
visit_state[spec_id] = 1
|
|
for dependency in specs_by_id[spec_id]["dependencies"]:
|
|
if dependency in specs_by_id:
|
|
visit(dependency, stack + (spec_id,))
|
|
visit_state[spec_id] = 2
|
|
|
|
for spec_id in sorted(specs_by_id):
|
|
visit(spec_id, ())
|
|
|
|
graph_edges: list[tuple[str, str]] = []
|
|
graph_label_counts: dict[str, int] = {}
|
|
dependency_sections = roadmap_sections.get("Dependency graph", [])
|
|
if len(dependency_sections) != 1:
|
|
self.add_finding(
|
|
"INVALID_SPEC_GRAPH",
|
|
index_path,
|
|
f"expected one visible Dependency graph section, found {len(dependency_sections)}",
|
|
)
|
|
graph_lines: list[str] = []
|
|
else:
|
|
graph_blocks, malformed_fence = fenced_blocks(dependency_sections[0], "mermaid")
|
|
if malformed_fence or len(graph_blocks) != 1:
|
|
self.add_finding(
|
|
"INVALID_SPEC_GRAPH",
|
|
index_path,
|
|
f"expected one complete visible mermaid block, found {len(graph_blocks)}",
|
|
)
|
|
graph_lines = []
|
|
else:
|
|
graph_lines = graph_blocks[0]
|
|
nonblank_graph_lines = [
|
|
(line_number, line.strip())
|
|
for line_number, line in enumerate(graph_lines, start=1)
|
|
if line.strip()
|
|
]
|
|
flowchart_lines = [
|
|
line_number
|
|
for line_number, line in nonblank_graph_lines
|
|
if line == "flowchart TD"
|
|
]
|
|
if not nonblank_graph_lines or nonblank_graph_lines[0][1] != "flowchart TD":
|
|
self.add_finding(
|
|
"INVALID_SPEC_GRAPH",
|
|
index_path,
|
|
"mermaid dependency graph must begin with 'flowchart TD'",
|
|
)
|
|
if len(flowchart_lines) != 1:
|
|
self.add_finding(
|
|
"INVALID_SPEC_GRAPH",
|
|
index_path,
|
|
f"mermaid dependency graph must contain one 'flowchart TD' declaration, found {len(flowchart_lines)}",
|
|
)
|
|
for line_number, line in enumerate(graph_lines, start=1):
|
|
stripped = line.strip()
|
|
if not stripped or stripped == "flowchart TD":
|
|
continue
|
|
if "-->" not in line:
|
|
self.add_finding(
|
|
"INVALID_SPEC_GRAPH_LINE",
|
|
index_path,
|
|
f"line {line_number} is not a dependency edge",
|
|
)
|
|
continue
|
|
graph_match = SPEC_GRAPH_EDGE_RE.fullmatch(line)
|
|
if graph_match is None:
|
|
self.add_finding(
|
|
"INVALID_SPEC_GRAPH_LINE",
|
|
index_path,
|
|
f"line {line_number} is not a canonical SNNN --> SNNN edge",
|
|
)
|
|
continue
|
|
graph_edges.append(
|
|
(
|
|
f"SPEC-{graph_match.group('source')}",
|
|
f"SPEC-{graph_match.group('target')}",
|
|
)
|
|
)
|
|
for endpoint, label in (
|
|
(f"SPEC-{graph_match.group('source')}", graph_match.group("source_label")),
|
|
(f"SPEC-{graph_match.group('target')}", graph_match.group("target_label")),
|
|
):
|
|
endpoint_record = specs_by_id.get(endpoint)
|
|
if label is None or endpoint_record is None:
|
|
continue
|
|
graph_label_counts[endpoint] = graph_label_counts.get(endpoint, 0) + 1
|
|
expected_label = f"{endpoint} {endpoint_record['title']}"
|
|
if label != expected_label:
|
|
self.add_finding(
|
|
"SPEC_GRAPH_LABEL_MISMATCH",
|
|
index_path,
|
|
f"graph label for {endpoint} must be {expected_label!r}",
|
|
endpoint,
|
|
)
|
|
|
|
for spec_id in sorted(specs_by_id):
|
|
label_count = graph_label_counts.get(spec_id, 0)
|
|
if label_count != 1:
|
|
self.add_finding(
|
|
"SPEC_GRAPH_LABEL_COUNT",
|
|
index_path,
|
|
f"{spec_id} must have exactly one canonical graph label, found {label_count}",
|
|
spec_id,
|
|
)
|
|
|
|
graph_edge_counts: dict[tuple[str, str], int] = {}
|
|
for edge in graph_edges:
|
|
graph_edge_counts[edge] = graph_edge_counts.get(edge, 0) + 1
|
|
for edge, count in sorted(graph_edge_counts.items()):
|
|
if count > 1:
|
|
self.add_finding(
|
|
"DUPLICATE_SPEC_GRAPH_EDGE",
|
|
index_path,
|
|
f"dependency edge {edge[0]} -> {edge[1]} appears {count} times",
|
|
)
|
|
|
|
expected_graph_edges = {
|
|
(dependency, spec_id)
|
|
for spec_id, record in specs_by_id.items()
|
|
for dependency in record["dependencies"]
|
|
if dependency in specs_by_id
|
|
}
|
|
actual_graph_edges = set(graph_edges)
|
|
for source, target in sorted(expected_graph_edges - actual_graph_edges):
|
|
self.add_finding(
|
|
"MISSING_SPEC_GRAPH_EDGE",
|
|
index_path,
|
|
f"dependency metadata requires {source} -> {target}",
|
|
)
|
|
for source, target in sorted(actual_graph_edges - expected_graph_edges):
|
|
self.add_finding(
|
|
"EXTRA_SPEC_GRAPH_EDGE",
|
|
index_path,
|
|
f"diagram edge is not declared in metadata: {source} -> {target}",
|
|
)
|
|
|
|
known_filenames = {path.name for path in specification_paths}
|
|
for filename in sorted(known_filenames):
|
|
count = index_link_counts.get(filename, 0)
|
|
path = specs_dir / filename
|
|
if count == 0:
|
|
self.add_finding(
|
|
"MISSING_SPEC_INDEX_LINK",
|
|
index_path,
|
|
f"specification is not linked: {filename}",
|
|
)
|
|
elif count > 1:
|
|
self.add_finding(
|
|
"DUPLICATE_SPEC_INDEX_LINK",
|
|
index_path,
|
|
f"specification is linked {count} times: {filename}",
|
|
)
|
|
filename_match = SPEC_FILENAME_RE.fullmatch(filename)
|
|
if filename_match and index_link_numbers.get(filename) not in {None, filename_match.group("number")}:
|
|
self.add_finding(
|
|
"SPEC_INDEX_MISMATCH",
|
|
index_path,
|
|
f"link label does not match target: {filename}",
|
|
)
|
|
for filename in sorted(set(index_link_counts) - known_filenames):
|
|
self.add_finding(
|
|
"UNKNOWN_SPEC_INDEX_LINK",
|
|
index_path,
|
|
f"index links an unknown specification: {filename}",
|
|
)
|
|
|
|
for record in records:
|
|
if record["status"] not in {
|
|
"architecture_reviewed",
|
|
"accepted",
|
|
"implemented",
|
|
"verified",
|
|
"operational",
|
|
}:
|
|
continue
|
|
raw_bindings = record.get("dependency_revisions")
|
|
parsed_bindings: dict[str, int] = {}
|
|
if raw_bindings == "none":
|
|
binding_parts: list[str] = []
|
|
elif isinstance(raw_bindings, str):
|
|
binding_parts = [part.strip() for part in raw_bindings.split(",")]
|
|
if raw_bindings != ", ".join(binding_parts):
|
|
self.add_finding(
|
|
"INVALID_SPEC_DEPENDENCY_REVISIONS",
|
|
self.root / record["path"],
|
|
"dependency revision bindings must be comma-and-space separated",
|
|
record["id"],
|
|
)
|
|
else:
|
|
binding_parts = []
|
|
self.add_finding(
|
|
"MISSING_SPEC_DEPENDENCY_REVISIONS",
|
|
self.root / record["path"],
|
|
"architecture-reviewed and later specifications must bind exact dependency revisions",
|
|
record["id"],
|
|
)
|
|
for part in binding_parts:
|
|
match = SPEC_DEPENDENCY_REVISION_RE.fullmatch(part)
|
|
if match is None:
|
|
self.add_finding(
|
|
"INVALID_SPEC_DEPENDENCY_REVISIONS",
|
|
self.root / record["path"],
|
|
f"invalid dependency revision binding: {part!r}",
|
|
record["id"],
|
|
)
|
|
continue
|
|
dependency_id = match.group(1)
|
|
if dependency_id in parsed_bindings:
|
|
self.add_finding(
|
|
"INVALID_SPEC_DEPENDENCY_REVISIONS",
|
|
self.root / record["path"],
|
|
f"duplicate dependency revision binding: {dependency_id}",
|
|
record["id"],
|
|
)
|
|
continue
|
|
parsed_bindings[dependency_id] = int(match.group(2))
|
|
expected_ids = set(record["dependencies"])
|
|
if set(parsed_bindings) != expected_ids:
|
|
self.add_finding(
|
|
"SPEC_DEPENDENCY_REVISION_MISMATCH",
|
|
self.root / record["path"],
|
|
"dependency revision bindings must cover the direct dependency set exactly",
|
|
record["id"],
|
|
)
|
|
if int(record["id"][-3:]) >= 4:
|
|
authority_spec = specs_by_id.get(AUTHORITY_VOCABULARY_SPEC)
|
|
if (
|
|
authority_spec is None
|
|
or authority_spec["revision"] < AUTHORITY_VOCABULARY_MIN_REVISION
|
|
or authority_spec["status"]
|
|
not in AUTHORITY_VOCABULARY_READY_STATUSES
|
|
or authority_binding is None
|
|
or authority_binding.constitution_revision
|
|
!= authority_spec["revision"]
|
|
):
|
|
self.add_finding(
|
|
"SPEC_AUTHORITY_VOCABULARY_NOT_READY",
|
|
self.root / record["path"],
|
|
"SPEC-004 and later cannot be architecture-reviewed or active "
|
|
"until accepted SPEC-000 revision 2 or later is current with "
|
|
"its exact AuthorityVocabularyRegistry and fixture manifest",
|
|
record["id"],
|
|
)
|
|
if record["status"] in AUTHORITY_IMPLEMENTED_STATUSES and (
|
|
authority_binding is None
|
|
or authority_binding.policy_conformance is None
|
|
):
|
|
self.add_finding(
|
|
"SPEC_AUTHORITY_POLICY_CONFORMANCE_REQUIRED",
|
|
self.root / record["path"],
|
|
"implemented, verified, and operational SPEC-004 and later "
|
|
"require the exact validated authority-policy conformance receipt",
|
|
record["id"],
|
|
)
|
|
if (
|
|
AUTHORITY_VOCABULARY_SPEC in expected_ids
|
|
and parsed_bindings.get(AUTHORITY_VOCABULARY_SPEC, 0)
|
|
< AUTHORITY_VOCABULARY_MIN_REVISION
|
|
):
|
|
self.add_finding(
|
|
"SPEC_AUTHORITY_VOCABULARY_NOT_READY",
|
|
self.root / record["path"],
|
|
"the frozen SPEC-000 dependency must be revision 2 or later",
|
|
record["id"],
|
|
)
|
|
return self._category(
|
|
"docs/specs/",
|
|
sorted(records, key=lambda item: (item["id"], item["path"])),
|
|
lifecycle=sorted(SPEC_STATUSES),
|
|
)
|
|
|
|
def build_architecture_decisions(self, specification_ids: set[str]) -> dict[str, Any]:
|
|
"""Source-bind durable ADRs and validate their canonical index."""
|
|
|
|
decisions_dir = self.root / "docs" / "decisions"
|
|
index_path = decisions_dir / "README.md"
|
|
if index_path.is_file():
|
|
self.add_source(index_path)
|
|
try:
|
|
index_text = index_path.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeError) as exc:
|
|
self.add_finding("PARSE_ERROR", index_path, str(exc))
|
|
index_text = ""
|
|
else:
|
|
self.add_finding("PARSE_ERROR", index_path, "architecture decision index is missing")
|
|
index_text = ""
|
|
|
|
decision_sections, malformed_index = markdown_level_two_sections(index_text)
|
|
if malformed_index:
|
|
self.add_finding(
|
|
"INVALID_ADR_INDEX",
|
|
index_path,
|
|
"decision index contains an unclosed HTML comment or fenced block",
|
|
)
|
|
if contains_raw_html_block(index_text):
|
|
self.add_finding(
|
|
"INVALID_ADR_INDEX",
|
|
index_path,
|
|
"canonical decision index cannot contain raw HTML blocks",
|
|
)
|
|
records_sections = decision_sections.get("Records", [])
|
|
if len(records_sections) != 1:
|
|
self.add_finding(
|
|
"INVALID_ADR_INDEX",
|
|
index_path,
|
|
f"expected one visible Records section, found {len(records_sections)}",
|
|
)
|
|
visible_record_lines: list[str] = []
|
|
else:
|
|
visible_record_lines = visible_unfenced_lines(records_sections[0])
|
|
|
|
index_links: dict[str, list[tuple[str, str]]] = {}
|
|
for line in visible_record_lines:
|
|
match = ADR_INDEX_ROW_RE.fullmatch(line)
|
|
if match is None:
|
|
continue
|
|
index_links.setdefault(match.group("path"), []).append(
|
|
(match.group("number"), match.group("title"))
|
|
)
|
|
|
|
records: list[dict[str, Any]] = []
|
|
decisions_by_id: dict[str, dict[str, Any]] = {}
|
|
paths_by_id: dict[str, Path] = {}
|
|
direct_paths: list[Path] = []
|
|
candidate_paths = sorted(
|
|
path
|
|
for path in decisions_dir.rglob("*")
|
|
if path.is_file()
|
|
and path != index_path
|
|
)
|
|
for path in candidate_paths:
|
|
digest, _ = self.add_source(path)
|
|
if path.parent != decisions_dir:
|
|
self.add_finding(
|
|
"INVALID_ADR_PATH",
|
|
path,
|
|
"architecture decisions must be direct children of docs/decisions",
|
|
)
|
|
continue
|
|
direct_paths.append(path)
|
|
filename_match = ADR_FILENAME_RE.fullmatch(path.name)
|
|
if filename_match is None:
|
|
self.add_finding(
|
|
"INVALID_ADR_FILENAME",
|
|
path,
|
|
"ADR filename must match NNNN-lowercase-slug.md",
|
|
)
|
|
continue
|
|
filename_number = filename_match.group("number")
|
|
filename_id = f"ADR-{filename_number}"
|
|
try:
|
|
text = path.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeError) as exc:
|
|
self.add_finding("PARSE_ERROR", path, str(exc), filename_id)
|
|
continue
|
|
lines = text.splitlines()
|
|
heading_match = ADR_HEADING_RE.fullmatch(lines[0] if lines else "")
|
|
if heading_match is None:
|
|
self.add_finding(
|
|
"INVALID_ADR_HEADING",
|
|
path,
|
|
"first line must be '# ADR-NNNN: Title'",
|
|
filename_id,
|
|
)
|
|
continue
|
|
adr_id = f"ADR-{heading_match.group('number')}"
|
|
if adr_id != filename_id:
|
|
self.add_finding(
|
|
"ADR_FILENAME_MISMATCH",
|
|
path,
|
|
f"heading {adr_id} does not match filename {filename_id}",
|
|
adr_id,
|
|
)
|
|
|
|
metadata: dict[str, str] = {}
|
|
for line in lines[1:]:
|
|
stripped = line.strip()
|
|
if stripped.startswith("## "):
|
|
break
|
|
if not stripped:
|
|
continue
|
|
metadata_match = re.fullmatch(r"- (?P<key>[^:]+):(?P<value>.*)", stripped)
|
|
if metadata_match is None:
|
|
self.add_finding(
|
|
"INVALID_ADR_METADATA",
|
|
path,
|
|
f"metadata line must be '- Key: value': {stripped!r}",
|
|
adr_id,
|
|
)
|
|
continue
|
|
key = metadata_match.group("key").strip()
|
|
value = metadata_match.group("value").strip()
|
|
if key not in ADR_ALLOWED_METADATA:
|
|
self.add_finding(
|
|
"INVALID_ADR_METADATA",
|
|
path,
|
|
f"unknown metadata key: {key}",
|
|
adr_id,
|
|
)
|
|
continue
|
|
if key in metadata:
|
|
self.add_finding(
|
|
"INVALID_ADR_METADATA",
|
|
path,
|
|
f"duplicate metadata key: {key}",
|
|
adr_id,
|
|
)
|
|
continue
|
|
metadata[key] = value
|
|
|
|
status = metadata.get("Status", "")
|
|
date_value = metadata.get("Date", "")
|
|
supersedes = metadata.get("Supersedes")
|
|
lifecycle_transition = metadata.get("Lifecycle transition")
|
|
if status not in ADR_STATUSES:
|
|
self.add_finding(
|
|
"INVALID_ADR_STATUS",
|
|
path,
|
|
f"unsupported status: {status!r}",
|
|
adr_id,
|
|
)
|
|
try:
|
|
parsed_date = date.fromisoformat(date_value)
|
|
valid_date = parsed_date.isoformat() == date_value
|
|
except ValueError:
|
|
valid_date = False
|
|
if not valid_date:
|
|
self.add_finding(
|
|
"INVALID_ADR_DATE",
|
|
path,
|
|
f"date must be a real ISO calendar date: {date_value!r}",
|
|
adr_id,
|
|
)
|
|
if supersedes is not None and (
|
|
ADR_ID_RE.fullmatch(supersedes) is None or supersedes == adr_id
|
|
):
|
|
self.add_finding(
|
|
"INVALID_ADR_SUPERSESSION",
|
|
path,
|
|
f"Supersedes must name a different ADR-NNNN: {supersedes!r}",
|
|
adr_id,
|
|
)
|
|
elif supersedes is not None and int(supersedes[-4:]) >= int(adr_id[-4:]):
|
|
self.add_finding(
|
|
"INVALID_ADR_SUPERSESSION",
|
|
path,
|
|
"Supersedes must point backward to a lower append-only ADR number",
|
|
adr_id,
|
|
)
|
|
if lifecycle_transition is not None:
|
|
transition_match = ADR_LIFECYCLE_TRANSITION_RE.fullmatch(
|
|
lifecycle_transition
|
|
)
|
|
if transition_match is None:
|
|
self.add_finding(
|
|
"INVALID_ADR_LIFECYCLE_TRANSITION",
|
|
path,
|
|
"Lifecycle transition must be 'SPEC-NNN@revision -> "
|
|
"amended|superseded|retired -> SPEC-NNN@next|none'",
|
|
adr_id,
|
|
)
|
|
else:
|
|
transition_spec = transition_match.group("spec")
|
|
transition_revision = int(transition_match.group("revision"))
|
|
transition_status = transition_match.group("status")
|
|
transition_replacement = transition_match.group("replacement")
|
|
expected_replacement = f"{transition_spec}@{transition_revision + 1}"
|
|
if (
|
|
transition_status in {"amended", "superseded"}
|
|
and transition_replacement != expected_replacement
|
|
) or (
|
|
transition_status == "retired"
|
|
and transition_replacement != "none"
|
|
):
|
|
self.add_finding(
|
|
"INVALID_ADR_LIFECYCLE_TRANSITION",
|
|
path,
|
|
"lifecycle transition replacement must be the next revision "
|
|
"of the same specification, or none for retirement",
|
|
adr_id,
|
|
)
|
|
|
|
scope_keys = [key for key in ("Spec", "Specs") if metadata.get(key)]
|
|
if len(scope_keys) != 1:
|
|
self.add_finding(
|
|
"INVALID_ADR_SPEC_SCOPE",
|
|
path,
|
|
"exactly one non-empty Spec or Specs field is required",
|
|
adr_id,
|
|
)
|
|
spec_scope = ""
|
|
referenced_specs: list[str] = []
|
|
else:
|
|
scope_key = scope_keys[0]
|
|
spec_scope = metadata[scope_key]
|
|
referenced_specs = []
|
|
if scope_key == "Spec":
|
|
if SPEC_ID_RE.fullmatch(spec_scope) is None:
|
|
self.add_finding(
|
|
"INVALID_ADR_SPEC_SCOPE",
|
|
path,
|
|
f"Spec must be one exact identifier: {spec_scope!r}",
|
|
adr_id,
|
|
)
|
|
else:
|
|
referenced_specs = [spec_scope]
|
|
else:
|
|
range_match = re.fullmatch(
|
|
r"SPEC-(?P<start>[0-9]{3}) through SPEC-(?P<end>[0-9]{3})",
|
|
spec_scope,
|
|
)
|
|
if range_match is not None:
|
|
start = int(range_match.group("start"))
|
|
end = int(range_match.group("end"))
|
|
if start > end:
|
|
self.add_finding(
|
|
"INVALID_ADR_SPEC_SCOPE",
|
|
path,
|
|
f"specification range is reversed: {spec_scope!r}",
|
|
adr_id,
|
|
)
|
|
else:
|
|
referenced_specs = [
|
|
f"SPEC-{number:03d}" for number in range(start, end + 1)
|
|
]
|
|
else:
|
|
candidates = [value.strip() for value in spec_scope.split(",")]
|
|
invalid_candidates = [
|
|
value for value in candidates if SPEC_ID_RE.fullmatch(value) is None
|
|
]
|
|
duplicate_candidates = sorted(
|
|
value
|
|
for value in set(candidates)
|
|
if value and candidates.count(value) > 1
|
|
)
|
|
if invalid_candidates or not candidates:
|
|
self.add_finding(
|
|
"INVALID_ADR_SPEC_SCOPE",
|
|
path,
|
|
f"Specs must be a canonical range or comma-separated identifiers: {spec_scope!r}",
|
|
adr_id,
|
|
)
|
|
if duplicate_candidates:
|
|
self.add_finding(
|
|
"INVALID_ADR_SPEC_SCOPE",
|
|
path,
|
|
f"duplicate specification identifiers: {', '.join(duplicate_candidates)}",
|
|
adr_id,
|
|
)
|
|
referenced_specs = sorted(
|
|
{value for value in candidates if SPEC_ID_RE.fullmatch(value)}
|
|
)
|
|
|
|
for spec_id in referenced_specs:
|
|
if spec_id not in specification_ids:
|
|
self.add_finding(
|
|
"DANGLING_ADR_SPEC",
|
|
path,
|
|
f"decision references unknown specification: {spec_id}",
|
|
adr_id,
|
|
)
|
|
|
|
if lifecycle_transition is not None:
|
|
transition_match = ADR_LIFECYCLE_TRANSITION_RE.fullmatch(
|
|
lifecycle_transition
|
|
)
|
|
if transition_match is not None:
|
|
transition_spec = transition_match.group("spec")
|
|
if scope_keys != ["Spec"] or referenced_specs != [transition_spec]:
|
|
self.add_finding(
|
|
"INVALID_ADR_LIFECYCLE_TRANSITION_SCOPE",
|
|
path,
|
|
"a lifecycle-transition ADR is one-purpose: Spec must "
|
|
f"name exactly {transition_spec}",
|
|
adr_id,
|
|
)
|
|
|
|
record = {
|
|
"id": adr_id,
|
|
"title": heading_match.group("title"),
|
|
"status": status,
|
|
"date": date_value,
|
|
"spec_scope": spec_scope,
|
|
"specifications": referenced_specs,
|
|
"path": self.relative(path),
|
|
"digest": digest,
|
|
}
|
|
if supersedes is not None:
|
|
record["supersedes"] = supersedes
|
|
if lifecycle_transition is not None:
|
|
record["lifecycle_transition"] = lifecycle_transition
|
|
if adr_id in decisions_by_id:
|
|
self.add_finding(
|
|
"DUPLICATE_ADR_ID",
|
|
path,
|
|
f"identifier already declared by {self.relative(paths_by_id[adr_id])}",
|
|
adr_id,
|
|
)
|
|
else:
|
|
decisions_by_id[adr_id] = record
|
|
paths_by_id[adr_id] = path
|
|
records.append(record)
|
|
|
|
for record in records:
|
|
supersedes = record.get("supersedes")
|
|
if supersedes is None or ADR_ID_RE.fullmatch(supersedes) is None:
|
|
continue
|
|
target = decisions_by_id.get(supersedes)
|
|
if target is None:
|
|
self.add_finding(
|
|
"DANGLING_ADR_SUPERSESSION",
|
|
self.root / record["path"],
|
|
f"Supersedes references unknown decision: {supersedes}",
|
|
record["id"],
|
|
)
|
|
elif target["status"] != "accepted":
|
|
self.add_finding(
|
|
"INVALID_ADR_SUPERSESSION",
|
|
self.root / record["path"],
|
|
f"Supersedes target must be accepted: {supersedes}",
|
|
record["id"],
|
|
)
|
|
|
|
accepted_successors: dict[str, list[str]] = {}
|
|
for record in records:
|
|
supersedes = record.get("supersedes")
|
|
if record["status"] == "accepted" and isinstance(supersedes, str):
|
|
accepted_successors.setdefault(supersedes, []).append(record["id"])
|
|
for target_id, successors in sorted(accepted_successors.items()):
|
|
if len(successors) > 1:
|
|
for successor_id in successors:
|
|
successor = decisions_by_id[successor_id]
|
|
self.add_finding(
|
|
"AMBIGUOUS_ADR_SUPERSESSION",
|
|
self.root / successor["path"],
|
|
f"{target_id} has multiple accepted direct successors: "
|
|
f"{', '.join(sorted(successors))}",
|
|
successor_id,
|
|
)
|
|
|
|
known_filenames = {path.name for path in direct_paths}
|
|
for filename in sorted(known_filenames):
|
|
links = index_links.get(filename, [])
|
|
if not links:
|
|
self.add_finding(
|
|
"MISSING_ADR_INDEX_LINK",
|
|
index_path,
|
|
f"architecture decision is not linked: {filename}",
|
|
)
|
|
continue
|
|
if len(links) > 1:
|
|
self.add_finding(
|
|
"DUPLICATE_ADR_INDEX_LINK",
|
|
index_path,
|
|
f"architecture decision is linked {len(links)} times: {filename}",
|
|
)
|
|
filename_match = ADR_FILENAME_RE.fullmatch(filename)
|
|
if filename_match is None:
|
|
continue
|
|
adr_id = f"ADR-{filename_match.group('number')}"
|
|
record = decisions_by_id.get(adr_id)
|
|
if record is None:
|
|
continue
|
|
expected_link = (filename_match.group("number"), record["title"])
|
|
if any(link != expected_link for link in links):
|
|
self.add_finding(
|
|
"ADR_INDEX_MISMATCH",
|
|
index_path,
|
|
f"index identity or title does not match {filename}",
|
|
adr_id,
|
|
)
|
|
for filename in sorted(set(index_links) - known_filenames):
|
|
self.add_finding(
|
|
"UNKNOWN_ADR_INDEX_LINK",
|
|
index_path,
|
|
f"index links an unknown architecture decision: {filename}",
|
|
)
|
|
|
|
return self._category(
|
|
"docs/decisions/",
|
|
sorted(records, key=lambda item: (item["id"], item["path"])),
|
|
lifecycle=sorted(ADR_STATUSES),
|
|
)
|
|
|
|
def validate_live_document_links(self) -> None:
|
|
for relative, required_link in LIVE_DOCUMENT_LINKS.items():
|
|
path = self.root / relative
|
|
try:
|
|
text = path.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeError) as exc:
|
|
self.add_finding("PARSE_ERROR", path, str(exc))
|
|
continue
|
|
if required_link not in text:
|
|
self.add_finding(
|
|
"STALE_COUNT",
|
|
path,
|
|
f"live document must link to generated inventory: {required_link}",
|
|
)
|
|
|
|
|
|
def render_summary(inventory: dict[str, Any]) -> str:
|
|
artifacts = inventory["artifacts"]
|
|
rows = [
|
|
("Specifications", artifacts["specifications"]["count"]),
|
|
("Architecture decisions", artifacts["architecture_decisions"]["count"]),
|
|
("Published skills", artifacts["skills"]["count"]),
|
|
("Mechanism registry records", artifacts["mechanisms"]["count"]),
|
|
("Accepted-ledger events", artifacts["mechanism_ledgers"]["accepted_event_count"]),
|
|
("Rejected-ledger events", artifacts["mechanism_ledgers"]["rejected_event_count"]),
|
|
("Provenance-tracked claims", artifacts["claims"]["count"]),
|
|
("Activation cases", artifacts["activation_cases"]["count"]),
|
|
("Router prompts", artifacts["router_prompts"]["count"]),
|
|
("Adversarial scenarios", artifacts["adversarial_scenarios"]["count"]),
|
|
("Adversarial goldens", artifacts["adversarial_goldens"]["count"]),
|
|
("Effectiveness tasks", artifacts["effectiveness_tasks"]["count"]),
|
|
("Example projects", artifacts["examples"]["count"]),
|
|
]
|
|
lines = [
|
|
"<!-- GENERATED by researcher/scripts/build_inventory.py. DO NOT EDIT. -->",
|
|
"# Live corpus inventory",
|
|
"",
|
|
"This is a generated view of canonical repository artifacts, not a second source of truth.",
|
|
"",
|
|
f"- Schema: `{inventory['schema_version']}`",
|
|
f"- Source tree: `{inventory['source_tree_digest']}`",
|
|
f"- Unresolved references: `{len(inventory['unresolved_references'])}`",
|
|
"",
|
|
"| Artifact | Count |",
|
|
"| --- | ---: |",
|
|
*[f"| {label} | {count} |" for label, count in rows],
|
|
"",
|
|
"## Compatibility",
|
|
"",
|
|
f"- Plugin version: `{artifacts['manifests']['versions']['open_plugin']}`",
|
|
f"- Router runner: `{inventory['compatibility']['benchmark_runner']['router']}`",
|
|
f"- Effectiveness runner: `{inventory['compatibility']['benchmark_runner']['effectiveness']}`",
|
|
"",
|
|
(
|
|
"Historical reports retain the counts and measurements from their dated snapshot. "
|
|
"Current documents should link here instead of copying live totals."
|
|
),
|
|
"",
|
|
]
|
|
return "\n".join(lines)
|
|
|
|
|
|
def print_findings(findings: Iterable[Finding]) -> None:
|
|
for finding in findings:
|
|
identity = f" ({finding.artifact_id})" if finding.artifact_id else ""
|
|
print(f"[{finding.code}] {finding.path}{identity}: {finding.message}", file=sys.stderr)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
modes = parser.add_mutually_exclusive_group()
|
|
modes.add_argument("--check", action="store_true", help="fail on unresolved references or generated drift")
|
|
modes.add_argument("--write", action="store_true", help="atomically refresh generated inventory and summary")
|
|
modes.add_argument("--json", action="store_true", help="print the generated inventory without writing")
|
|
parser.add_argument("--root", type=Path, default=ROOT)
|
|
parser.add_argument("--inventory", type=Path)
|
|
parser.add_argument("--summary", type=Path)
|
|
args = parser.parse_args()
|
|
|
|
root = args.root.resolve()
|
|
inventory_path = args.inventory or root / "researcher" / "corpus" / "inventory.json"
|
|
summary_path = args.summary or root / "researcher" / "generated" / "corpus-summary.md"
|
|
builder = InventoryBuilder(root)
|
|
try:
|
|
inventory = builder.build()
|
|
except (InventoryError, OSError) as exc:
|
|
print(f"[PARSE_ERROR] {exc}", file=sys.stderr)
|
|
return 1
|
|
|
|
if args.json:
|
|
print(pretty_json(inventory), end="")
|
|
print_findings(builder.findings)
|
|
return 1 if builder.findings else 0
|
|
|
|
if builder.findings:
|
|
print_findings(builder.findings)
|
|
print(f"Inventory generation failed: {len(builder.findings)} unresolved finding(s)", file=sys.stderr)
|
|
return 1
|
|
|
|
inventory_text = pretty_json(inventory)
|
|
summary_text = render_summary(inventory)
|
|
if args.write:
|
|
atomic_write_text(inventory_path, inventory_text)
|
|
atomic_write_text(summary_path, summary_text)
|
|
print(
|
|
f"Inventory written: {artifacts_count(inventory)} artifact records, "
|
|
f"digest {inventory['source_tree_digest'][7:19]}"
|
|
)
|
|
return 0
|
|
|
|
drift: list[Finding] = []
|
|
if not inventory_path.exists() or inventory_path.read_text(encoding="utf-8") != inventory_text:
|
|
drift.append(
|
|
Finding("GENERATED_DRIFT", str(inventory_path.relative_to(root)), None, "run build_inventory.py --write")
|
|
)
|
|
if not summary_path.exists() or summary_path.read_text(encoding="utf-8") != summary_text:
|
|
drift.append(
|
|
Finding("STALE_COUNT", str(summary_path.relative_to(root)), None, "generated summary is stale")
|
|
)
|
|
if drift:
|
|
print_findings(drift)
|
|
return 1
|
|
print(
|
|
f"Inventory check passed: {artifacts_count(inventory)} artifact records, "
|
|
f"{len(inventory['sources'])} canonical sources, digest {inventory['source_tree_digest'][7:19]}"
|
|
)
|
|
return 0
|
|
|
|
|
|
def artifacts_count(inventory: dict[str, Any]) -> int:
|
|
return sum(
|
|
category.get("count", 0)
|
|
for category in inventory["artifacts"].values()
|
|
if isinstance(category, dict)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|