mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-30 18:56:16 +00:00
Follow-up to PR #10. Addresses comments left by Devin Review. Primary fix (the BUG comment, .pre-commit-config.yaml:86-87): The receipt-required-for-math-content hook used files: '<math-track regex>' with pass_filenames: true. Pre-commit applies that regex to the staged file list BEFORE invoking the hook, so evidence files (receipts under shared-data/artifacts/deepseek_review/, claims.yaml) were stripped from argv. require_math_evidence.py then saw only the math-track files, found no evidence, and exited 1 -- even when proper evidence was committed alongside. The only case that worked was Lean-only commits, because Lean files are dual-classified as both math-track and evidence. Fix: drive the hook from the index instead of argv. * require_math_evidence.py grows a --staged mode that runs 'git diff --cached --name-only' itself, plus a mutex check so --staged, --from-git-diff, and explicit FILES cannot be combined. * .pre-commit-config.yaml hook switches to always_run: true, pass_filenames: false, and 'entry: ... --staged'. The script exits 0 early when no math-track files are staged, so the cost of always_run is negligible. Polish: * claims-registry.schema.json: add required: ["status"] inside each 'if' subschema. Without it, an entry missing 'status' would also spuriously trip the 'then' clauses (review_receipts, lean) before the top-level required catch. Pure error-message cleanup. * validate_claims_registry.py: replace the catch-all re.compile(r'^[A-Za-z]+:') with a closed list of well-known URI schemes (http, https, arxiv, doi, isbn, mailto, urn). Module-name-shaped strings like 'Module:Theorem' will no longer silently bypass the on-disk path check. * validate_claims_registry.py: thread a FormatChecker through the Draft202012Validator so format-keyword behaviour matches validate_deepseek_receipts.py. No-op for today's schema but cheap insurance for the next contributor who adds 'format'. Regression tests: * New scripts/math-first/test_require_math_evidence.py covers ten classification cases plus the actual --staged regression: it spins up a temp git repo, stages a math-track file + a receipt, invokes the script with --staged, and asserts exit 0. Without the fix this case fails, demonstrating the bug end-to-end. * math-check.yml runs the new self-tests in CI. Docs: * docs/math-first-tooling.md: document the --staged contract, why always_run + pass_filenames: false is necessary, and how to run the new self-tests. Co-Authored-By: Allaun Silverfox <bigdataiscoming+9i37y6j2@protonmail.com>
190 lines
6.4 KiB
Python
Executable file
190 lines
6.4 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Validate ``claims.yaml`` against the claims-registry JSON Schema.
|
|
|
|
Usage:
|
|
scripts/math-first/validate_claims_registry.py [PATH]
|
|
|
|
When no PATH is provided, the registry at the repo root (``claims.yaml``) is
|
|
validated. The script enforces:
|
|
|
|
* the YAML parses and conforms to
|
|
``shared-data/schemas/claims-registry.schema.json``;
|
|
* every ``id`` is unique across the registry;
|
|
* every repo-relative path referenced from ``lean``, ``review_receipts``,
|
|
and ``sources`` resolves to a tracked file on disk (external citations
|
|
that do not look like repo paths -- e.g. ``http`` URLs, ``arXiv:...`` --
|
|
are skipped).
|
|
|
|
Exit code:
|
|
0 registry valid.
|
|
1 registry invalid (schema, duplicate id, or missing referenced file).
|
|
2 schema malformed, dependencies missing, or registry file not found.
|
|
|
|
See ``docs/math-first-tooling.md`` for the math-first tooling contract.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
SCHEMA_PATH = REPO_ROOT / "shared-data" / "schemas" / "claims-registry.schema.json"
|
|
DEFAULT_REGISTRY = REPO_ROOT / "claims.yaml"
|
|
|
|
# Anything matching one of these prefixes (case-insensitively) is treated
|
|
# as an external citation rather than a repo-relative path, and is therefore
|
|
# not required to resolve to a file on disk. The list is intentionally
|
|
# closed: matching every ``scheme:`` blob would silently skip path checks
|
|
# for anything that happens to contain a colon (e.g. ``Module:Theorem``),
|
|
# which would hide registry rot.
|
|
_EXTERNAL_PREFIXES: tuple[str, ...] = (
|
|
"http://",
|
|
"https://",
|
|
"arxiv:",
|
|
"doi:",
|
|
"isbn:",
|
|
"mailto:",
|
|
"urn:",
|
|
)
|
|
|
|
|
|
def _load_schema(schema_path: Path) -> dict[str, Any]:
|
|
try:
|
|
from jsonschema import Draft202012Validator
|
|
except ImportError as exc:
|
|
print(
|
|
"error: jsonschema>=4.18 is required (Draft 2020-12). "
|
|
"Install via `uv pip install jsonschema>=4.21 PyYAML`.",
|
|
file=sys.stderr,
|
|
)
|
|
raise SystemExit(2) from exc
|
|
|
|
try:
|
|
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
except FileNotFoundError:
|
|
print(f"error: schema not found at {schema_path}", file=sys.stderr)
|
|
raise SystemExit(2)
|
|
except json.JSONDecodeError as exc:
|
|
print(f"error: schema {schema_path} is not valid JSON: {exc}", file=sys.stderr)
|
|
raise SystemExit(2)
|
|
|
|
Draft202012Validator.check_schema(schema)
|
|
return schema
|
|
|
|
|
|
def _load_registry(registry_path: Path) -> dict[str, Any]:
|
|
try:
|
|
import yaml
|
|
except ImportError as exc:
|
|
print(
|
|
"error: PyYAML is required. Install via `uv pip install PyYAML`.",
|
|
file=sys.stderr,
|
|
)
|
|
raise SystemExit(2) from exc
|
|
|
|
try:
|
|
text = registry_path.read_text(encoding="utf-8")
|
|
except FileNotFoundError:
|
|
print(f"error: registry not found at {registry_path}", file=sys.stderr)
|
|
raise SystemExit(2)
|
|
|
|
data = yaml.safe_load(text)
|
|
if not isinstance(data, dict):
|
|
print(f"error: registry {registry_path} did not parse as a mapping", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
return data
|
|
|
|
|
|
def _is_external(reference: str) -> bool:
|
|
lowered = reference.lower()
|
|
return any(lowered.startswith(prefix) for prefix in _EXTERNAL_PREFIXES)
|
|
|
|
|
|
def _check_path(reference: str) -> tuple[bool, str]:
|
|
if _is_external(reference):
|
|
return True, ""
|
|
if reference.startswith("/"):
|
|
return False, "must be repo-relative (no leading '/')"
|
|
candidate = REPO_ROOT / reference
|
|
if not candidate.exists():
|
|
return False, f"path does not exist on disk: {reference}"
|
|
return True, ""
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
"path",
|
|
nargs="?",
|
|
type=Path,
|
|
default=DEFAULT_REGISTRY,
|
|
help=f"Registry file to validate (default: {DEFAULT_REGISTRY.relative_to(REPO_ROOT)}).",
|
|
)
|
|
parser.add_argument(
|
|
"--schema",
|
|
type=Path,
|
|
default=SCHEMA_PATH,
|
|
help=f"Path to the JSON Schema (default: {SCHEMA_PATH.relative_to(REPO_ROOT)}).",
|
|
)
|
|
args = parser.parse_args(argv)
|
|
|
|
schema = _load_schema(args.schema)
|
|
registry = _load_registry(args.path)
|
|
|
|
from jsonschema import Draft202012Validator, FormatChecker
|
|
|
|
# FormatChecker keeps date-time / uri / etc. behaviour consistent with
|
|
# validate_deepseek_receipts.py. Even though the current claims schema
|
|
# does not declare any ``format`` keywords, threading the checker in
|
|
# avoids a footgun for the next contributor who adds one.
|
|
validator = Draft202012Validator(schema, format_checker=FormatChecker())
|
|
errors = sorted(validator.iter_errors(registry), key=lambda e: list(e.absolute_path))
|
|
if errors:
|
|
print(f"FAIL {args.path}")
|
|
for err in errors:
|
|
location = "/".join(str(p) for p in err.absolute_path) or "<root>"
|
|
print(f" - {location}: {err.message}")
|
|
return 1
|
|
|
|
failures: list[str] = []
|
|
seen_ids: dict[str, int] = {}
|
|
for index, entry in enumerate(registry.get("claims", [])):
|
|
cid = entry.get("id", f"<index {index}>")
|
|
if cid in seen_ids:
|
|
failures.append(
|
|
f"duplicate id '{cid}' (also defined at index {seen_ids[cid]})"
|
|
)
|
|
seen_ids[cid] = index
|
|
|
|
for key in ("lean",):
|
|
value = entry.get(key)
|
|
if not value:
|
|
continue
|
|
# Lean entries may be either a file path or a theorem symbol; only
|
|
# validate the file path form, which contains a `/` or ends in `.lean`.
|
|
if "/" in value or value.endswith(".lean"):
|
|
ok, msg = _check_path(value)
|
|
if not ok:
|
|
failures.append(f"claim '{cid}': {key}: {msg}")
|
|
|
|
for key in ("review_receipts", "sources"):
|
|
for value in entry.get(key, []) or []:
|
|
ok, msg = _check_path(value)
|
|
if not ok:
|
|
failures.append(f"claim '{cid}': {key}: {msg}")
|
|
|
|
if failures:
|
|
print(f"FAIL {args.path}")
|
|
for line in failures:
|
|
print(f" - {line}")
|
|
return 1
|
|
|
|
print(f"OK {args.path} ({len(registry.get('claims', []))} claim(s))")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|