oc_chat
Loading...
Searching...
No Matches
plan_fold_gate.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tools/plan_fold_gate.py
4
5"""
6Fold-landing gate for review-hardened plans (built for plan 151).
7
8Purpose
9-------
10What this is — and, honestly, what it is NOT (2026-07-15, reviews 151/R17+R18).
11This is a **region-scoped exact-substring checker**. For each folded finding it
12asserts:
13
14 * NEW — the new mechanism's literal appears INSIDE the finding's executing
15 region (a bounded span between two anchor strings — a step, a Gate,
16 an AC row), **not merely somewhere in the document**. This is the
17 capability whose absence let the v1 gate certify step-landings that
18 were only in prose (Opus R18 O18-P0-1: the `/save` check passed while
19 the T8 step had no `/save`).
20 * GONE — the retired literal and its listed paraphrases appear nowhere outside
21 the sanctioned historical narrative.
22
23It is **NOT** semantic paraphrase detection. The ``gone`` lists are a hand-curated
24exact-substring denylist, case-sensitive; an unforeseen rewording is not caught.
25The v1 tool over-claimed "concept-aware"; that claim is RETRACTED. A deterministic
26checker cannot prove a concept is gone — so a MANDATORY HUMAN CONCEPT AUDIT remains
27part of the fold procedure (plan §7(iii)); this gate is the mechanical floor under
28it, not a replacement for it.
29
30It is also NOT a completeness proof. ``CHECKS`` is reconciled against ``MANIFEST``
31(the review-finding IDs it must cover) so the denominator cannot silently shrink by
32deleting a check — but a finding nobody added to the manifest is still uncovered.
33
34V30 (Sol R25 SOL25-03): the R24 lifecycle literals are the smallest complete OPERATIVE
35contracts (the T8 custody phrase with its ledger-hash equality; the freeze-marker
36baseline comparison) instead of bare tokens — Sol executed two single-leg mutations
37that deleted the safety action while keeping the token, and the V29 checks stayed
38green; both are now pinned regression tests. The three T8 blocked-path surfaces
39(step / Gate / AC 22) are checked per-surface, and the receipt enumerates every
40audited source. Still a phrase floor, not a lifecycle proof.
41
42V31 (operator re-scope directive 2026-07-16 + Fable/Sol R26): the plan's evidence-layer
43dispatch gates were retired by operator directive (threat-matched for a sole-operator,
44local, reversible migration), so SOL25-01/SOL25-01-probe/SOL25-01-vars (the
45authenticated-ledger dispatch gate), TERRA22-04 (the SOL20-04 pre-dispatch commit at
46AC 22), and S23-P1-3 (the retired section-1.0 commit variable) are RETIRED from
47CHECKS+MANIFEST — their anchor text no longer exists in the plan. SOL25-04 is
48RETARGETED to the simple one-commit receipt form of section 7 clause v, and the
49SOL25-02 blocked-path window is WIDENED over the two new close items. New checks:
50F26-01 (the T2 PRESERVE equality oracle), F26-02-step/-gate/-ac22 (the terminal
51artifact manifest blocked-path item, per surface), F26-03 (the rollback-copy and
52DACL-save blocked-path items), R25-01-root (the durable-root declaration — Sol R26
53mutation C, executed false-green at V30, now red), and V31-T8-RECONCILE (the kept T8
54terminal-transition contract at AC 22). The V30 claim that each new check carried a
55single-leg adversarial test was FALSE for five of the nine (Sol R26 SOL26-05); the
56test suite now carries a META-TEST mapping every manifest fid to a named test or the
57documented untested-phrase-floor list, failing on drift.
58
59Usage
60-----
61 python tools/plan_fold_gate.py # gate the committed HEAD blob (authoritative)
62 python tools/plan_fold_gate.py --worktree # gate the working tree (iterating a fold)
63
64Responsibilities
65----------------
66- :class:`Region` — a bounded span of the plan, located between two anchor strings.
67- :class:`Check` — one finding: its ``new`` literal + the ``region`` it must land in,
68 plus whole-document ``gone`` literals.
69- :data:`CHECKS` / :data:`MANIFEST` — the fold set and the review IDs it must cover.
70- :func:`evaluate` — THE single production predicate: returns the problems for one
71 check against the plan text (the tests call this — they do not re-implement it).
72- :func:`read_plan` / :func:`run` / :func:`main`.
73
74Exit codes
75----------
76- 0 — every check passed and the manifest is covered.
77- 1 — a finding did not land at its region, a retired literal survived, or the
78 manifest/checks disagree.
79- 2 — the plan file could not be read.
80
81Diagnostics
82-----------
83Domain: OC_CHAT.MAIN
84Levels:
85 L2 — lifecycle events (the pass/fail tally)
86 L3 — semantic details
87 L4 — deep tracing
88
89Contracts
90---------
91- The committed blob (``git show HEAD:<plan>``) is authoritative; ``--worktree`` is
92 only for iterating a fold before committing.
93- Historical-scope decision (Opus R17 O17-P1-4): ``gone`` targets a retired concept's
94 OPERATIVE phrasing; a dated statement of fact ("renamed from X", "branch Y is merged
95 and deleted") is allowed, which is why ``gone`` lists operative phrasings not bare
96 nouns.
97
98Dependencies
99------------
100- oc_diagnostics (structured debug logging via _dbg)
101"""
102from __future__ import annotations
103
104import argparse
105import subprocess
106import sys
107from dataclasses import dataclass, field
108from pathlib import Path
109
110import oc_diagnostics
111from oc_diagnostics import _dbg
112
113_REPO = Path(__file__).resolve().parent.parent
114PLAN = "oc_chat/plans/completed/151-monorepo-migration.md" # moved by the 2026-07-19 completed/ sweep
115DEFERRED = "tools/151_deferred_sol20_tooling.md"
116# Every source a check may evaluate against. The plan is the default; a CROSS-FILE
117# check names another source (V29 — Sol R24 SOL24-03: the plan-only gate could not see
118# a retired phrase surviving in the deferred-tooling doc — the sibling-leg miss one file over).
119_SOURCES = {"plan": PLAN, "deferred": DEFERRED}
120_REGION_MAX = 20000 # a step/Gate/AC span; wider means the anchor is too loose (Opus R19 O19-P0-1)
121
122
123@dataclass(frozen=True)
124class Region:
125 """A bounded span of the plan, from the line containing ``start`` up to (but not
126 including) the next line containing ``end``. ``end=None`` runs to end-of-document.
127 ``label`` names the surface (step / Gate / AC) for the receipt."""
128
129 label: str
130 start: str
131 end: str | None = None
132
133 def slice(self, text: str) -> str | None:
134 i = text.find(self.start)
135 if i < 0:
136 return None
137 if self.end is None:
138 return text[i:]
139 j = text.find(self.end, i + len(self.start))
140 return text[i:] if j < 0 else text[i:j]
141
142
143@dataclass
144class Check:
145 """One folded finding. ``new`` must appear INSIDE ``region``; ``gone`` literals
146 must be ABSENT from the whole document."""
147
148 fid: str # the review-finding ID (reconciled against MANIFEST)
149 name: str
150 new: str | None = None
151 region: Region | None = None
152 gone: list[str] = field(default_factory=list)
153 source: str = "plan" # which _SOURCES text this check evaluates against (V29 SOL24-03: cross-file)
154
155
156# Executing regions (anchors chosen to be stable substrings of the plan).
157# _T8_CLOSE end tightened at V30 (Sol R25 SOL25-03: the "## " end made it a ~10k-char
158# span — too loose to be an executing site; it now covers the canonical close list only).
159_T8_CLOSE = Region("T8 close order", "close order (ONE canonical list", "Pin the mechanism as")
160_T1_STEP3 = Region("T1 step 3 (fetch)", "OC_MIGRATION_BRANCH=<pinned>", "**⚠️")
161_T2_D4 = Region("T2 D4 step", "D4 — distribution-SUPPLY fix", "D5 — ONE")
162_T0_LEDGER = Region("§2.1 ledger schema", "ledger_schema_version, tranche", "**")
163_S24_ROLLBACK = Region("§2.4 rollback", "rollback of the structural move", "###")
164_T1_REHEARSAL = Region("T1 rehearsal Gate", "the rehearsal runs on a MERGED", "**")
165# _T0_GATE end anchor retargeted at V31: F26-03 renamed the free-space clause to the
166# per-volume budget receipt.
167_T0_GATE = Region("T0 Gate", "each anchor tag (`pre-oc-chat-merge`", "free-space PER-VOLUME BUDGET receipt")
168_T8_MIRROR = Region("T8 terminal transition (canonical close step 6)",
169 "LIFT the freeze (a marker removal, not a failure-capable gate)", "NEVER the artifact root")
170_AC22_ATOMIC = Region("AC row 22 (T8 close)", "| 22 | **T8 (close):**", "| T8 report;")
171# _S10_VARS start widened at V30 so the whole §1.0 block is covered (the old start
172# anchored on the OC_CHAT_DISPATCH_WORKTREE line, which excluded the path roots above it).
173_S10_VARS = Region("§1.0 canonical vars", "ACTIVE_FREEZE_PATH = <dispatch-pinned", "```")
174_T2_GATE = Region("T2 Gate (workspaces row)", "the scm `root`-DEPTH re-derivation recorded", "report-T2.")
175# V30 (Sol R25 SOL25-02/-04): per-surface T8 blocked-path regions and the §7(v) protocol
176# paragraph. (The V30 _T0_DISPATCH region is RETIRED at V31 — the operator re-scope removed
177# the pre-dispatch tooling clause it anchored on.)
178_T8_FAILPATH = Region("T8 step failure path (blocked-path set)",
179 "Failure path: if ANY close gate fails", "write a NON-terminal")
180_T8_GATE_BLOCKED = Region("T8 Gate (blocked-path set)",
181 "non-terminal BLOCKED path taken on any failed close gate",
182 "never `Completed` before the final ledger entry lands")
183_S7_V = Region("§7(v) fold-evidence protocol", "(v) THE MECHANICAL FOLD-LANDING GATE", "| # | Check |")
184# The tag-receipt item inside the ONE canonical T8 blocked-path enumeration — byte-identical
185# at the step, the Gate, and AC row 22 (V30 Sol R25 SOL25-02 added the item to all three;
186# V31 F26-03 ≡ SOL26-06 inserted the rollback-copy + DACL-save items directly after it, so
187# the window is WIDENED over them). The checked window starts inside the tag-receipt item and
188# runs through the items that follow it, so deleting ANY covered item OR moving one past the
189# lock breaks the match. (Windows, not the full 16-item set, are pinned: the strict
190# no-hardcoded-secrets entropy floor caps single literals at 4.5 bits/char — the widened
191# window measures 4.64 as one constant, so it is composed via EXPLICIT `+` from sub-4.5
192# literals (4.17/4.11/4.23/3.73 bits/char; implicit adjacent literals are parse-folded into
193# ONE constant the floor then measures); the full-set byte-identity is diffed mechanically
194# at each fold per the plan's own T8 rule.)
195_BLOCKED_WINDOW = ("`tag_receipt_sha256` re-check**, "
196 + "**independent rollback-copy verified**, "
197 + "**archive+parent DACL `/save` verified**, "
198 + "lock, smoke, intact-check")
199# The terminal-artifact-manifest item (V31 F26-02 ≡ SOL26-02) — its window pins BOTH presence
200# and position (after the final report-hash, before the final ledger-append) at each surface.
201_MANIFEST_WINDOW = ("final report-hash, **`terminal_artifact_manifest` compute + read-back**"
202 ", final ledger-append")
203# AC-row regions for the OPUS22 positive-phrasing legs (V29 — Opus R24-09: the OPUS22 gate
204# checked the positive phrase only in the T2 Gate; a future reworded negative at AC 11 or AC 18
205# would pass. These regions exercise the same legs the human concept audit currently must.)
206_AC11 = Region("AC row 11 (runtime-state boundary)", "| 11 | **Runtime-state boundary", "| 12 |")
207_AC18 = Region("AC row 18 (T2 D5 + settings)", "| 18 | **T2 D5 + settings", "| 19 |")
208
209# The finding manifest: every ID here MUST have a Check, and every Check's fid MUST be here.
210MANIFEST = {
211 "O16-P1-1", "O16-P1-3", "V17-1", "V17-3", "V17-4", "V17-6",
212 "13b", "SOL16-01", "O16-P2-1", "O16-P2-2", "O17-P1-4", "O17-P2-2",
213 "SOL17-01", "SOL17-03",
214 # R18 non-landing cluster (O18-P1-1) — must land at executing sites:
215 "SOL14-07-rollback-tags", "SOL16-03-parent-acl", "D4-gate-ac",
216 "O19-P1-1", "SOL14-04",
217 # R23: every V27 fold landed at ONE leg and left a sibling STALE. These checks make a
218 # future sibling-leg miss FAIL the gate (Terra R23 + Sonnet5 R23). (TERRA22-04 and
219 # S23-P1-3 were RETIRED at V31 with the operator re-scope — the SOL20-04 pre-dispatch
220 # commit gate they anchored on is removed from the plan.)
221 "TERRA23-01", "S23-P1-2", "OPUS22",
222 # R24: the META-PROBLEM fold — generalize the gate to LIFECYCLE + CROSS-FILE misses
223 # (Opus R24 + Sol R24). Each has an adversarial-revert test.
224 "R24-01", "R24-02", "SOL24-01", "SOL24-03", "SOL24-03-status", "OPUS22-AC11", "OPUS22-AC18",
225 # R25 (Opus R25 + Sol R25): operative-contract literals, per-surface blocked-path
226 # checks, and the durable receipt pin. (V30 claimed each carried a single-leg
227 # adversarial test — FALSE for five of the nine, Sol R26 SOL26-05; the meta-test in the
228 # test suite now enforces the mapping. SOL25-01/-probe/-vars — the hard ledger dispatch
229 # gate — were RETIRED at V31 with the operator re-scope; SOL25-04 is RETARGETED to the
230 # simple one-commit §7(v) receipt form.)
231 "R24-02-schema", "R25-01",
232 "SOL25-02-step", "SOL25-02-gate", "SOL25-02-ac22", "SOL25-04",
233 # V31 (operator re-scope 2026-07-16 + Fable/Sol R26 + the executed rehearsal): the
234 # PRESERVE oracle, the terminal-artifact-manifest blocked-path item (per surface), the
235 # rollback-copy/DACL-save items, the durable-root declaration (Sol R26 mutation C), and
236 # the kept T8 terminal-transition contract. Each has a single-leg test (meta-test-mapped).
237 "F26-01", "F26-02-step", "F26-02-gate", "F26-02-ac22", "F26-03",
238 "R25-01-root", "V31-T8-RECONCILE",
239}
240
241CHECKS: list[Check] = [
242 Check("O16-P1-1", "fetch pins the migration branch",
243 new="OC_MIGRATION_BRANCH", region=_T1_STEP3, gone=["main:occ-import"]),
244 Check("SOL17-01", "T1 asserts the POST-filter path + exact tip",
245 new="oc_chat/oc_chat/scripts/py_link_check.py", region=_T1_STEP3,
246 gone=["occ-import:oc_chat/scripts/py_link_check.py"]),
247 Check("O16-P1-3", "session-log against real symbols", new=None,
248 gone=["update_changelog", "open(CHANGELOG_PATH", "core/changelog.py:"]),
249 Check("V17-1", "rehearsal on a MERGED copy of both repos", new="MERGED throwaway copy of BOTH repos",
250 region=_T1_REHEARSAL, gone=["calibrated RED-then-GREEN on the rehearsal clone"]),
251 Check("V17-3", "freeze-lift order fixed at the STEP", new=None,
252 gone=["no evidence write, no failure-capable gate",
253 "LIFT the freeze + write the SEPARATE"]),
254 Check("V17-4", "ledger schema carries terminal fields",
255 new="expected_terminal_transition", region=_T0_LEDGER),
256 Check("V17-6", "DACL /save AT THE T8 STEP", new="/save", region=_T8_CLOSE,
257 gone=[]), # /restore lives in the reversal-record line, a different region; /save at the step is the landing
258 Check("SOL16-03-parent-acl", "archive parent-ACL + independent copy AT THE T8 STEP",
259 new="rollback copy of the archive medium", region=_T8_CLOSE),
260 Check("SOL14-07-rollback-tags", "rollback deletes imported tag refs AT §2.4",
261 new="oc_chat-v*", region=_S24_ROLLBACK),
262 Check("13b", "143-family is SIX, 143h merged", new=None,
263 gone=["the five members", "the 5 members", "all five members"]),
264 Check("SOL16-01", "D4 is a distribution-supply defect at the STEP",
265 new="prove it in a `--no-index` venv", region=_T2_D4,
266 gone=["oc-diagnostics>=2.0.0` (D4)", "D4 — dist-name fix", "dist-name mismatch"]),
267 Check("D4-gate-ac", "D4 Gate/AC no longer gate on the cosmetic spelling grep", new=None,
268 gone=["D4 verified (`pip check`", "hyphenated-form grep-zero",
269 "post-reinstall `python -m pip check` exits 0 AND a grep-zero"]),
270 Check("O16-P2-1", "false dev-count receipt deleted", new=None, gone=["0.8.0.dev894"]),
271 Check("O16-P2-2", "hardcoded tag counts gone", new=None,
272 gone=["EXACTLY the 11", "the 11 renamed", "option_c's own 5", "option_c's own **5**"]),
273 Check("O17-P1-4", "no garbled mechanical-replace corruption", new=None,
274 gone=["<path> the bare-`main`", "unless the confirmed mechanism supersedes static fields"]),
275 Check("O17-P2-2", "pyproject dep-cite re-derived", new=None, gone=["pyproject.toml:41-42"]),
276 Check("O19-P1-1", "T0 Gate verifies peeled target, not existence-only", new="PEELED target", region=_T0_GATE),
277 Check("SOL14-04", "the real terminal command is present at the canonical close step so that replacing it with prose fails the check",
278 new="plan_status set --status Completed --path", region=_T8_MIRROR),
279 Check("SOL17-03", "no active ref to the removed writer", new=None,
280 gone=["changelog.py:6", "CHANGELOG is updated automatically"]),
281 # (TERRA22-04 retired at V31 — the operator re-scope removed the SOL20-04 pre-dispatch
282 # commit gate from AC row 22; the kept T8 contract is guarded by V31-T8-RECONCILE below.)
283 Check("TERRA23-01", "the T0 tag receipt is external + carried by the ledger schema, not a report-T0 self-reference",
284 new="tag_receipt_sha256", region=_T0_LEDGER,
285 gone=["preexisting:false}` is written into report-T0"]),
286 Check("S23-P1-2", "the T8 Gate does not name validate_migration.py as a T0-dispatch check", new=None,
287 gone=["`validate_migration.py` preflights the required-registry mode"]),
288 # (S23-P1-3 retired at V31 — the SOL20_04_FIX_COMMIT variable it declared canonical was
289 # itself removed from §1.0 by the operator re-scope.)
290 Check("OPUS22", "the T2 Gate asserts positive workspaces-row equality, not the retired negative check",
291 new="its `workspaces` identity row asserted to equal the pinned", region=_T2_GATE,
292 gone=["NO `Option_C` literal in the workspaces row"]),
293 # R24 (V29 — Opus R24 + Sol R24): the META-PROBLEM fold. V28's per-finding checks caught
294 # REWORDED-sibling misses but were blind to (a) LIFECYCLE-completeness misses and (b) CROSS-FILE
295 # misses. These generalize the gate so both classes now FAIL mechanically.
296 # V30 (Sol R25 SOL25-03): the two lifecycle literals are now the smallest complete OPERATIVE
297 # contracts — the V29 token forms passed Sol's two executed single-leg mutations (delete the
298 # custody assertion / the baseline comparison while the token survives nearby).
299 Check("R24-01", "lifecycle: the t8 close order performs the tag receipt custody action — the operative contract, not the filename token",
300 new="exists + hash unchanged (== ledger entry 0's `tag_receipt_sha256`",
301 region=_T8_CLOSE),
302 Check("R24-02", "lifecycle: the t8 freeze marker re check names its ledger baseline — the operative comparison, not the field token",
303 new="== ledger entry 0's `freeze_marker_sha256`", region=_T8_CLOSE),
304 Check("R24-02-schema", "lifecycle: the ledger schema carries the freeze marker hash field",
305 new="freeze_marker_sha256", region=_T0_LEDGER),
306 Check("SOL24-01", "the append only chain integrity claim is downgraded to the honest mutable ledger model", new=None,
307 gone=["binds forward via the append-only"]),
308 Check("SOL24-03", "cross file: the retired report t0 receipt model is absent from the deferred tooling doc", new=None,
309 gone=["hash-chained into report-T0"], source="deferred"),
310 Check("SOL24-03-status", "the retired report t0 tag mapping receipt phrasing is gone from the plan status and gate note", new=None,
311 gone=["report-T0 tag mapping receipt", "report-T0 mapping receipt"]),
312 Check("OPUS22-AC11", "ac row 11 asserts the positive workspaces equality not the retired negative",
313 new="ASSERTED to EQUAL the pinned", region=_AC11),
314 Check("OPUS22-AC18", "ac row 18 asserts the positive workspaces equality via the sol22 producer",
315 new="REWRITTEN and ASSERTED == pinned", region=_AC18),
316 # R25 (V30 — Opus R25 + Sol R25).
317 Check("R25-01", "the tag receipt is pinned under the durable artifact root, not at an arbitrary out of worktree path",
318 new="T0_TAG_RECEIPT_PATH = <MIGRATION_ARTIFACT_ROOT>", region=_S10_VARS,
319 gone=["T0_TAG_RECEIPT_PATH = <dispatch-pinned"]),
320 # (SOL25-01/-probe/-vars retired at V31 — the operator re-scope re-classified the
321 # authenticated ledger as tracked post-migration hardening; its dispatch gate, probe,
322 # and §1.0 variable are removed from the plan.)
323 Check("SOL25-02-step", "the t8 step blocked path set carries the tag receipt re check and the new close items",
324 new=_BLOCKED_WINDOW, region=_T8_FAILPATH),
325 Check("SOL25-02-gate", "the t8 gate blocked path set carries the tag receipt re check and the new close items",
326 new=_BLOCKED_WINDOW, region=_T8_GATE_BLOCKED),
327 Check("SOL25-02-ac22", "the ac row 22 blocked path set carries the tag receipt re check and the new close items",
328 new=_BLOCKED_WINDOW, region=_AC22_ATOMIC),
329 Check("SOL25-04", "section 7 clause v is the simple one commit receipt form run against the committed head",
330 new="run `python tools/plan_fold_gate.py` "
331 + "in committed-HEAD mode and confirm the SAME green result", region=_S7_V,
332 gone=["records `C`'s SHA, every audited source's blob hash"]),
333 # V31 (operator re-scope 2026-07-16 + Fable/Sol R26 + the executed rehearsal).
334 Check("F26-01", "the t2 preserve equality oracle is the stripped payload content hash, never the re-export file sha",
335 new="the `exported_at`/`signature`-stripped payload "
336 "content hash MATCH the T0 receipt", region=_T2_GATE,
337 gone=["re-export SHA-256 against T0's receipt",
338 "re-export SHA-256 MATCH the T0 receipt",
339 "re-export SHA matched"]),
340 Check("F26-02-step", "the t8 step blocked path set carries the terminal artifact manifest item in position",
341 new=_MANIFEST_WINDOW, region=_T8_FAILPATH),
342 Check("F26-02-gate", "the t8 gate blocked path set carries the terminal artifact manifest item in position",
343 new=_MANIFEST_WINDOW, region=_T8_GATE_BLOCKED),
344 Check("F26-02-ac22", "the ac row 22 blocked path set carries the terminal artifact manifest item in position",
345 new=_MANIFEST_WINDOW, region=_AC22_ATOMIC),
346 Check("F26-03", "the t8 step blocked path set carries the rollback copy and dacl save items",
347 new="**independent rollback-copy verified**, "
348 "**archive+parent DACL `/save` verified**", region=_T8_FAILPATH),
349 Check("R25-01-root", "the migration artifact root declaration keeps its durable comment (sol r26 mutation c)",
350 new="# the DURABLE terminal-evidence root", region=_S10_VARS),
351 Check("V31-T8-RECONCILE", "ac row 22 carries the kept t8 contract, monorepo cwd both surfaces and manual reconcile",
352 new="documented MANUAL reconcile path on divergence", region=_AC22_ATOMIC),
353]
354
355
356def evaluate(text: str, check: Check) -> list[str]:
357 """THE production predicate. Return the problems for ``check`` against ``text``
358 (empty list == pass). Tests call THIS — they do not re-implement it (SOL18-01)."""
359 problems: list[str] = []
360 if check.new is not None:
361 if check.region is None:
362 problems.append(f"NEW {check.new!r} has no region (a whole-doc NEW is not a landing)")
363 else:
364 span = check.region.slice(text)
365 r = check.region
366 if span is None:
367 problems.append(f"region {r.label!r} not found (start anchor moved)")
368 elif text.count(r.start) != 1:
369 # START-UNIQUENESS (Sol R20 SOL20-03): a start anchor identifies WHICH
370 # region. If it appears more than once, slice() silently picks the first
371 # occurrence and a cross-site copy (correction prose, a diagnosis paragraph)
372 # can stand in for the executing action. Require exactly one match.
373 problems.append(f"region {r.label!r} start anchor {r.start!r} is AMBIGUOUS "
374 f"(appears {text.count(r.start)}x, must be exactly 1) — "
375 "cross-site prose could substitute for the executing action")
376 elif r.end is not None and r.end not in text[text.find(r.start):]:
377 # REGION-INTEGRITY (Opus R19 O19-P0-1): a dead `end` anchor makes slice()
378 # fall through to EOF, silently degrading the region to a whole-tail match.
379 problems.append(f"region {r.label!r} end anchor {r.end!r} DEAD "
380 f"(span ran to EOF, {len(span)} chars) — region-scoping defeated")
381 elif len(span) > _REGION_MAX:
382 problems.append(f"region {r.label!r} span {len(span)} > {_REGION_MAX} "
383 "(anchor too loose to be an executing site)")
384 elif check.new not in span:
385 problems.append(f"NEW {check.new!r} ABSENT from {r.label}")
386 for g in check.gone:
387 if g in text:
388 problems.append(f"RETIRED {g!r} SURVIVES")
389 return problems
390
391
392def _manifest_problems() -> list[str]:
393 """Return problems if CHECKS and MANIFEST have drifted (a required finding uncovered, or a check not in the manifest)."""
394 fids = {c.fid for c in CHECKS}
395 problems = []
396 for missing in sorted(MANIFEST - fids):
397 problems.append(f"manifest ID {missing!r} has NO check (a required finding is uncovered)")
398 for extra in sorted(fids - MANIFEST):
399 problems.append(f"check {extra!r} is NOT in the manifest (denominator drift)")
400 return problems
401
402
403def read_source(rel_path: str, worktree: bool) -> str | None:
404 """Return a source file's text: the committed HEAD blob (authoritative) or the worktree.
405 Generalizes ``read_plan`` so a CROSS-FILE check can gate a second file (V29 SOL24-03)."""
406 func = "read_source"
407 _dbg("OC_CHAT.MAIN", func, f"{rel_path} worktree={worktree}", 4)
408 if worktree:
409 p = _REPO / rel_path
410 return p.read_text(encoding="utf-8") if p.is_file() else None
411 proc = subprocess.run(
412 ["git", "show", f"HEAD:{rel_path}"], cwd=str(_REPO),
413 text=True, capture_output=True, encoding="utf-8",
414 )
415 return proc.stdout if proc.returncode == 0 else None
416
417
418def read_plan(worktree: bool) -> str | None:
419 """Return the plan text (back-compat shim over :func:`read_source`)."""
420 return read_source(PLAN, worktree)
421
422
423def run(worktree: bool) -> int:
424 """Evaluate every check, print the labelled receipt, return the process exit code."""
425 func = "run"
426 texts: dict[str, str] = {}
427 for name, rel in _SOURCES.items():
428 t = read_source(rel, worktree)
429 if not t:
430 print(f"!! could not read {rel}", file=sys.stderr)
431 return 2
432 texts[name] = t
433 src = "WORKTREE" if worktree else "HEAD"
434 print(f"fold landing gate, region scoped and exact substring, at {src}")
435 # Enumerate EVERY audited source (V30 — Sol R25 SOL25-03: the old banner named only
436 # the plan even when the cross-file deferred doc affected the result).
437 for name in sorted(_SOURCES):
438 print(f"audited source {name}: {_SOURCES[name]} at {src}")
439 print("=" * 80)
440 fails = 0
441 for c in CHECKS:
442 problems = evaluate(texts[c.source], c)
443 print(f"[{'PASS' if not problems else 'FAIL'}] {c.fid:24} {c.name}")
444 for p in problems:
445 print(f" - {p}")
446 if problems:
447 fails += 1
448 mprobs = _manifest_problems()
449 for p in mprobs:
450 print(f"[FAIL] MANIFEST {p}")
451 total = len(CHECKS) + (1 if mprobs else 0)
452 fails += 1 if mprobs else 0
453 print("=" * 80)
454 print(f"{total - fails} of {total} checks pass and {fails} failed. the new text is region scoped and the retired text is gone; the human concept audit is still required.")
455 _dbg("OC_CHAT.MAIN", func, f"{total - fails}/{total} pass ({src})", 2)
456 return 1 if fails else 0
457
458
459def main(argv: list[str] | None = None) -> int:
460 """CLI entry point."""
461 func = "main"
462 # Initialize diagnostics before the first _dbg so the CLI run does not emit
463 # "RuntimeWarning: _dbg() called before init()" (Opus R19/R20 O19-P3-1). Streams
464 # and exceptions are NOT intercepted — the gate owns its stdout receipt, and
465 # stream interception swallows summary lines (see the pytest-summary lesson).
466 oc_diagnostics.init(intercept_streams=False, intercept_exceptions=False)
467 _dbg("OC_CHAT.MAIN", func, "parsing args", 4)
468 ap = argparse.ArgumentParser(prog="plan_fold_gate.py", description="Fold-landing gate for plan 151.")
469 ap.add_argument("--worktree", action="store_true",
470 help="gate the working tree instead of the committed HEAD blob")
471 args = ap.parse_args(argv)
472 return run(args.worktree)
473
474
475if __name__ == "__main__":
476 raise SystemExit(main())
str|None slice(self, str text)
list[str] evaluate(str text, Check check)
list[str] _manifest_problems()
int run(bool worktree)
str|None read_plan(bool worktree)
str|None read_source(str rel_path, bool worktree)
int main(list[str]|None argv=None)