Files
pulse/scripts/release_control/subsystem_contracts.py
T
rcourtman 5f5f7ac7fc Read the guard's registry and contracts from the evaluated commit
The completion guard's CI mode now compares contract text against the
evaluated commit (#2502), but it still decided which contracts a commit
owed from HEAD's registry and contract index. CI walks a branch with HEAD
at the tip (or at main after a merge), so a later commit could change an
earlier commit's verdict. If a later commit added a Shared Boundaries
reference to a file, an earlier commit that was complete when written got
blocked. If a later commit dropped ownership of a file, an earlier commit
that changed it without its contract passed. The pre-commit hook read
both from the commit's own index, so CI and the hook could still
disagree.

repo_file_io and subsystem_contracts can now read from a revision as well
as the working tree or index. A revision read uses only that commit's
tree and fails closed rather than falling back to the disk. In diff-base
mode the guard reads the registry and contract index from the evaluated
commit, as does the Prettier-only inference that --commit enables.
Replaying CI's per-commit loop over the last 120 commits on main gives
the same verdicts as before.
2026-10-05 10:04:06 +01:00

273 lines
9.5 KiB
Python

#!/usr/bin/env python3
"""Shared parsing helpers for active release profile subsystem contracts."""
from __future__ import annotations
import json
import os
from pathlib import Path
import re
import subprocess
from typing import Any
from control_plane import DEFAULT_CONTROL_PLANE
REPO_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_REPO_ROOT = REPO_ROOT
CONTRACTS_DIR = DEFAULT_CONTROL_PLANE["subsystems_dir_path"]
TEMPLATE_REL = DEFAULT_CONTROL_PLANE["subsystem_contract_template_rel"]
REQUIRED_SECTIONS = [
"## Contract Metadata",
"## Purpose",
"## Canonical Files",
"## Shared Boundaries",
"## Extension Points",
"## Forbidden Paths",
"## Completion Obligations",
"## Current State",
]
LIST_SECTIONS = {
"## Canonical Files",
"## Shared Boundaries",
"## Extension Points",
"## Forbidden Paths",
"## Completion Obligations",
}
PATH_SUFFIXES = (
".go",
".json",
".md",
".mod",
".mjs",
".py",
".sh",
".sum",
".ts",
".tsx",
".yaml",
".yml",
)
def git_env() -> dict[str, str]:
env = os.environ.copy()
if REPO_ROOT != DEFAULT_REPO_ROOT:
# Unit tests patch REPO_ROOT to a temporary repository. Scrub the full
# inherited hook environment: a pre-commit run from a linked worktree
# exports an absolute GIT_DIR, which would silently point git at the
# real repository instead of the temp one.
for name in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR"):
env.pop(name, None)
return env
def git(*args: str, text: bool) -> subprocess.CompletedProcess:
return subprocess.run(
["git", *args],
cwd=REPO_ROOT,
check=True,
capture_output=True,
text=text,
env=git_env(),
)
def tracked_contract_paths(*, staged: bool = False, revision: str | None = None) -> list[str]:
if staged or revision is not None:
contracts_rel = CONTRACTS_DIR.relative_to(REPO_ROOT).as_posix()
if revision is not None:
result = git("ls-tree", "-r", "-z", "--name-only", revision, "--", contracts_rel, text=False)
else:
result = git("ls-files", "-z", "--", contracts_rel, text=False)
git_paths = sorted(
entry.decode("utf-8")
for entry in result.stdout.split(b"\x00")
if entry and entry.decode("utf-8").endswith(".md")
)
if git_paths or revision is not None:
# A commit's contracts are what its tree holds, never the disk's.
return git_paths
# Governance contract files are filesystem-only (gitignored) — fall back to disk
return sorted(
path.relative_to(REPO_ROOT).as_posix()
for path in CONTRACTS_DIR.glob("*.md")
)
def git_contract_text(rel: str, revision: str | None = None) -> str:
"""Read a contract from `revision`, or from the index when it is None."""
result = git("show", f"{revision or ''}:{rel}", text=True)
return result.stdout
def tracked_contract_files(*, staged: bool = False, revision: str | None = None) -> dict[str, str]:
"""Read contracts from the working tree, the index, or a commit.
`revision` wins over `staged`; a contract the commit tracks is read only
from that commit, so a caller judging it never sees another tree's text.
"""
payload: dict[str, str] = {}
for rel in tracked_contract_paths(staged=staged, revision=revision):
if staged or revision is not None:
try:
payload[rel] = git_contract_text(rel, revision)
continue
except subprocess.CalledProcessError:
if revision is not None:
raise
payload[rel] = (REPO_ROOT / rel).read_text(encoding="utf-8")
return payload
def section_body(lines: list[str], heading: str) -> list[str]:
start = next(index for index, line in enumerate(lines) if line == heading) + 1
end = len(lines)
for index in range(start, len(lines)):
if lines[index].startswith("## "):
end = index
break
return lines[start:end]
def section_list_items(body_lines: list[str]) -> list[tuple[int, str]]:
items: list[tuple[int, str]] = []
for index, line in enumerate(body_lines):
stripped = line.strip()
if not stripped:
continue
for marker in [f"{n}." for n in range(1, 100)]:
if stripped.startswith(marker + " "):
items.append((index, stripped[len(marker) + 1 :]))
break
return items
def looks_like_repo_path(token: str) -> bool:
candidate = token.strip()
return "/" in candidate or candidate.endswith(PATH_SUFFIXES)
def parse_contract_metadata(body_lines: list[str]) -> tuple[dict[str, Any] | None, list[str]]:
errors: list[str] = []
meaningful = [line for line in body_lines if line.strip()]
if len(meaningful) < 3:
return None, ["contract metadata section must contain a JSON fenced block"]
if meaningful[0].strip() != "```json":
errors.append("contract metadata section must start with ```json")
return None, errors
if meaningful[-1].strip() != "```":
errors.append("contract metadata section must end with ```")
return None, errors
json_block = "\n".join(meaningful[1:-1]).strip()
if not json_block:
errors.append("contract metadata JSON block must not be empty")
return None, errors
try:
payload = json.loads(json_block)
except json.JSONDecodeError as exc:
errors.append(f"contract metadata JSON is invalid: {exc}")
return None, errors
if not isinstance(payload, dict):
errors.append("contract metadata JSON must be an object")
return None, errors
return payload, errors
def parse_contract_text(rel: str, content: str) -> tuple[dict[str, Any], list[str]]:
errors: list[str] = []
path_references: list[dict[str, str]] = []
lines = content.splitlines()
if not lines or not lines[0].startswith("# "):
errors.append(f"{rel} must start with a level-1 heading")
heading_positions: dict[str, int] = {}
for index, line in enumerate(lines):
if line in REQUIRED_SECTIONS:
if line in heading_positions:
errors.append(f"{rel} duplicates required section {line!r}")
heading_positions[line] = index
metadata: dict[str, Any] | None = None
if "## Contract Metadata" in heading_positions:
metadata, metadata_errors = parse_contract_metadata(section_body(lines, "## Contract Metadata"))
errors.extend(f"{rel} {error}" for error in metadata_errors)
for heading in ("## Canonical Files", "## Shared Boundaries", "## Extension Points"):
if heading not in heading_positions:
continue
heading_line = heading_positions[heading] + 1
body_start = heading_positions[heading] + 1
body = section_body(lines, heading)
items = section_list_items(body)
for item_index, item in items:
line_number = body_start + item_index + 1
for token in re.findall(r"`([^`]+)`", item):
if looks_like_repo_path(token):
path_references.append(
{
"heading": heading,
"path": token,
"line": line_number,
"heading_line": heading_line,
}
)
return {
"title": lines[0].strip() if lines else "",
"metadata": metadata,
"path_references": path_references,
"heading_positions": heading_positions,
"lines": lines,
}, errors
def load_contract_index(
contract_texts: dict[str, str] | None = None,
*,
staged: bool = False,
revision: str | None = None,
) -> dict[str, dict[str, Any]]:
contract_index: dict[str, dict[str, Any]] = {}
texts = contract_texts or tracked_contract_files(staged=staged, revision=revision)
for rel, content in texts.items():
if rel == TEMPLATE_REL or not rel.endswith(".md"):
continue
parsed, _ = parse_contract_text(rel, content)
metadata = parsed.get("metadata")
if not isinstance(metadata, dict):
continue
subsystem_id = str(metadata.get("subsystem_id", "")).strip()
if not subsystem_id:
continue
contract_index[subsystem_id] = {
"subsystem_id": subsystem_id,
"contract": rel,
"metadata": metadata,
"path_references": list(parsed.get("path_references", [])),
"title": parsed.get("title", ""),
}
return contract_index
def contract_reference_matches_path(reference_path: str, path: str) -> bool:
if reference_path.endswith("/"):
return path.startswith(reference_path)
return path == reference_path
def referenced_contracts_for_path(
path: str,
contract_index: dict[str, dict[str, Any]] | None = None,
) -> list[dict[str, Any]]:
contracts_by_subsystem = contract_index or load_contract_index()
matches: list[dict[str, Any]] = []
for contract in contracts_by_subsystem.values():
referenced_paths = [
reference
for reference in contract.get("path_references", [])
if contract_reference_matches_path(str(reference.get("path", "")), path)
]
if referenced_paths:
matches.append({**contract, "matched_references": referenced_paths})
return sorted(matches, key=lambda contract: str(contract.get("subsystem_id", "")).casefold())