mirror of
https://github.com/allaunthefox/SilverSight.git
synced 2026-07-30 17:16:16 +00:00
ci(gate): add anti-smuggle vacuity check script + CI workflow layer
- scripts/anti_smuggle_check.py: detects vacuous theorem patterns: * := rfl theorem bodies * fun ... => rfl lambda bodies * syntactically identical LHS/RHS in equalities * quantified sums with bound variable renames - Integrated as Layer 4 in existing anti-smuggle CI workflow - Verified: catches the old vacuous YB tautology (fun ... => rfl pattern) - Verified: zero false positives on current main - --ci mode exits non-zero on any finding
This commit is contained in:
parent
2ef30d8a82
commit
f87c78d4e8
2 changed files with 163 additions and 0 deletions
3
.github/workflows/anti-smuggle.yml
vendored
3
.github/workflows/anti-smuggle.yml
vendored
|
|
@ -41,3 +41,6 @@ jobs:
|
|||
|
||||
- name: Layer 3 — CAS/SMT Grounding
|
||||
run: python3 scripts/verify_with_sympy.py
|
||||
|
||||
- name: Layer 4 — Vacuity Gate
|
||||
run: python3 scripts/anti_smuggle_check.py --ci
|
||||
|
|
|
|||
160
scripts/anti_smuggle_check.py
Normal file
160
scripts/anti_smuggle_check.py
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Anti-smuggle CI gate: detect vacuous theorems that pass `lake build`.
|
||||
|
||||
Catches patterns like:
|
||||
- theorem body is `:= rfl` with no prior computation
|
||||
- equality where both sides are syntactically identical (bound var rename)
|
||||
- `:=` followed by a reference to a trivially-true definition
|
||||
- `by` block with only `rfl` or `simp` that does no work
|
||||
|
||||
Usage:
|
||||
python3 scripts/anti_smuggle_check.py # scan formal/
|
||||
python3 scripts/anti_smuggle_check.py --ci # strict mode (exit 1 on any finding)
|
||||
python3 scripts/anti_smuggle_check.py --file foo.lean # single file
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SUSPICIOUS_RFL = re.compile(
|
||||
r'(theorem|lemma)\s+(\w+)\s*[^:]*:\s*([^=]+)=\s*([^=]+)\s*:=\s*rfl\b'
|
||||
)
|
||||
SUSPICIOUS_BY_RFL = re.compile(
|
||||
r'(theorem|lemma)\s+(\w+)\s*[^:]*:\s*([^=]+)=\s*([^=]+)\s*:=\s*by\s+rfl\b'
|
||||
)
|
||||
SUSPICIOUS_RENAME = re.compile(
|
||||
r'(theorem|lemma)\s+(\w+)\s*[^:]*:\s*'
|
||||
r'∀[^,]+,([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),([^,]+)\.\s*'
|
||||
r'\(∑[^:]+\s*=\s*∑[^:]+\)'
|
||||
)
|
||||
|
||||
BARE_RFL_LINE = re.compile(r'^\s*(:=|=>)\s*rfl\s*$')
|
||||
LAMBDA_RFL = re.compile(
|
||||
r'^\s*:=\s*(fun\s+(\w+\s+)+\w+\s*=>\s*rfl|λ\s+(\w+\s+)+\w+\s*=>\s*rfl)\s*$'
|
||||
)
|
||||
EMPTY_SORRY = re.compile(r'^\s*(:=|=>)\s*(by\s+)?sorry\s*$')
|
||||
|
||||
|
||||
def check_file(path: str, ci_mode: bool) -> int:
|
||||
"""Check a single .lean file for smuggle patterns. Returns finding count."""
|
||||
findings = 0
|
||||
try:
|
||||
with open(path, 'r') as f:
|
||||
content = f.read()
|
||||
except Exception as e:
|
||||
print(f" ERROR reading {path}: {e}")
|
||||
return 0
|
||||
|
||||
rel = os.path.relpath(path)
|
||||
|
||||
# 1. Check for `:= rfl` after an equality
|
||||
for m in SUSPICIOUS_RFL.finditer(content):
|
||||
lhs = m.group(3).strip()
|
||||
rhs = m.group(4).strip()
|
||||
if lhs == rhs:
|
||||
findings += 1
|
||||
print(f" [SMUGGLE] {rel}:{line_of(content, m.start())} "
|
||||
f"theorem '{m.group(2)}' is `:= rfl` with identical LHS/RHS '{lhs}'")
|
||||
|
||||
# 2. Check for `:= by rfl`
|
||||
for m in SUSPICIOUS_BY_RFL.finditer(content):
|
||||
lhs = m.group(3).strip()
|
||||
rhs = m.group(4).strip()
|
||||
if lhs == rhs:
|
||||
findings += 1
|
||||
print(f" [SMUGGLE] {rel}:{line_of(content, m.start())} "
|
||||
f"theorem '{m.group(2)}' is `:= by rfl` with identical LHS/RHS")
|
||||
|
||||
# 3. Check for the YB pattern: sums with bound variable rename
|
||||
for m in SUSPICIOUS_RENAME.finditer(content):
|
||||
# This is a specific pattern: ∀ ... (sum ... r ... = sum ... s ...)
|
||||
# It might still be non-trivial if the body does work; flag for review
|
||||
findings += 1
|
||||
print(f" [SMUGGLE] {rel}:{line_of(content, m.start())} "
|
||||
f"theorem '{m.group(2)}' has ∀-quantified equation with sums "
|
||||
f"(possible bound-variable rename)")
|
||||
|
||||
# 4. Check for trivial theorem bodies: `:= rfl`, `:= fun ... => rfl`, `:= by rfl`
|
||||
lines = content.split('\n')
|
||||
for i, line in enumerate(lines):
|
||||
stripped = line.strip()
|
||||
|
||||
# Check single-line `:= rfl`
|
||||
if re.match(r'^\s*:=\s*rfl\s*$', stripped):
|
||||
surrounding = '\n'.join(lines[max(0, i - 3):i + 1])
|
||||
if re.search(r'(theorem|lemma)\s+\w+', surrounding):
|
||||
findings += 1
|
||||
print(f" [SMUGGLE] {rel}:{i + 1} bare `:= rfl` as theorem body")
|
||||
|
||||
# Check `:= fun ... => rfl` (possibly multi-line: `:=` on prev line, `fun ... => rfl` on this line)
|
||||
if re.match(r'^\s*fun\s+(\w+\s+)+\w+\s*=>\s*rfl\s*$', stripped) or \
|
||||
re.match(r'^\s*λ\s+(\w+\s+)+\w+\s*=>\s*rfl\s*$', stripped):
|
||||
surrounding = '\n'.join(lines[max(0, i - 2):i + 1])
|
||||
if re.search(r'(theorem|lemma)\s+\w+|:=\s*$', surrounding, re.MULTILINE):
|
||||
findings += 1
|
||||
print(f" [SMUGGLE] {rel}:{i + 1} `fun ... => rfl` lambda as theorem body")
|
||||
|
||||
if EMPTY_SORRY.match(stripped) and ci_mode:
|
||||
findings += 1
|
||||
print(f" [WARN] {rel}:{i + 1} bare sorry (not an axiom with TODO)")
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def line_of(content: str, pos: int) -> int:
|
||||
"""Return 1-indexed line number for a character position."""
|
||||
return content[:pos].count('\n') + 1
|
||||
|
||||
|
||||
def check_dir(root: str, ci_mode: bool) -> int:
|
||||
"""Recursively check all .lean files under root."""
|
||||
total = 0
|
||||
for dirpath, _, filenames in os.walk(root):
|
||||
for fn in filenames:
|
||||
if fn.endswith('.lean'):
|
||||
total += check_file(os.path.join(dirpath, fn), ci_mode)
|
||||
return total
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Anti-smuggle CI gate for Lean theorem vacuity"
|
||||
)
|
||||
parser.add_argument('paths', nargs='*', default=['formal'],
|
||||
help='Files or directories to check')
|
||||
parser.add_argument('--ci', action='store_true',
|
||||
help='Strict mode: exit 1 on any finding')
|
||||
parser.add_argument('--file', type=str,
|
||||
help='Check a single file')
|
||||
args = parser.parse_args()
|
||||
|
||||
total = 0
|
||||
if args.file:
|
||||
total += check_file(args.file, args.ci)
|
||||
for path in args.paths:
|
||||
if os.path.isfile(path):
|
||||
total += check_file(path, args.ci)
|
||||
elif os.path.isdir(path):
|
||||
total += check_dir(path, args.ci)
|
||||
elif path != 'formal':
|
||||
print(f"WARNING: {path} not found", file=sys.stderr)
|
||||
|
||||
if total == 0:
|
||||
print("Anti-smuggle check: PASSED (no vacuities detected)")
|
||||
return 0
|
||||
else:
|
||||
msg = f"Anti-smuggle check: {total} potential vacuit{'y' if total == 1 else 'ies'} found"
|
||||
if args.ci:
|
||||
print(f"FAIL: {msg}", file=sys.stderr)
|
||||
return 1
|
||||
else:
|
||||
print(f"WARNING: {msg}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Reference in a new issue