Research-Stack/scripts/math-first/validate_claims_registry.py
Devin AI c946319be1 Add math-first tooling: receipt schema, claims registry, pre-commit, CI, MCP
Adds automated guardrails so mathematical rigor is enforced by tooling
instead of by convention. See docs/math-first-tooling.md for the full
contract.

Schemas + registry:
- shared-data/schemas/deepseek-review-receipt.schema.json
  Draft 2020-12 schema for the existing ollama_deepseek_review_receipt_v1
  and ollama_deepseek_review_continuation_receipt_v1 receipt formats. Pins
  sha256:<hex> hashes, non-negative token counts, repo-relative POSIX
  paths, and rejects additional fields.
- shared-data/schemas/claims-registry.schema.json
  Schema for claims.yaml. Requires review_receipts when status is
  verified-by-ai and a lean source when status is formally-proven.
- claims.yaml
  Initial registry entry: prime-gap-entropy-collapse (verified-by-ai)
  linked to the two existing receipts under
  shared-data/artifacts/deepseek_review/.

Validators (scripts/math-first/):
- validate_deepseek_receipts.py: validates tracked or passed receipts
  against the JSON Schema; shared by pre-commit and CI.
- test_validate_deepseek_receipts.py: positive + 7 negative fixtures
  asserting exit-code behaviour.
- validate_claims_registry.py: schema check + unique id check + on-disk
  existence check for every referenced repo-relative path.
- require_math_evidence.py: gate that requires a DeepSeek receipt, a
  Lean change, or a claims.yaml update alongside edits to math-track
  surfaces (Lean Semantics kernels, ArithmeticSpec docs, stack
  solidification receipts).

Pre-commit (.pre-commit-config.yaml):
- check-json, check-yaml, end-of-file-fixer, trim trailing whitespace,
  detect-private-key (scoped to math-first files only per AGENTS.md
  Do Not Sweep).
- Local hooks wiring all three math-first validators above.

CI (.github/workflows/math-check.yml):
- validate-schemas: compiles every schema, runs both validators, runs
  the validator self-tests, then re-invokes the canonical Ollama
  emitter in --verify-only mode against every tracked receipt to
  re-check answer_sha256 against the answer-file bytes on disk.
- require-evidence: enforces the math-track evidence rule at PR scope.
- pre-commit: runs all pre-commit hooks against the PR diff so the
  contract holds even for contributors who skip installing hooks
  locally.

MCP (.mcp.json):
- filesystem, sympy, wolfram-alpha, lean, deepseek-review entries
  pointing at off-the-shelf upstream servers and at the canonical
  ollama_deepseek_review_emitter.py. Secrets stay in the runtime env
  (WOLFRAM_ALPHA_APPID, OLLAMA_API_KEY) and are never embedded.

Docs (docs/math-first-tooling.md):
- Philosophy, surfaces, schema reference, registry workflow, hook
  catalogue, CI catalogue, MCP catalogue, end-to-end verify command.

shared-data/schemas/*.schema.json and claims.yaml live under paths the
top-level .gitignore would normally exclude; they are force-added via
git add -f the same way existing promoted receipts under
shared-data/artifacts/deepseek_review/ are tracked (per AGENTS.md).

Co-Authored-By: Allaun Silverfox <bigdataiscoming+9i37y6j2@protonmail.com>
2026-05-12 04:25:52 +00:00

178 lines
5.8 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 re
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 is treated as an external citation
# rather than a repo-relative path, and is therefore not required to resolve
# to a file on disk.
_EXTERNAL_PREFIXES = ("http://", "https://", "arXiv:", "arxiv:", "doi:", "DOI:")
_EXTERNAL_RE = re.compile(r"^[A-Za-z]+:")
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:
if reference.startswith(_EXTERNAL_PREFIXES):
return True
return bool(_EXTERNAL_RE.match(reference))
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
validator = Draft202012Validator(schema)
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())