Files
turnstone/lint_hypothesis.py
Patrick Buckley 4baf6f81c3 docs(hypothesis): clarity pass, GitHub-render fixes, consistency linter (#720)
* docs(hypothesis): clarity pass, GitHub-render fixes, consistency linter

Document (HYPOTHESIS.md):
- split the dense "Formal" definition into labeled subsections
- define the load-bearing terms: certificate (proven witness vs measured
  surrogate) and the controller / plant (= M_W) / shell triad
- corrections: three-way drift split (+ r_env), scope the success/safety
  collapse to absorbing refusal, unify tau*->tau_H and drop the orphaned bare tau
- calibrations: pin the incompressibility conjecture (still conjectural),
  mark the interlingua=certificate identity as figure, soften the two-walls trade
- GitHub math rendering: brace command-subscripts (_\bot -> _{\bot}, etc.) so the
  markdown emphasis parser stops breaking inline math; replace R_\# with R_{\sharp}
  (\# unescapes to a raw # in GitHub math)

Linter (lint_hypothesis.py):
- deterministic consistency checks A-G; G adds an orphan/redundant-declaration
  scan that catches the bare-tau failure mode
- residue guards so tau^star, unbraced _\cmd subscripts, and \# cannot return

* fix(hypothesis): make lint_hypothesis.py pass ruff under py311

- precompute the inline-$ count so no backslash sits inside an f-string
  expression (backslashes in f-strings are 3.12+; the project targets 3.11)
- split the one-line import (E401/I001); open HYPOTHESIS.md via a context manager (SIM115)
2026-06-26 03:57:36 -07:00

247 lines
9.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Consistency linter for HYPOTHESIS.md.
Deterministic checks — no model, no confabulation:
A. delimiter / emphasis balance
B. residue regexes (things prior rounds fixed must not reappear)
C. single-capital-letter collision scan (one letter, two meanings)
D. definition check for the symbols recent rounds introduced
E. γ/ρ role-usage scan (gate=authorize/reject-proposal ; ρ=verify/fold-back/response)
F. display-only symbols (used in $$…$$ but nowhere in prose)
G. orphan / redundant-declaration scan (symbol used once; or two declaration sites)
Path resolves to HYPOTHESIS.md beside this script, or argv[1] if given.
Known benign flags: E flags the γ,ρ symbol-table row; G2 flags τ_H (it legitimately
owns both a stopping-time/filtration statement and its = inf{…} formula).
"""
import os
import re
import sys
PATH = (
sys.argv[1]
if len(sys.argv) > 1
else os.path.join(os.path.dirname(os.path.abspath(__file__)), "HYPOTHESIS.md")
)
with open(PATH, encoding="utf-8") as _f:
T = _f.read()
LINES = T.splitlines()
def lineno(idx): # char index -> 1-based line
return T.count("\n", 0, idx) + 1
def ctx(idx, w=55):
a = max(0, idx - w)
b = min(len(T), idx + w)
return T[a:b].replace("\n", " ")
# math spans (so we can scan symbols in math only)
math_spans = []
for m in re.finditer(r"\$\$.*?\$\$", T, flags=re.S):
math_spans.append((m.start(), m.end()))
for m in re.finditer(r"(?<!\$)\$(?!\$).*?(?<!\$)\$(?!\$)", T, flags=re.S):
math_spans.append((m.start(), m.end()))
def in_math(idx):
return any(a <= idx < b for a, b in math_spans)
print("=" * 70)
print("A. BALANCE")
print("=" * 70)
nomath = re.sub(r"\$[^$]*\$", "", T)
display = T.count("$$")
inline = len(re.findall(r"(?<!\$)\$(?!\$)", T))
print(f" display $$ : {display} even={display % 2 == 0}")
print(f" inline $ : {inline} even={inline % 2 == 0}")
print(f" braces {{ }} : net {T.count('{') - T.count('}')}")
print(f" bold ** : {nomath.count('**')} even={nomath.count('**') % 2 == 0}")
print(
f" italic * : {nomath.replace('**', '').count('*')} even={nomath.replace('**', '').count('*') % 2 == 0}"
)
print("\n" + "=" * 70)
print("B. RESIDUE REGEXES (expect 0 each)")
print("=" * 70)
residue = {
"stray p_{ok}": r"p_\{\\mathrm\{ok\}\}",
"halt/ready leftover": r"halt/ready",
"(I-γP) discount collision": r"\(I-\\gamma P\)",
"B as pushforward dummy": r"M_W\(c, B\)",
"old c_τ-as-output law": r"M_W\(c\) = \\mathrm\{Law\}\(c_\\tau\)",
"R=id ill-typed": r"R=\\mathrm\{id\}",
"Y_⊥ after ⊥∈Y decision": r"\\mathcal\{Y\}_\\bot",
"'terminal sets are'": r"The terminal sets are",
"ρ rejects ⊥ branch": r"what \$\\rho\$ rejects",
"rejection at ρ": r"fail-closed rejection at \$\\rho\$",
"orphan τ^star (unify→τ_H)": r"\\tau\^\\star",
"unbraced _\\cmd subscript (GitHub emphasis hazard)": r"_\\",
"\\# in math (GitHub unescapes → raw #)": r"\\#",
}
for lbl, rx in residue.items():
hits = [lineno(m.start()) for m in re.finditer(rx, T)]
flag = "OK " if not hits else "HIT "
print(f" {flag}{lbl:32} lines={hits}")
print("\n" + "=" * 70)
print("C. SINGLE-CAPITAL COLLISION SCAN (eyeball for two meanings)")
print("=" * 70)
for L in ["G", "U", "P", "N", "R", "V", "F", "D", "K", "T"]:
occ = []
for m in re.finditer(r"(?<![A-Za-z\\_])" + L + r"(?![A-Za-z_])", T):
if in_math(m.start()):
occ.append(m.start())
if occ:
print(f" [{L}] {len(occ)} math occ:")
for i in occ:
print(f" L{lineno(i):>3}: …{ctx(i, 38)}…")
print("\n" + "=" * 70)
print("D. DEFINITION CHECK (symbols recent rounds introduced)")
print("=" * 70)
defs = {
"Π (adversary class)": r"the class \$\\Pi\$ of policies",
"D (divergent set)": r"D=\\\{s:\\mathbb\{E\}_s\[\\tau_H\]=\\infty\\\}",
"μ (measure)": r"reference/sampling measure \$\\mu\$",
"e_0 (no-op response)": r"no-op response \$e_0\\in\\mathcal\{E\}\$",
"Stop (stop set)": r"stop set \$\\mathrm\{Stop\}\$",
"p_succ": r"p_\{\\mathrm\{succ\}\}\(s\)",
"p_safe": r"p_\{\\mathrm\{safe\}\}\(s\)",
"β (RL discount)": r"discount \$\\beta\$",
"A_Y (pushforward set)": r"measurable \$A_Y",
"r (per-step drift)": r"per-step drift \$r\(s\)=",
"z_t triple": r"z_t = \(c_t, b_t, m_t\)",
"μ_0 (initial dist)": r"initial \$s_0 \\sim \\mu_0\$",
"certificate (2-sense)": r"A \*\*certificate\*\* is a \*witness\*",
"controller/plant/shell": r"\*shell : plant :: the part you write",
"r_env (3-way drift)": r"r_\{\\text\{env\}\}",
}
for lbl, rx in defs.items():
found = bool(re.search(rx, T))
print(f" {'OK ' if found else 'MISS'}{lbl}")
print("\n" + "=" * 70)
print("E. γ / ρ ROLE SCAN")
print("=" * 70)
# γ should sit near authorize/gate/reject-proposal/capability/irreversible/before
# ρ should sit near verify/validate-response/fold-back/after
g_bad = re.compile(r"fold[- ]back|folds back", re.I) # γ doing ρ's job
r_bad = re.compile(r"rejects the proposal|authoriz|is the gate|gates ", re.I) # ρ doing γ's job
def scan(sym_rx, label, bad_rx):
flagged = 0
for m in re.finditer(sym_rx, T):
if not in_math(m.start()):
continue
window = T[max(0, m.start() - 15) : m.start() + 70].replace("\n", " ")
if bad_rx.search(window):
flagged += 1
print(f" FLAG {label} L{lineno(m.start())}: …{window}…")
if not flagged:
print(f" OK no {label} usages land in the wrong role-neighborhood")
scan(r"\\gamma", "γ", g_bad)
scan(r"\\rho", "ρ", r_bad)
print("\n" + "=" * 70)
print("F. DISPLAY-ONLY SYMBOLS (in $$…$$, absent from prose)")
print("=" * 70)
disp = " ".join(T[a:b] for a, b in math_spans if T[a : a + 2] == "$$")
prose = re.sub(r"\$\$.*?\$\$", "", T, flags=re.S)
toks = set(re.findall(r"\\[A-Za-z]+(?:_\{[A-Za-z]+\})?|[A-Z]_[A-Za-z]|[A-Za-z]_\\[a-z]+", disp))
suspicious = []
for tk in sorted(toks):
base = tk.split("_")[0]
if base and base not in prose and tk not in prose and len(base) > 1:
suspicious.append(tk)
print(" (heuristic; review only) ", suspicious if suspicious else "none flagged")
print("\n" + "=" * 70)
print("G. ORPHAN / REDUNDANT-DECLARATION SCAN (review only)")
print("=" * 70)
# G1 — a math symbol occurring exactly once is usually a rename residue or a typo
# (a unification can strip a symbol of all but one use). LaTeX operators and
# formatting commands are not symbols, so filter them out. Review, do not trust.
OPS = {
r"\Pr",
r"\sum",
r"\int",
r"\sup",
r"\inf",
r"\infty",
r"\in",
r"\notin",
r"\cap",
r"\cup",
r"\setminus",
r"\subseteq",
r"\subset",
r"\mid",
r"\ge",
r"\le",
r"\sim",
r"\circ",
r"\cdot",
r"\star",
r"\hat",
r"\bar",
r"\to",
r"\Rightarrow",
r"\rightsquigarrow",
r"\longrightarrow",
r"\quad",
r"\qquad",
r"\Big",
r"\big",
r"\mathbb",
r"\mathcal",
r"\mathrm",
r"\mathbf",
r"\text",
}
sym_rx = re.compile(r"\\[A-Za-z]+(?:_\{[^{}]*\}|_[A-Za-z0-9])?")
counts = {}
for a, b in math_spans:
for m in sym_rx.finditer(T[a:b]):
counts[m.group()] = counts.get(m.group(), 0) + 1
singletons = sorted(s for s, c in counts.items() if c == 1 and s.split("_")[0] not in OPS)
print(" G1 singletons (occur once in math, operators filtered — orphan/typo candidates):")
print(" " + (", ".join(singletons) if singletons else "none"))
# G2 — the bare-τ failure mode the τ-unification introduced: a stopping/hitting-time
# symbol carrying BOTH an enumeration declaration (a "…stopping/hitting time…"
# sentence) AND a separate "= \inf\{…}" formula on a *different* line — one of the
# two sites is usually redundant. A formula restated in adjacent prose is benign
# (same kind of site), and so is τ_H, which legitimately owns a filtration statement
# plus its formula. A *newly* enum+formula-split symbol is the smell.
decl_rx = re.compile(
r"hitting times? are|are stopping times|is a stopping time|stopping times? for the"
)
formula_tail = r"\s*=\s*\\inf\\\{" # "= \inf\{" — the hitting/stop-time def, not \infty
tau_syms = [r"\tau", r"\tau_A", r"\tau_H", r"\tau_B", r"\tau_F", r"\tau_{H_{\mathrm{ok}}}"]
print(" G2 stopping/hitting-time family (count | enum-decl lines | formula lines):")
for s in tau_syms:
pat = re.escape(s) + (r"(?![A-Za-z_^{])" if s == r"\tau" else r"(?![A-Za-z0-9])")
occ = list(re.finditer(pat, T))
enum_lines, formula_lines = set(), set()
for m in occ:
ln = lineno(m.start())
line = LINES[ln - 1]
if re.search(pat + formula_tail, line):
formula_lines.add(ln)
if decl_rx.search(line):
enum_lines.add(ln)
split = any(e != f for e in enum_lines for f in formula_lines)
note = " <-- enum + separate formula; eyeball (benign: τ_H)" if split else ""
print(
f" {s:24} count={len(occ):>2} enum={sorted(enum_lines)} formula={sorted(formula_lines)}{note}"
)
print("\nDONE.")