Files
rune/scripts/carry_forward_check.py
T
Nick 229bad13bd
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 2s
Auto-label / priority-gate (pull_request) Skipped
Auto-label / auto-label (pull_request) Successful in 1s
CI Pipeline / canary (push) Successful in 28s
CI Pipeline / lint (push) Successful in 31s
CI Pipeline / unit-tests (push) Successful in 37s
CI Pipeline / type-check (push) Successful in 25s
CI Pipeline / gitea-offline (push) Successful in 13s
CI Pipeline / required-checks (push) Skipped
CI Pipeline / best-practices (pull_request) Successful in 2s
CI Pipeline / documentation (pull_request) Successful in 2s
CI Pipeline / lint (pull_request) Successful in 28s
CI Pipeline / canary (pull_request) Successful in 24s
Spec gate (headless) / spec-gate (pull_request) Successful in 6s
CI Pipeline / type-check (pull_request) Successful in 23s
CI Pipeline / gitea-offline (pull_request) Successful in 14s
CI Pipeline / unit-tests (pull_request) Successful in 36s
CI Pipeline / required-checks (pull_request) Successful in 0s
feat(robot): carry-forward policy — checks re-run only on contributed-code change (Nick's policy, 2026-09-08)
Behind-base self-heal previously forced a full CI re-run even when the
update's tree diff touched no code path — redundant by construction
(identical code-path trees cannot produce different test outcomes).

- merge_robot emits UPDATED protocol line after self-heal
- scripts/carry_forward_check.py classifies the tested-head→new-head tree
  diff: CARRY_FORWARD (docs-only) vs RERUN_REQUIRED (any code-path touch)
- workflow consumes it: CARRY_FORWARD republishes the required-checks
  aggregate citing tree identity; RERUN_REQUIRED waits for CI as today
- policy recorded: first pass = full pipeline; base movement with
  unchanged code paths = carry; bugs/mistakes = new commits = full re-run

Red-first: 2 new tests (classification + companion script contract).
2026-09-08 02:06:34 +01:00

64 lines
2.0 KiB
Python

#!/usr/bin/env python3
"""carry_forward_check.py — classify a behind-base update's tree diff.
Nick's policy (2026-09-08): pipeline checks re-run only when contributed
code changes. The merge-robot's self-heal updates a stale branch; this
classifier decides whether the prior green may be CARRIED FORWARD to the
new head (docs-only movement) or a full re-run is REQUIRED (any code-path
touch — semantic interactions are real; #210/#212 proved it live).
Usage (workflow step):
git fetch origin <tested_head> <new_head>
CHANGED=$(git diff --name-only <tested_head> <new_head>)
python3 scripts/carry_forward_check.py classify <<< "$CHANGED"
# prints CARRY_FORWARD or RERUN_REQUIRED; exit 0 either way
The evidence contract: CARRY_FORWARD asserts the code-path tree is
IDENTICAL between the tested head and the new head, so prior test outcomes
necessarily still hold. The workflow republishes the required-checks
aggregate citing this identity. Any code-path touch → RERUN_REQUIRED.
"""
from __future__ import annotations
import sys
CODE_PATHS = (
"src/",
"tests/",
"scripts/",
"shared/",
"config/",
".gitea/",
"henxels.yaml",
"henxels_checks.py",
"pyproject.toml",
"requirements-ci.txt",
"lint-baseline.json",
)
def touches_code_path(changed_files: list[str], code_paths=CODE_PATHS) -> bool:
for f in changed_files:
for cp in code_paths:
if f == cp or f.startswith(cp):
return True
return False
def classify(changed_files: list[str], tested_head: str = "", new_head: str = "") -> str:
return "RERUN_REQUIRED" if touches_code_path(changed_files) else "CARRY_FORWARD"
def main_with(argv: list[str]) -> int:
if len(argv) >= 2 and argv[1] == "classify":
files = [ln.strip() for ln in sys.stdin.read().splitlines() if ln.strip()]
print(classify(files))
return 0
print("usage: carry_forward_check.py classify (file list on stdin)", file=sys.stderr)
return 2
if __name__ == "__main__":
sys.exit(main_with(sys.argv))