mirror of
https://github.com/allaunthefox/SilverSight.git
synced 2026-07-30 17:16:16 +00:00
- 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)
277 lines
7 KiB
Python
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())
|