SilverSight/.github/scripts/glossary_lint.py
allaun 4490dc28a7 feat(rrc): bare-minimum RRC refactor into SilverSight
- Move canonical FixedPoint to Core/SilverSight/FixedPoint.lean
- Add SilverSightRRC library: RRC logogram gates, receipt bridge, AVM ISA
- Add AVMIsa.Emit as the sole top-level JSON output boundary
- Add rrc-emit-fixture executable and Python I/O shims
- Update AGENTS.md, glossary, project map, and build baseline

Build: 2981 jobs, 0 errors (lake build)
2026-06-21 09:08:48 -05:00

277 lines
7 KiB
Python

#!/usr/bin/env python3
"""Glossary lint: warn when domain terms are used but not defined.
This script scans SilverSight source, docs, and CI files for terms that look
like project-specific concepts. If a term appears multiple times but is not
listed in docs/GLOSSARY.md (or docs/GLOSSARY_ALLOWLIST.md), it prints a
warning. It is intentionally a warning, not a build failure, to avoid
blocking work-in-progress.
Run from repo root:
python3 .github/scripts/glossary_lint.py
"""
import re
import sys
from collections import Counter
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent
GLOSSARY = ROOT / "docs" / "GLOSSARY.md"
ALLOWLIST = ROOT / "docs" / "GLOSSARY_ALLOWLIST.md"
# Files and globs to scan.
# v1 scans Markdown documentation only; this is where terminology is introduced
# and where LLMs (and humans) most need glossary support. Code-only identifiers
# that never surface in docs are not flagged.
SCAN_GLOBS = [
"*.md",
"docs/**/*.md",
]
# Auto-generated reports from Research Stack are not SilverSight terminology.
SKIP_PATHS = {
"docs/research_stack_usage_graph.md",
"docs/research_stack_porting_candidates.md",
}
# Minimum number of occurrences before a term is reported.
MIN_OCCURRENCES = 2
# Always-allowed words: project names, common acronyms, file formats, etc.
ALWAYS_ALLOWED = {
# Project / repo names
"SilverSight",
"Research Stack",
"Research-Stack",
"GitHub",
# Licenses / formats
"MIT",
"Apache-2.0",
"Apache",
"YAML",
"JSON",
"JSONL",
"SVG",
"PNG",
"CFF",
# Common acronyms
"CI",
"LLM",
"AI",
"DOI",
"arXiv",
"URL",
"API",
"GPU",
"CPU",
"RAM",
"OS",
"UI",
"CLI",
"HTTP",
"HTTPS",
# Meta / workflow
"TODO",
"FIXME",
"README",
"AGENTS.md",
"PORTING_MAP.md",
"CITATION.cff",
"GLOSSARY.md",
"REBASE_RULES.md",
# File extensions / toolchains
".lean",
".py",
".md",
".yml",
".yaml",
".json",
".svg",
".png",
# Lean / math common
"Mathlib",
"Prop",
"Type",
"Sort",
" theorem",
" lemma",
" definition",
" structure",
" inductive",
# Git / GitHub
"push",
"pull request",
"pull_request",
"workflow",
"action",
}
def extract_glossary_terms(path: Path) -> set[str]:
"""Return the set of SilverSight terms defined in the glossary."""
text = path.read_text(encoding="utf-8")
terms: set[str] = set()
for line in text.splitlines():
line = line.strip()
# Table rows of the form | **Term** | ... |
m = re.match(r"^\|\s*\*\*([^*|]+?)\*\*\s*\|", line)
if m:
terms.add(m.group(1).strip())
# Crosswalk first column: | Term | standard | note |
m = re.match(r"^\|\s*\*\*([^*|]+?)\*\*\s*\|", line)
if m:
terms.add(m.group(1).strip())
return terms
def extract_allowlist(path: Path) -> set[str]:
"""Return the set of intentionally unglossary'd terms."""
terms: set[str] = set()
if not path.exists():
return terms
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if line.startswith("- "):
line = line[2:]
terms.add(line.strip())
return terms
TERM_RE = re.compile(
r"""
(?:\*\*([^*|\n]{2,40})\*\*) # **Term** (do not span table cells)
|
(?:`([^`|\n]{2,40})`) # `Term`
""",
re.VERBOSE,
)
# Terms that are clearly code snippets / paths rather than domain terms.
SKIP_RE = re.compile(
r"""
^https?://|
[/.\\]| # file paths or dotted identifiers
^\.| # file extensions or dotted paths
^/|\$|
[=<>(){}\[\]]|
\b\d+\b
""",
re.VERBOSE,
)
# Common words / UI labels / sentence fragments that are not domain terms.
COMMON_WORDS = {
"Input",
"Output",
"Status",
"Maps to",
"Delta",
"Note",
"Notes",
"Principle",
"Principles",
"Rule",
"Rules",
"Boundary",
"Boundaries",
"Verification",
"Grounding",
"Atoms",
"Append",
"Only",
"Not",
"Stop",
"Why the original failed",
"The Complete Variety Isomorphism",
"Python shim OK",
"PORT ONLY IF",
"No `Float` in Lean compute paths",
"Theorem 1a",
"Theorem 1b",
"Theorem 1d",
}
def scan_file(path: Path) -> Counter[str]:
"""Return a Counter of candidate terms found in one file."""
counts: Counter[str] = Counter()
try:
text = path.read_text(encoding="utf-8")
except UnicodeDecodeError:
return counts
for bold, code in TERM_RE.findall(text):
term = (bold or code).strip()
if len(term) < 2 or len(term) > 40:
continue
if SKIP_RE.search(term):
continue
# Strip leading/trailing punctuation
term = term.strip(".,;:!?')\"")
if not term:
continue
if term in COMMON_WORDS:
continue
# Skip all-lowercase phrases (not domain terms) unless they are
# known acronyms already in ALWAYS_ALLOWED.
if term.islower():
continue
counts[term] += 1
return counts
def normalize(term: str) -> str:
"""Normalize for case-insensitive comparison."""
return re.sub(r"\s+", " ", term).lower()
def main() -> int:
glossary = extract_glossary_terms(GLOSSARY)
allowlist = extract_allowlist(ALLOWLIST)
allowed = ALWAYS_ALLOWED | allowlist
allowed_norm = {normalize(t) for t in allowed}
total_counts: Counter[str] = Counter()
for glob in SCAN_GLOBS:
for path in ROOT.glob(glob):
if not path.is_file():
continue
rel = path.relative_to(ROOT).as_posix()
if rel in SKIP_PATHS:
continue
total_counts.update(scan_file(path))
warnings: list[tuple[str, int]] = []
for term, count in total_counts.items():
if count < MIN_OCCURRENCES:
continue
if term in allowed:
continue
if term in glossary:
continue
term_norm = normalize(term)
if term_norm in allowed_norm:
continue
# Also accept if any glossary term normalizes the same way.
if term_norm in {normalize(t) for t in glossary}:
continue
warnings.append((term, count))
warnings.sort(key=lambda x: (-x[1], x[0]))
if warnings:
print(f"⚠️ Glossary lint: {len(warnings)} terms used {MIN_OCCURRENCES}+ times but not defined")
for term, count in warnings:
print(f" '{term}'{count} occurrence(s)")
print()
print("Add genuine terms to docs/GLOSSARY.md or intentional omissions to docs/GLOSSARY_ALLOWLIST.md.")
return 0 # warning only
else:
print("✅ Glossary lint: no undefined repeated terms.")
return 0
if __name__ == "__main__":
sys.exit(main())