RedFlag/.publication/commit_voice.py
Fimeg 67e26be2d9 projection: begin the exact-path public epoch
The public tree and its history contain only the listed paths. Earlier projection history remains preserved internally.

Source-Sha: 913fde029b935671833254797f0f20f1eb9fabba

Policy-Sha: 913fde029b935671833254797f0f20f1eb9fabba

Tree-Digest: 180ae530c1058a2a5c89837bdce2d323ae83e669e38590ca72e75b8d92b7262f
2026-09-08 21:59:33 -04:00

156 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""Block public commit prose that is not fit to leave the private forge."""
import argparse
import json
import re
import subprocess
import sys
from datetime import date
MAX_SUBJECT = 59
MAX_PROSE = 180
MAX_SENTENCES = 2
TRAILER = re.compile(r"^(?:Source-Sha|Policy-Sha|Tree-Digest):\s+\S+$")
PRIVATE = re.compile(
r"\b(?:10|127)(?:\.[0-9]{1,3}){2,3}\b"
r"|\b192\.168(?:\.[0-9]{1,3}){1,2}\b"
r"|\b172\.(?:1[6-9]|2[0-9]|3[01])(?:\.[0-9]{1,3}){1,2}\b"
r"|\bwiuf[-a-z0-9_]*\b|\barchdev\b|/home/[a-z0-9_-]+|/root/",
re.I,
)
ATTRIBUTION = re.compile(
r"co-authored-by:.*(?:claude|openai|chatgpt|copilot|letta|cursor)"
r"|generated by|generated with|ai-assisted|auto-generated by",
re.I,
)
PROMPT = re.compile(
r"\bcasey\b|\b(?:the\s+)?user\s+(?:said|asked|wanted|reported|told)\b"
r"|(?:\*|_)[\"“].+?[\"”](?:\*|_)|^\s*>\s+",
re.I | re.M | re.S,
)
RULES = {
"subject-empty", "subject-length", "subject-case", "prose-length",
"prose-sentences", "private-infrastructure", "model-attribution",
"prompt-prose",
}
def public_prose(body):
"""Drop transport trailers; they are proof, not Field Notes prose."""
lines = body.strip().splitlines()
while lines and (not lines[-1].strip() or TRAILER.match(lines[-1].strip())):
lines.pop()
return "\n".join(lines).strip()
def violations(subject, body):
prose = public_prose(body)
message = f"{subject}\n{prose}"
found = []
if not subject:
found.append(("subject-empty", "subject is empty"))
if len(subject) > MAX_SUBJECT:
found.append(("subject-length", f"subject is {len(subject)} characters; maximum is {MAX_SUBJECT}"))
first_alpha = next((char for char in subject if char.isalpha()), "")
if first_alpha and not first_alpha.islower():
found.append(("subject-case", "subject must start lowercase"))
if len(prose) > MAX_PROSE:
found.append(("prose-length", f"public prose is {len(prose)} characters; maximum is {MAX_PROSE}"))
sentences = len(re.findall(r"[.!?](?=\s|$)", prose))
if sentences > MAX_SENTENCES:
found.append(("prose-sentences", f"public prose has {sentences} sentences; maximum is {MAX_SENTENCES}"))
if PRIVATE.search(message):
found.append(("private-infrastructure", "message contains private infrastructure"))
if ATTRIBUTION.search(message):
found.append(("model-attribution", "message contains model attribution"))
if PROMPT.search(message):
found.append(("prompt-prose", "message contains prompt or private conversational prose"))
return found
def load_allowlist(path, repository):
with open(path) as handle:
raw = json.load(handle)
if raw.get("schema_version") != 2:
raise ValueError("allowlist schema_version must be 2")
if raw.get("repository") != repository:
raise ValueError("allowlist repository does not match this publication")
allowed = {}
for item in raw.get("exceptions", []):
item_repository = str(item.get("repository") or "")
sha = str(item.get("sha") or "").lower()
rule = str(item.get("rule") or "")
reason = str(item.get("reason") or "").strip()
reviewer = str(item.get("reviewer") or "").strip()
reviewed = str(item.get("reviewed") or "")
scope = str(item.get("scope") or "").strip()
if item_repository != repository:
raise ValueError("every exception must bind this exact repository")
if not re.fullmatch(r"[0-9a-f]{40}", sha):
raise ValueError("every exception needs one exact 40-character SHA")
if rule not in RULES:
raise ValueError(f"unknown exception rule: {rule}")
if not reason:
raise ValueError(f"{sha[:12]} {rule}: human reason is required")
if not reviewer:
raise ValueError(f"{sha[:12]} {rule}: reviewer identity is required")
if not scope:
raise ValueError(f"{sha[:12]} {rule}: lifetime or epoch scope is required")
try:
date.fromisoformat(reviewed)
except ValueError as error:
raise ValueError(f"{sha[:12]} {rule}: ISO review date is required") from error
allowed[(sha, rule)] = item
return allowed
def records(repo, revision_range):
result = subprocess.run(
["git", "-C", repo, "log", "--format=%H%x1f%s%x1f%b%x1e", revision_range],
capture_output=True, text=True,
)
if result.returncode:
raise RuntimeError(result.stderr.strip() or "git log failed")
for record in result.stdout.split("\x1e"):
fields = record.strip("\r\n").split("\x1f", 2)
if len(fields) == 3:
yield fields
def check(repo, revision_range, allowlist):
errors = []
notes = []
for sha, subject, body in records(repo, revision_range):
for rule, reason in violations(subject.strip(), body.strip()):
exception = allowlist.get((sha.lower(), rule))
if exception:
notes.append(f"{sha[:12]} {rule}: approved: {exception['reason']} (reviewed {exception['reviewed']})")
else:
errors.append(f"{sha[:12]} {rule}: {reason}")
return errors, notes
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--repo", default=".")
parser.add_argument("--range", required=True)
parser.add_argument("--allowlist", required=True)
parser.add_argument("--repository", required=True)
args = parser.parse_args()
try:
errors, notes = check(args.repo, args.range, load_allowlist(args.allowlist, args.repository))
except (OSError, ValueError, RuntimeError, json.JSONDecodeError) as error:
print(f"[commit-voice] {error}", file=sys.stderr)
return 1
for note in notes:
print(f"[commit-voice] {note}")
for error in errors:
print(f"[commit-voice] {error}", file=sys.stderr)
if errors:
return 1
print("[commit-voice] clean")
return 0
if __name__ == "__main__":
sys.exit(main())