#!/usr/bin/env python3 """#390 Slice B revision-mode patch-adoption lint (#424). Block-scoped string checks per repo convention (check_394 pattern) on the prompt surfaces that carry the patch protocol, plus schema example validation. Spec: docs/design/2026-06-10-390-diff-patch-revision-mode- spec.md (§0 Slice B amendment + §3.2-§3.6 + §8.5). Invariants: 1. draft_writer_agent.md carries the patch-output discipline block: patch-not-full-draft, sidecar emission path, copy-hashes-never-compute, the pre-drafting escalation tag, the ` comment before computing" not in word_count_text: fails.append( "invariant 6: word_count_conventions lost the strip-before-" "count rule") return fails def _cli_default_is_the_constant(apply_src: str) -> bool: """True iff the apply script's --touched-ratio-threshold argparse arg has `default=DEFAULT_TOUCHED_RATIO_THRESHOLD`. AST, not string match, so a regression to `default=None` or a re-hardcoded literal is caught even though the constant itself still equals 0.6.""" import ast tree = ast.parse(apply_src) for node in ast.walk(tree): if not (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "add_argument"): continue if not (node.args and isinstance(node.args[0], ast.Constant) and node.args[0].value == "--touched-ratio-threshold"): continue for kw in node.keywords: if kw.arg == "default": return (isinstance(kw.value, ast.Name) and kw.value.id == "DEFAULT_TOUCHED_RATIO_THRESHOLD") return False def check_threshold_lock(spec_text: str, apply_src: str) -> list[str]: """Invariant 7.""" fails: list[str] = [] if DEFAULT_TOUCHED_RATIO_THRESHOLD != 0.6: fails.append( "invariant 7: DEFAULT_TOUCHED_RATIO_THRESHOLD is " f"{DEFAULT_TOUCHED_RATIO_THRESHOLD!r}, the recorded #424 ship " "decision is 0.6 — changing it requires a new spec amendment " "AND updating every 0.6 prose citation this lint guards") if not _cli_default_is_the_constant(apply_src): fails.append( "invariant 7: the --touched-ratio-threshold argparse default is " "not `DEFAULT_TOUCHED_RATIO_THRESHOLD` — a regression to None or " "a re-hardcoded literal would disable/desync the ship-decision " "default while the constant still reads 0.6") fails.extend(check_section_literals( 7, spec_text, AMENDMENT_HEADING, "spec amendment", { "threshold decision": "0.6", "exemption decision": "heading-anchor exemption", "emission decision": "sidecar file", })) return fails def spec_example_patch(spec_text: str) -> dict | None: """First ```json block of spec §3.2, parsed.""" section = spec_text.split("### 3.2 Patch document", 1) if len(section) < 2: return None m = re.search(r"```json\n(.*?)```", section[1], re.DOTALL) if m is None: return None return json.loads(m.group(1)) def check_spec_example(spec_text: str, schema: dict) -> list[str]: """Invariant 8.""" example = spec_example_patch(spec_text) if example is None: return ["invariant 8: spec §3.2 example patch JSON block not found"] try: jsonschema.validate(example, schema) except jsonschema.ValidationError as exc: return [f"invariant 8: spec §3.2 example no longer validates " f"against revision_patch.schema.json: {exc.message}"] return [] def main() -> int: failures: list[str] = [] failures += check_writer(WRITER.read_text(encoding="utf-8")) failures += check_orchestrator(ORCHESTRATOR.read_text(encoding="utf-8")) failures += check_paper_skill(PAPER_SKILL.read_text(encoding="utf-8")) failures += check_schema8(SCHEMAS.read_text(encoding="utf-8")) failures += check_protocol_doc( PROTOCOL.read_text(encoding="utf-8") if PROTOCOL.exists() else None) failures += check_marker_rules( FORMATTER.read_text(encoding="utf-8"), WORD_COUNT.read_text(encoding="utf-8")) spec_text = SPEC.read_text(encoding="utf-8") failures += check_threshold_lock( spec_text, APPLY_SCRIPT.read_text(encoding="utf-8")) failures += check_spec_example( spec_text, json.loads(PATCH_SCHEMA.read_text(encoding="utf-8"))) if failures: print("check_390_revision_patch_discipline: FAIL") for failure in failures: print(f" - {failure}") return 1 print("check_390_revision_patch_discipline: OK (8 invariants)") return 0 if __name__ == "__main__": sys.exit(main())