Files
pulse/scripts/release_control/canonical_completion_guard.py
T
rcourtman a4d2cdc2b8 Integrate current workload evidence and merge qualification
Keep the current clock and stored-History readout alongside canonical row
selection and resource-owned drawer state.

Use one Git inheritance reader for browser and canonical qualification.
Inherited modes and objects keep their parent proof, while new resolutions
and mode changes remain subject to current contract and proof requirements.
Range checks still qualify the complete proposed change.

Refs #1669
2026-10-12 23:07:52 +01:00

832 lines
32 KiB
Python

#!/usr/bin/env python3
"""Canonical subsystem completion guard.
Blocks commits when staged runtime changes touch a canonical subsystem but the
matching subsystem contract file and required verification artifact are not
staged in the same commit.
"""
from __future__ import annotations
import argparse
import difflib
import fnmatch
import json
import os
from pathlib import Path
import subprocess
import sys
from typing import Dict, List, Sequence, Set
import browser_verification_guard
from control_plane import DEFAULT_CONTROL_PLANE
from repo_file_io import git_env, load_repo_json, merged_in_paths
from subsystem_contracts import load_contract_index, referenced_contracts_for_path
CONTRACT_NEUTRAL_OVERRIDE_ENV = "PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT"
def contract_neutral_override_reason() -> str | None:
"""Return the operator-supplied reason for bypassing canonical-shape checks.
The `PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT` env var opts a single commit out of
the canonical-shape block when the author knows the runtime change is
behavioral (no public-contract delta). A non-empty value is required so the
reason is auditable in stderr; empty strings are treated as unset.
"""
reason = os.environ.get(CONTRACT_NEUTRAL_OVERRIDE_ENV, "").strip()
return reason or None
def prettier_only_contract_neutral_reason(
staged_files: Sequence[str],
*,
commit: str | None,
) -> str | None:
"""Recognise a committed frontend reformat without trusting its message.
Historical commits cannot be amended after review. CI may therefore need
to establish that a formatting repair without a Contract-Neutral trailer
is contract-neutral from the commit's bytes rather than retroactively
changing its metadata. This path
fails closed: every canonical runtime path in the commit must be a
user-visible frontend path whose new blob is exactly locked Prettier output
for its parent blob. Staged/pre-commit checks still require the explicit
operator reason because they have no immutable commit to inspect.
"""
if not commit:
return None
impacted = infer_impacted_subsystems(staged_files, revision=commit)
runtime_paths = {
path
for data in impacted.values()
for path in data.get("touched_runtime_files", [])
if path not in set(data.get("cycle_artifact_files", []))
}
if not runtime_paths:
return None
frontend_paths = set(browser_verification_guard.frontend_runtime_paths(runtime_paths))
if frontend_paths != runtime_paths:
return None
reformatted = browser_verification_guard.formatting_only_paths(
sorted(frontend_paths),
commit=commit,
)
if reformatted != frontend_paths:
return None
return f"mechanically verified Prettier-only frontend reformat in {commit}"
REPO_ROOT = Path(__file__).resolve().parents[2]
SUBSYSTEM_REGISTRY = DEFAULT_CONTROL_PLANE["registry_path"]
REQUIRED_VERIFICATION_FIELDS: tuple[str, ...] = (
"allow_same_subsystem_tests",
"test_prefixes",
"exact_files",
"require_explicit_path_policy_coverage",
)
REQUIRED_PATH_POLICY_FIELDS: tuple[str, ...] = (
"allow_same_subsystem_tests",
"test_prefixes",
"exact_files",
)
SUBSTANTIVE_CONTRACT_SECTIONS: tuple[str, ...] = (
"## Purpose",
"## Canonical Files",
"## Shared Boundaries",
"## Extension Points",
"## Forbidden Paths",
"## Completion Obligations",
"## Current State",
)
def validate_verification_policy(
subsystem_id: str,
policy: dict,
*,
context: str,
required_fields: tuple[str, ...] = REQUIRED_VERIFICATION_FIELDS,
) -> None:
if not isinstance(policy, dict):
raise ValueError(f"subsystem {subsystem_id} {context} missing verification policy")
for field in required_fields:
if field not in policy:
raise ValueError(f"subsystem {subsystem_id} {context} missing {field}")
def load_subsystem_rules(*, staged: bool = False, revision: str | None = None) -> List[dict]:
payload = load_repo_json(SUBSYSTEM_REGISTRY, staged=staged, revision=revision)
rules = list(payload.get("subsystems", []))
for rule in rules:
subsystem_id = str(rule.get("id"))
verification = rule.get("verification")
validate_verification_policy(subsystem_id, verification, context="verification policy")
path_policies = verification.get("path_policies", [])
if not isinstance(path_policies, list):
raise ValueError(f"subsystem {subsystem_id} verification policy path_policies must be a list")
for index, policy in enumerate(path_policies):
validate_verification_policy(
subsystem_id,
policy,
context=f"path policy #{index}",
required_fields=REQUIRED_PATH_POLICY_FIELDS,
)
if "id" not in policy:
raise ValueError(f"subsystem {subsystem_id} path policy #{index} missing id")
if "match_prefixes" not in policy:
raise ValueError(
f"subsystem {subsystem_id} path policy {policy['id']} missing match_prefixes"
)
if "match_files" not in policy:
raise ValueError(
f"subsystem {subsystem_id} path policy {policy['id']} missing match_files"
)
return rules
IGNORED_PREFIXES: tuple[str, ...] = (
DEFAULT_CONTROL_PLANE["profile_root_rel"] + "/",
"internal/repoctl/",
".husky/",
"scripts/release_control/",
)
def git_staged_files() -> List[str]:
result = subprocess.run(
["git", "diff", "--cached", "--name-only", "--diff-filter=ACMRD"],
cwd=REPO_ROOT,
check=True,
capture_output=True,
text=True,
env=git_env(REPO_ROOT),
)
paths = [line.strip() for line in result.stdout.splitlines() if line.strip()]
inherited = merged_in_paths(paths, commit=None, repo_root=REPO_ROOT)
return [path for path in paths if path not in inherited]
def stdin_files(stdin: Sequence[str]) -> List[str]:
return [line.strip() for line in stdin if line.strip()]
def is_test_or_fixture(path: str) -> bool:
if path.startswith("scripts/tests/"):
return True
if path.endswith("_test.go"):
return True
if "/__tests__/" in path:
return True
if path.endswith(".test.ts") or path.endswith(".test.tsx"):
return True
if path.endswith(".spec.ts") or path.endswith(".spec.tsx"):
return True
return False
def is_ignored_runtime_file(path: str) -> bool:
return any(path.startswith(prefix) for prefix in IGNORED_PREFIXES)
def subsystem_matches_path(rule: dict, path: str) -> bool:
prefixes = tuple(rule.get("owned_prefixes", []))
exact_files = tuple(rule.get("owned_files", []))
return (
any(path == prefix.rstrip("/") or path.startswith(prefix) for prefix in prefixes)
or path in exact_files
)
def is_release_cycle_artifact(rule: dict, path: str) -> bool:
"""Return True if path matches one of this subsystem's release_cycle_artifact_globs.
Release-cycle artifacts (RC packet drafts, version-pointer docs, regenerated
record files) are tracked by the subsystem but are per-release-cycle content
rather than contract-shape surfaces. They skip the contract-update
requirement and verification requirements so routine RC-cycle maintenance
doesn't need the contract-neutral bypass.
"""
globs = tuple(rule.get("release_cycle_artifact_globs", []))
return any(fnmatch.fnmatchcase(path, glob) for glob in globs)
def path_policy_matches(policy: dict, path: str) -> bool:
prefixes = tuple(policy.get("match_prefixes", []))
exact_files = tuple(policy.get("match_files", []))
return (
any(path == prefix.rstrip("/") or path.startswith(prefix) for prefix in prefixes)
or path in exact_files
)
def normalize_verification_requirement(
source: dict,
*,
requirement_id: str,
label: str,
touched_runtime_files: Sequence[str],
) -> dict:
return {
"id": requirement_id,
"label": label,
"touched_runtime_files": sorted(set(touched_runtime_files)),
"allow_same_subsystem_tests": bool(source.get("allow_same_subsystem_tests", False)),
"test_prefixes": sorted(set(source.get("test_prefixes", []))),
"exact_files": sorted(set(source.get("exact_files", []))),
}
def build_verification_requirements(rule: dict, touched_runtime_files: Sequence[str]) -> List[dict]:
verification = dict(rule.get("verification", {}))
path_policies = list(verification.get("path_policies", []))
require_explicit_path_policy_coverage = bool(
verification.get("require_explicit_path_policy_coverage", False)
)
requirements: List[dict] = []
matched_by_policy: Dict[str, dict] = {}
unmatched_runtime_files: List[str] = []
for path in touched_runtime_files:
matching_policy = next((policy for policy in path_policies if path_policy_matches(policy, path)), None)
if matching_policy is None:
unmatched_runtime_files.append(path)
continue
policy_id = str(matching_policy["id"])
requirement = matched_by_policy.get(policy_id)
if requirement is None:
requirement = normalize_verification_requirement(
matching_policy,
requirement_id=policy_id,
label=str(matching_policy.get("label", policy_id)),
touched_runtime_files=[],
)
matched_by_policy[policy_id] = requirement
requirements.append(requirement)
requirement["touched_runtime_files"].append(path)
if unmatched_runtime_files:
if require_explicit_path_policy_coverage:
requirements.insert(
0,
{
"id": "missing-path-policy-coverage",
"label": "registry path policy coverage",
"touched_runtime_files": list(unmatched_runtime_files),
"allow_same_subsystem_tests": False,
"test_prefixes": [],
"exact_files": [],
"path_policy_gap": True,
},
)
else:
requirements.insert(
0,
normalize_verification_requirement(
verification,
requirement_id="default",
label="default subsystem verification",
touched_runtime_files=unmatched_runtime_files,
),
)
return requirements
def infer_impacted_subsystems(
staged_files: Sequence[str],
*,
use_staged_registry: bool = False,
revision: str | None = None,
) -> Dict[str, dict]:
impacted: Dict[str, dict] = {}
rules = load_subsystem_rules(staged=use_staged_registry, revision=revision)
rules_by_id = {str(rule["id"]): rule for rule in rules}
for path in staged_files:
if is_ignored_runtime_file(path) or is_test_or_fixture(path):
continue
for rule in rules:
if not subsystem_matches_path(rule, path):
continue
entry = impacted.setdefault(
str(rule["id"]),
{
"id": str(rule["id"]),
"contract": str(rule["contract"]),
"touched_runtime_files": [],
"cycle_artifact_files": [],
"verification": dict(rule.get("verification", {})),
# Ownership travels with the entry because the guards pass
# this entry, not the registry rule, as the `rule` argument
# of staged_verification_files_for_requirement; without it
# a requirement's allow_same_subsystem_tests could never
# match while subsystem_lookup reported that it did.
"owned_prefixes": list(rule.get("owned_prefixes", [])),
"owned_files": list(rule.get("owned_files", [])),
},
)
entry["touched_runtime_files"].append(path)
if is_release_cycle_artifact(rule, path):
# Path is owned by this subsystem but is a per-release-cycle
# artifact (RC packet draft, version-pointer doc, regenerated
# record). Tracked here so the lookup tool can still report
# ownership; contract-update / verification requirement
# builders consult this list to exclude these paths.
entry["cycle_artifact_files"].append(path)
for subsystem_id, data in impacted.items():
# Verification requirements derive from non-cycle-artifact touches only.
# Release-cycle artifacts (RC packet drafts, version-pointer docs) don't
# trigger verification proof requirements.
cycle_artifacts = set(data.get("cycle_artifact_files", []))
non_artifact_files = [
path for path in data["touched_runtime_files"] if path not in cycle_artifacts
]
data["verification_requirements"] = build_verification_requirements(
rules_by_id[subsystem_id],
non_artifact_files,
)
return impacted
def required_contract_updates(
staged_files: Sequence[str],
impacted: Dict[str, dict] | None = None,
*,
use_staged_contract_index: bool = False,
revision: str | None = None,
) -> Dict[str, dict]:
impacted_subsystems = (
impacted
if impacted is not None
else infer_impacted_subsystems(staged_files, revision=revision)
)
required: Dict[str, dict] = {}
contract_index = load_contract_index(staged=use_staged_contract_index, revision=revision)
for subsystem_id, data in impacted_subsystems.items():
# Contract update requirement triggers on non-cycle-artifact touches
# only. If every touched file is a release-cycle artifact (RC packet
# draft, version-pointer doc, regenerated record), there's no
# contract-shape change to require — the contract describes the
# release process, not the per-RC content of any specific cycle.
cycle_artifacts = set(data.get("cycle_artifact_files", []))
contract_impacting = sorted(
set(data["touched_runtime_files"]) - cycle_artifacts
)
if not contract_impacting:
continue
required[data["contract"]] = {
"subsystem": subsystem_id,
"contract": data["contract"],
"reason": "owner",
"touched_runtime_files": contract_impacting,
"matched_references": [],
}
# Dependent-reference contracts also skip cycle artifacts — the contract
# describes the release process, not specific RC packet contents.
cycle_artifact_paths = {
path
for data in impacted_subsystems.values()
for path in data.get("cycle_artifact_files", [])
}
touched_runtime_files = sorted(
{
path
for data in impacted_subsystems.values()
for path in data.get("touched_runtime_files", [])
if path not in cycle_artifact_paths
}
)
for path in touched_runtime_files:
for contract in referenced_contracts_for_path(path, contract_index):
contract_path = str(contract["contract"])
entry = required.setdefault(
contract_path,
{
"subsystem": str(contract["subsystem_id"]),
"contract": contract_path,
"reason": "dependent-reference",
"touched_runtime_files": [],
"matched_references": [],
},
)
if entry["reason"] == "owner":
continue
if path not in entry["touched_runtime_files"]:
entry["touched_runtime_files"].append(path)
for reference in contract.get("matched_references", []):
descriptor = f"{reference['heading']}: {reference['path']}"
if descriptor not in entry["matched_references"]:
entry["matched_references"].append(descriptor)
detail = {
"heading": str(reference.get("heading", "")),
"path": str(reference.get("path", "")),
}
if isinstance(reference.get("line"), int):
detail["line"] = int(reference["line"])
if isinstance(reference.get("heading_line"), int):
detail["heading_line"] = int(reference["heading_line"])
detail_list = entry.setdefault("matched_reference_details", [])
if detail not in detail_list:
detail_list.append(detail)
for data in required.values():
data["touched_runtime_files"] = sorted(set(data["touched_runtime_files"]))
data["matched_references"] = sorted(set(data["matched_references"]), key=str.casefold)
details = data.get("matched_reference_details")
if isinstance(details, list) and details:
data["matched_reference_details"] = sorted(
details,
key=lambda item: (
int(item.get("line", 0) or 0),
str(item.get("heading", "")).casefold(),
str(item.get("path", "")).casefold(),
),
)
return dict(sorted(required.items()))
def staged_verification_files_for_requirement(rule: dict, requirement: dict, staged_files: Sequence[str]) -> List[str]:
exact_files = set(requirement.get("exact_files", []))
test_prefixes = tuple(requirement.get("test_prefixes", []))
allow_same_subsystem_tests = bool(requirement.get("allow_same_subsystem_tests", False))
matches: List[str] = []
for path in staged_files:
if path in exact_files:
matches.append(path)
continue
if not is_test_or_fixture(path):
continue
if any(path.startswith(prefix) for prefix in test_prefixes):
matches.append(path)
continue
if allow_same_subsystem_tests and subsystem_matches_path(rule, path):
matches.append(path)
return sorted(set(matches))
def git_blob_text(spec: str) -> str:
result = subprocess.run(
["git", "show", spec],
cwd=REPO_ROOT,
check=False,
capture_output=True,
text=True,
)
if result.returncode != 0:
return ""
return result.stdout
def contract_section_map(lines: Sequence[str]) -> List[str]:
"""Map each line to the `## ` section it belongs to, from the full file."""
sections: List[str] = []
current = ""
for line in lines:
stripped = line.strip()
if stripped.startswith("## "):
current = stripped
sections.append(current)
return sections
def _is_substantive_contract_line(stripped: str, section: str) -> bool:
if section not in SUBSTANTIVE_CONTRACT_SECTIONS:
return False
if not stripped:
return False
if stripped.startswith("## "):
return False
if stripped in {"```", "```json"}:
return False
return True
def contract_texts_have_substantive_change(base_text: str, staged_text: str) -> bool:
"""Compare full contract texts so section membership never depends on how
close a change sits to its `## ` header (diff-context attribution broke on
sections longer than the context window)."""
base_lines = base_text.splitlines()
staged_lines = staged_text.splitlines()
base_sections = contract_section_map(base_lines)
staged_sections = contract_section_map(staged_lines)
matcher = difflib.SequenceMatcher(a=base_lines, b=staged_lines, autojunk=False)
for tag, base_start, base_end, staged_start, staged_end in matcher.get_opcodes():
if tag == "equal":
continue
for idx in range(base_start, base_end):
if _is_substantive_contract_line(base_lines[idx].strip(), base_sections[idx]):
return True
for idx in range(staged_start, staged_end):
if _is_substantive_contract_line(staged_lines[idx].strip(), staged_sections[idx]):
return True
return False
def resolve_diff_base(ref: str, revision: str = "HEAD") -> str:
"""Resolve a diff-base ref to its merge base with the evaluated revision.
A caller may pass a range base whose changed-file list was computed with
a three-dot diff (`base...revision`), which diffs from the merge base.
Using the same anchor here keeps the contract-text comparison consistent
with the file list; for the per-commit form (`<commit>^` against
`<commit>`) it is simply the parent. Falls back to the raw ref when the
merge base cannot be resolved (e.g. a zero SHA on branch creation).
"""
result = subprocess.run(
["git", "merge-base", ref, revision],
cwd=REPO_ROOT,
check=False,
capture_output=True,
text=True,
)
merge_base = result.stdout.strip()
return merge_base if result.returncode == 0 and merge_base else ref
def staged_contract_has_substantive_change(
path: str,
diff_base: str | None = None,
revision: str = "HEAD",
) -> bool:
"""Return True if the contract's change includes a substantive section edit.
Both modes compare the evaluated change against its own parent. Without a
diff base this compares HEAD against the index — the pre-commit mode,
where the pending change is staged. With a diff base (CI mode) it compares
the base against `revision`, the commit whose file list is being checked:
in a CI checkout the index equals HEAD, so the index comparison is always
empty there and would misreport every contract update as insubstantial.
HEAD is the right new side only when the evaluated commit is checked out.
CI and the pre-push check walk a branch's commits with HEAD at its tip,
where comparing against HEAD judged each commit by the whole branch: a
later commit reverting a contract paragraph blocked the commit that wrote
it, and a later contract update let an earlier bare runtime change pass.
"""
if diff_base is None:
return contract_texts_have_substantive_change(
git_blob_text(f"HEAD:{path}"),
git_blob_text(f":0:{path}"),
)
return contract_texts_have_substantive_change(
git_blob_text(f"{diff_base}:{path}"),
git_blob_text(f"{revision}:{path}"),
)
def format_missing_requirements(
missing_contracts: Dict[str, dict],
insufficient_contract_updates: Dict[str, dict],
missing_verification: Dict[str, dict],
) -> str:
lines = [
"BLOCKED: canonical subsystem changes require matching contract and verification updates.",
"",
"Stage the required subsystem file(s) in the same commit:",
]
for contract_path, data in sorted(missing_contracts.items()):
if data.get("reason") == "dependent-reference":
lines.append(
f"- dependent subsystem {data['subsystem']}: missing contract {contract_path}"
)
lines.append(
" touched runtime files are canonical references in that dependent subsystem contract"
)
else:
lines.append(f"- subsystem {data['subsystem']}: missing contract {contract_path}")
for path in sorted(data["touched_runtime_files"]):
lines.append(f" touched by {path}")
if data.get("reason") == "dependent-reference":
for reference in data.get("matched_references", []):
lines.append(f" referenced by {reference}")
for contract_path, data in sorted(insufficient_contract_updates.items()):
lines.append(
f"- subsystem {data['subsystem']}: contract {contract_path} is staged but does not include a substantive section update"
)
lines.append(
" update one of: Purpose, Canonical Files, Shared Boundaries, Extension Points, Forbidden Paths, Completion Obligations, or Current State"
)
for path in sorted(data["touched_runtime_files"]):
lines.append(f" touched by {path}")
if data.get("reason") == "dependent-reference":
for reference in data.get("matched_references", []):
lines.append(f" referenced by {reference}")
for subsystem_id, data in sorted(missing_verification.items()):
for requirement in data["missing_requirements"]:
if requirement.get("path_policy_gap"):
lines.append(
f"- subsystem {subsystem_id}: missing registry path policy coverage for touched runtime files"
)
else:
lines.append(
f"- subsystem {subsystem_id}: missing verification artifact for {requirement['label']}"
)
for path in sorted(requirement["touched_runtime_files"]):
lines.append(f" touched by {path}")
if requirement.get("path_policy_gap"):
lines.append(
" update "
f"{DEFAULT_CONTROL_PLANE['registry_rel']} so each touched path matches an explicit path policy"
)
lines.append(
f" default subsystem verification is forbidden for governed {DEFAULT_CONTROL_PLANE['active_profile_id']} subsystems"
)
continue
exact_files = sorted(requirement.get("exact_files", []))
test_prefixes = sorted(requirement.get("test_prefixes", []))
allow_same_subsystem_tests = bool(requirement.get("allow_same_subsystem_tests", False))
if exact_files:
lines.append(" acceptable exact proof files include:")
for path in exact_files:
lines.append(f" {path}")
if test_prefixes:
lines.append(" acceptable staged test prefixes include:")
for prefix in test_prefixes:
lines.append(f" {prefix}")
if allow_same_subsystem_tests:
lines.append(" same-subsystem test/spec files are also accepted")
lines.extend(
[
"",
"Rule:",
"If a canonical subsystem changes, its contract under",
f"`{DEFAULT_CONTROL_PLANE['subsystems_dir_rel']}/` must be updated in the same commit.",
"If a touched runtime path is also named in another subsystem contract's",
"`Canonical Files`, `Shared Boundaries`, or `Extension Points`, that dependent contract must be updated too.",
"A staged contract file only counts if its staged diff changes a substantive contract section,",
"not just `Contract Metadata` or cosmetic noise.",
"Each touched runtime path must also satisfy the first matching",
"verification policy from that subsystem's registry entry.",
]
)
return "\n".join(lines)
def check_staged_contracts(
staged_files: Sequence[str],
*,
diff_base: str | None = None,
revision: str = "HEAD",
inferred_override_reason: str | None = None,
) -> int:
staged_set: Set[str] = set(staged_files)
# Judge the change by the rules it was written under: the staged registry
# and contracts before a commit exists, the evaluated commit's own after.
# Reading HEAD's in CI let a later commit on the branch (or main merged
# under it) add or drop ownership and contract references for an
# earlier commit, so its verdict depended on what came after it.
governance_revision = revision if diff_base is not None else None
impacted = infer_impacted_subsystems(
staged_files,
use_staged_registry=True,
revision=governance_revision,
)
required_contracts = required_contract_updates(
staged_files,
impacted,
use_staged_contract_index=True,
revision=governance_revision,
)
missing_contracts = {
contract_path: data
for contract_path, data in required_contracts.items()
if contract_path not in staged_set
}
insufficient_contract_updates = {
contract_path: data
for contract_path, data in required_contracts.items()
if contract_path in staged_set
if not staged_contract_has_substantive_change(contract_path, diff_base, revision)
}
missing_verification: Dict[str, dict] = {}
for subsystem_id, data in impacted.items():
missing_requirements = [
requirement
for requirement in data.get("verification_requirements", [])
if not staged_verification_files_for_requirement(data, requirement, staged_files)
]
if missing_requirements:
missing_verification[subsystem_id] = {
**data,
"missing_requirements": missing_requirements,
}
if not missing_contracts and not insufficient_contract_updates and not missing_verification:
return 0
formatted = format_missing_requirements(
missing_contracts,
insufficient_contract_updates,
missing_verification,
)
explicit_override_reason = contract_neutral_override_reason()
override_reason = explicit_override_reason or inferred_override_reason
if override_reason:
if explicit_override_reason:
print(
"Canonical completion guard: canonical-shape block bypassed by "
f"{CONTRACT_NEUTRAL_OVERRIDE_ENV}={override_reason!r}",
file=sys.stderr,
)
else:
print(
"Canonical completion guard: canonical-shape block bypassed by "
f"fail-closed commit inspection ({override_reason}).",
file=sys.stderr,
)
print("Suppressed requirements (logged for audit):", file=sys.stderr)
print(formatted, file=sys.stderr)
return 0
print(formatted, file=sys.stderr)
print(
"",
file=sys.stderr,
)
print(
"If this change is contract-neutral (behavioral fix, no public-contract "
"delta), you may bypass only this block by setting "
f"{CONTRACT_NEUTRAL_OVERRIDE_ENV}=<reason>. The reason is logged to "
"stderr. Sensitivity, gitleaks, governance-stage, control-plane, "
"status, registry, and contract audits still run.",
file=sys.stderr,
)
return 1
def parse_args(argv: Sequence[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Enforce canonical subsystem contract and proof-of-change updates for "
"staged files or an explicit changed-file list."
)
)
parser.add_argument(
"--files-from-stdin",
action="store_true",
help="Read newline-delimited changed files from standard input instead of git staged files.",
)
parser.add_argument(
"--diff-base",
default=None,
help=(
"Commit-ish to compare contract texts against instead of the index. "
"Use in CI, where nothing is staged and the changed-file list comes "
"from a commit range; requires --files-from-stdin. Pass --commit "
"too unless HEAD is the commit being evaluated."
),
)
parser.add_argument(
"--commit",
default=None,
help=(
"Committed revision represented by the changed-file list. With "
"--diff-base, contract texts are compared against this revision "
"instead of HEAD. In CI this also permits a fail-closed byte "
"comparison for mechanically verified Prettier-only frontend "
"reformats."
),
)
args = parser.parse_args(list(argv))
if args.diff_base and not args.files_from_stdin:
parser.error("--diff-base requires --files-from-stdin")
if args.commit and not args.files_from_stdin:
parser.error("--commit requires --files-from-stdin")
return args
def main(argv: Sequence[str] | None = None) -> int:
args = parse_args(list(argv or ()))
revision = args.commit or "HEAD"
diff_base = resolve_diff_base(args.diff_base, revision) if args.diff_base else None
if args.files_from_stdin:
files = stdin_files(sys.stdin)
inferred_reason = prettier_only_contract_neutral_reason(files, commit=args.commit)
return check_staged_contracts(
files,
diff_base=diff_base,
revision=revision,
inferred_override_reason=inferred_reason,
)
return check_staged_contracts(git_staged_files())
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))