CI Pipeline / lint (push) Failing after 45s
CI Pipeline / unit-tests (push) Failing after 17s
CI Pipeline / canary (push) Successful in 26s
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / type-check (push) Successful in 21s
CI Pipeline / gitea-offline (push) Successful in 13s
CI Pipeline / required-checks (push) Skipped
CI Pipeline / lint (pull_request) Failing after 23s
CI Pipeline / unit-tests (pull_request) Failing after 16s
CI Pipeline / canary (pull_request) Successful in 25s
CI Pipeline / best-practices (pull_request) Successful in 2s
CI Pipeline / documentation (pull_request) Successful in 3s
CI Pipeline / type-check (pull_request) Successful in 55s
CI Pipeline / gitea-offline (pull_request) Successful in 13s
CI Pipeline / required-checks (pull_request) Skipped
Spec gate (headless) / spec-gate (pull_request) Successful in 5s
170 lines
6.9 KiB
Python
170 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
|
"""gen_prd_state.py — regenerate the living-state sections of the PRDs.
|
|
|
|
The PRDs' doctrine sections are hand-written; their "current state" sections
|
|
are GENERATED from governed sources (config/umbrellas.json, the pipeline/TUI
|
|
registries, the version surface) so they cannot drift from reality. Henxels
|
|
`changed_with` rules + the CI staleness check make regeneration mandatory in
|
|
the same PR as the change (docs/bugfix-contract.md discipline).
|
|
|
|
Usage:
|
|
gen_prd_state.py # rewrite generated sections in both PRDs
|
|
gen_prd_state.py --check # exit 1 if any generated section is stale (CI)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from datetime import date
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
BEGIN = "<!-- BEGIN GENERATED: state — edits here are overwritten by scripts/gen_prd_state.py -->"
|
|
END = "<!-- END GENERATED: state -->"
|
|
|
|
|
|
def version() -> str:
|
|
text = (ROOT / "src" / "rune" / "__init__.py").read_text()
|
|
match = re.search(r'__version__ = "([0-9]+\.[0-9]+\.[0-9]+)"', text)
|
|
return match.group(1) if match else "unknown"
|
|
|
|
|
|
def git_head() -> str:
|
|
try:
|
|
return subprocess.run(
|
|
["git", "rev-parse", "--short", "HEAD"], cwd=ROOT, capture_output=True, text=True, check=True
|
|
).stdout.strip()
|
|
except subprocess.CalledProcessError:
|
|
return "unknown"
|
|
|
|
|
|
def pipeline_state() -> str:
|
|
umbrellas = json.loads((ROOT / "config" / "umbrellas.json").read_text())["umbrellas"]
|
|
lines = [
|
|
f"**Version**: v{version()} · **HEAD**: {git_head()} · **Regenerated**: {date.today().isoformat()}",
|
|
"",
|
|
"### Impact map (config/umbrellas.json — enforced complete by henxels)",
|
|
"",
|
|
"| Umbrella | Modules | Canaries | Depends on |",
|
|
"|---|---|---|---|",
|
|
]
|
|
for name, d in umbrellas.items():
|
|
lines.append(
|
|
f"| {name} | {len(d['modules'])} | {', '.join(d['canaries']) or '—'} | {', '.join(d.get('depends_on', [])) or '—'} |"
|
|
)
|
|
total = sum(len(d["modules"]) for d in umbrellas.values())
|
|
lines.append(f"| **total** | **{total} modules, 1:1 mapped** | | |")
|
|
lines += ["", "### Gates (merge-blocking, in enforcement order)", ""]
|
|
gates = [
|
|
("priority/labels", "auto-label.yml", "issues carry exactly one priority; research-pending on ready"),
|
|
(
|
|
"research evidence",
|
|
"pipeline-gate.yml research-clear",
|
|
"INWARD citations + OUTWARD live-fetched sources with per-source trail + synthesis qualification (sufficiency, feasibility, kill test) or honest MORE-RESEARCH",
|
|
),
|
|
(
|
|
"umbrella propagation",
|
|
"ci.yml unit-tests job → scripts/umbrella_impact.py",
|
|
"PR diff resolves against the impact map; unmapped surfaces fail; dependent umbrellas need files-in-PR or a Propagation statement",
|
|
),
|
|
(
|
|
"discharge contract",
|
|
"scripts/check_discharge_contract.py",
|
|
"bug-fix PRs claiming discharges carry red test hunks (docs/bugfix-contract.md)",
|
|
),
|
|
(
|
|
"dependency lock",
|
|
"scripts/check_dep_lock.py",
|
|
"requirements-ci.txt regenerates identically; floor-style lines fail",
|
|
),
|
|
("coverage ratchet", "ci.yml test job", "global floor only rises; changed lines ≥70% via diff-cover"),
|
|
("lint + type ratchet", "ci.yml lint/type-check", "ruff E9/F63/F7/F82; pyright counts never increase"),
|
|
("canaries", "ci.yml canary job", "every src/rune/**/canary_*.py registered and passing"),
|
|
("merge", "merge-robot.yml", "green+mergeable PRs merge deterministically; refusals fail the job loudly"),
|
|
(
|
|
"release",
|
|
"auto-version.yml + maintenance.yml",
|
|
"PR-title semver (authoritative) + push-triggered convergence safety net + drift reconciliation",
|
|
),
|
|
]
|
|
lines += [f"| {name} | `{where}` | {what} |" for name, where, what in gates]
|
|
lines += ["", "### henxels contract surface", ""]
|
|
checks = (ROOT / "henxels_checks.py").read_text()
|
|
lines += [f"- `{m}`" for m in re.findall(r'@statement\(\s*"(\w+)"', checks)]
|
|
return "\n".join(lines)
|
|
|
|
|
|
def tui_state() -> str:
|
|
registry = (ROOT / "docs" / "tui-registry.md").read_text()
|
|
rows = [ln for ln in registry.splitlines() if ln.startswith("| `")]
|
|
tui_lines = (ROOT / "src" / "rune" / "rune_tui.py").read_text().count("\n")
|
|
lines = [
|
|
f"**Version**: v{version()} · **HEAD**: {git_head()} · **Regenerated**: {date.today().isoformat()}",
|
|
"",
|
|
"### Governed TUI surfaces (docs/tui-registry.md)",
|
|
"",
|
|
]
|
|
lines += rows
|
|
lines += [
|
|
"",
|
|
f"`rune_tui.py` is {tui_lines} lines — the known giant, file-size debt tracked in henxels; decomposition target #125.",
|
|
"",
|
|
"### TUI-adjacent subsystems (impact map)",
|
|
"",
|
|
]
|
|
umbrellas = json.loads((ROOT / "config" / "umbrellas.json").read_text())["umbrellas"]
|
|
lines.append(
|
|
f"- orchestration umbrella: {len(umbrellas['orchestration']['modules'])} modules (operator, TUI presentation, task lifecycle)"
|
|
)
|
|
lines.append(
|
|
f"- perception umbrella: {len(umbrellas['perception']['modules'])} modules (context assembly, capsule)"
|
|
)
|
|
lines.append(f"- capability umbrella: {len(umbrellas['capability']['modules'])} modules (tool authority)")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def regenerate(path: Path, new_state: str) -> bool:
|
|
text = path.read_text()
|
|
if BEGIN not in text or END not in text:
|
|
# insert the section after the first '---' separator (header block)
|
|
idx = text.index("\n---\n", text.index("---")) + len("\n---\n")
|
|
block = f"\n{BEGIN}\n{new_state}\n{END}\n"
|
|
path.write_text(text[:idx] + block + text[idx:])
|
|
return True
|
|
pattern = re.compile(re.escape(BEGIN) + r"[\s\S]*?" + re.escape(END))
|
|
updated = pattern.sub(BEGIN + "\n" + new_state + "\n" + END, text)
|
|
stamp = re.sub(r"(\*\*Last Updated\*\*: )\S+", rf"\g<1>{date.today().isoformat()}", updated)
|
|
if stamp != text:
|
|
path.write_text(stamp)
|
|
return True
|
|
return False
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
check = "--check" in argv
|
|
targets = [(ROOT / "docs" / "pipeline-prd.md", pipeline_state), (ROOT / "PRD.md", tui_state)]
|
|
stale = []
|
|
for path, gen in targets:
|
|
before = path.read_text() if path.exists() else ""
|
|
changed = regenerate(path, gen())
|
|
if check and changed:
|
|
stale.append(str(path.relative_to(ROOT)))
|
|
elif changed:
|
|
print(f"regenerated: {path.relative_to(ROOT)}")
|
|
if check:
|
|
if stale:
|
|
print("PRD state sections are STALE — run scripts/gen_prd_state.py and commit:")
|
|
for s in stale:
|
|
print(f" - {s}")
|
|
return 1
|
|
print("PRD state sections in sync")
|
|
return 0
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv))
|