machinechecked 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. machinechecked-0.1.0.dist-info/METADATA +108 -0
  2. machinechecked-0.1.0.dist-info/RECORD +58 -0
  3. machinechecked-0.1.0.dist-info/WHEEL +5 -0
  4. machinechecked-0.1.0.dist-info/entry_points.txt +3 -0
  5. machinechecked-0.1.0.dist-info/licenses/LICENSE +201 -0
  6. machinechecked-0.1.0.dist-info/top_level.txt +1 -0
  7. prooflens/__init__.py +1 -0
  8. prooflens/__main__.py +3 -0
  9. prooflens/assess.py +504 -0
  10. prooflens/audit.py +115 -0
  11. prooflens/bundle.py +70 -0
  12. prooflens/cli.py +542 -0
  13. prooflens/config.py +145 -0
  14. prooflens/data/ProofLensProbe.lean +469 -0
  15. prooflens/data/prooflens.sty +75 -0
  16. prooflens/data/schema/evidence.schema.json +24 -0
  17. prooflens/data/schema/inventory.schema.json +55 -0
  18. prooflens/data/schema/manifest-claim.schema.json +126 -0
  19. prooflens/data/schema/manifest-index.schema.json +128 -0
  20. prooflens/evidence.py +60 -0
  21. prooflens/export.py +227 -0
  22. prooflens/formalization.py +106 -0
  23. prooflens/manifest.py +296 -0
  24. prooflens/pdfclaims.py +129 -0
  25. prooflens/pipeline.py +331 -0
  26. prooflens/probe.py +126 -0
  27. prooflens/promote.py +204 -0
  28. prooflens/review_queue.py +138 -0
  29. prooflens/reviews.py +105 -0
  30. prooflens/schemas.py +19 -0
  31. prooflens/server.py +63 -0
  32. prooflens/simple.py +628 -0
  33. prooflens/sources.py +164 -0
  34. prooflens/tex.py +408 -0
  35. prooflens/util.py +66 -0
  36. prooflens/web/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  37. prooflens/web/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  38. prooflens/web/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  39. prooflens/web/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  40. prooflens/web/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  41. prooflens/web/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  42. prooflens/web/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  43. prooflens/web/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  44. prooflens/web/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  45. prooflens/web/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  46. prooflens/web/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  47. prooflens/web/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  48. prooflens/web/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  49. prooflens/web/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  50. prooflens/web/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  51. prooflens/web/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  52. prooflens/web/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  53. prooflens/web/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  54. prooflens/web/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  55. prooflens/web/assets/index-B2GDDbsW.css +1 -0
  56. prooflens/web/assets/index-DN7uLe7p.js +285 -0
  57. prooflens/web/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
  58. prooflens/web/index.html +14 -0
prooflens/export.py ADDED
@@ -0,0 +1,227 @@
1
+ """The public export.
2
+
3
+ The export is assembled from fields each paper's `publish` settings allow; it is never a copy of
4
+ the private preview with things removed. After assembly every output file is scanned against the
5
+ private inputs (manuscript sources, Lean sources, PDFs not approved), and any match blocks the
6
+ export.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import shutil
13
+ from pathlib import Path
14
+
15
+ from . import evidence as ev
16
+ from . import manifest as mf
17
+ from .config import Config
18
+ from .util import read_json, sha256_bytes, sha256_file, write_json
19
+
20
+ MIN_LINE = 30 # a private source line at least this long must not occur in the export
21
+
22
+
23
+ class ExportError(RuntimeError):
24
+ pass
25
+
26
+
27
+ def _pick(d: dict | None, keys: list[str]) -> dict | None:
28
+ if d is None:
29
+ return None
30
+ return {k: d[k] for k in keys if k in d}
31
+
32
+
33
+ SEG_KEYS = ["text", "segments"]
34
+ DECL_KEYS_LEAN = ["name", "kind", "module", "range", "doc", "signature", "value", "uses",
35
+ "fingerprint", "legacy_hash"]
36
+ DECL_KEYS_MIN = ["name", "kind", "module", "fingerprint", "legacy_hash"]
37
+
38
+
39
+ def public_claim(chunk: dict, pub) -> dict:
40
+ """The fields of a claim passport that `pub` allows."""
41
+ out = _pick(chunk, ["schema", "release", "id", "paper", "number", "title", "kind", "scope",
42
+ "scope_note", "scope_reviewed", "status", "class", "source_matches",
43
+ "source_sha256", "blockers"])
44
+ out["location"] = chunk.get("location") if pub.source_text else None
45
+ out["statement_tex"] = chunk.get("statement_tex") if pub.source_text else None
46
+ a = chunk.get("anchor")
47
+ out["anchor"] = _pick(a, ["id", "env", "number", "label", "begin", "end", "source_sha256"] +
48
+ (["file", "line"] if pub.source_text else [])) if a else None
49
+ clauses = []
50
+ for cl in chunk["clauses"]:
51
+ c = _pick(cl, ["name", "spec", "accept", "refute", "corrects", "witness", "defs", "state",
52
+ "review", "checks", "ladder"])
53
+ c["regimes"] = [_pick(r, ["name", "description", "witness", "discharged"] +
54
+ (["type"] if pub.lean else [])) for r in cl.get("regimes", [])]
55
+ if pub.lean and cl.get("statement"):
56
+ c["statement"] = cl["statement"]
57
+ c["proofs"] = cl.get("proofs")
58
+ c["witness_record"] = cl.get("witness_record")
59
+ else:
60
+ c["statement"] = None
61
+ if "spec_review" in cl:
62
+ c["spec_review"] = _reviews(cl["spec_review"], pub.reviews)
63
+ if "def_reviews" in cl:
64
+ c["def_reviews"] = {n: _pick(r, ["state", "by", "kind"])
65
+ for n, r in cl["def_reviews"].items()}
66
+ if not pub.source_text:
67
+ for rung in c["ladder"]:
68
+ if rung["rung"] == "source" and rung["state"] == "pass":
69
+ rung["detail"] = "located in the compiled paper"
70
+ clauses.append(c)
71
+ out["clauses"] = clauses
72
+ out["decls"] = dict(chunk.get("decls", {}))
73
+ out["findings"] = [_pick(f, ["id", "tier", "code", "subject", "message", "trigger",
74
+ "signal", "dismissed", "dismissal_stale"])
75
+ for f in chunk["findings"]]
76
+ return out
77
+
78
+
79
+ def _reviews(r: dict | None, with_text: bool) -> dict | None:
80
+ if r is None:
81
+ return None
82
+ keys = ["target", "name", "hash", "by", "kind", "provenance"] + (["text"] if with_text else [])
83
+ return {"state": r["state"], "current": [_pick(x, keys) for x in r.get("current", [])],
84
+ "stale": [_pick(x, keys) for x in r.get("stale", [])]}
85
+
86
+
87
+ def private_inputs(cfg: Config) -> tuple[list[Path], set[str]]:
88
+ """Text files whose lines must not appear, and file hashes that must not appear."""
89
+ texts, hashes = [], set()
90
+ for paper in cfg.papers:
91
+ src = cfg.out_dir / "sources" / paper.id
92
+ if not paper.publish.source_text:
93
+ texts += [p for p in src.rglob("*") if p.suffix in (".tex", ".bib", ".sty", ".cls",
94
+ ".bbl", ".md")]
95
+ if not paper.publish.pdf:
96
+ hashes |= {sha256_file(p) for p in src.rglob("*.pdf")}
97
+ built = cfg.out_dir / "site" / "data" / "papers" / f"{paper.id}.pdf"
98
+ if built.exists():
99
+ hashes.add(sha256_file(built))
100
+ if not any(p.publish.lean for p in cfg.papers):
101
+ texts += [p for p in cfg.lean_root.rglob("*.lean") if ".lake" not in p.parts]
102
+ return texts, hashes
103
+
104
+
105
+ def scan(out: Path, texts: list[Path], hashes: set[str]) -> list[str]:
106
+ """Every leak of a private input into the export."""
107
+ lines = {}
108
+ for t in texts:
109
+ for ln in t.read_text(encoding="utf-8", errors="replace").splitlines():
110
+ s = ln.strip()
111
+ if len(s) >= MIN_LINE and not s.startswith(("%", "--", "/-", "import ")):
112
+ lines.setdefault(s, t)
113
+ leaks = []
114
+ for f in sorted(out.rglob("*")):
115
+ if not f.is_file():
116
+ continue
117
+ b = f.read_bytes()
118
+ if sha256_bytes(b) in hashes:
119
+ leaks.append(f"{f.relative_to(out)}: is a private PDF")
120
+ continue
121
+ if f.suffix in (".json", ".html", ".js", ".css", ".txt", ".md", ".svg"):
122
+ text = b.decode("utf-8", errors="replace")
123
+ # JSON escapes; compare against the decoded strings as well.
124
+ hay = text
125
+ if f.suffix == ".json":
126
+ try:
127
+ hay = text + "\n" + "\n".join(_strings(json.loads(text)))
128
+ except json.JSONDecodeError:
129
+ pass
130
+ for s, src in lines.items():
131
+ if s in hay:
132
+ leaks.append(f"{f.relative_to(out)}: contains a line of {src.name}")
133
+ break
134
+ return leaks
135
+
136
+
137
+ def _strings(obj):
138
+ if isinstance(obj, str):
139
+ yield obj
140
+ elif isinstance(obj, dict):
141
+ for v in obj.values():
142
+ yield from _strings(v)
143
+ elif isinstance(obj, list):
144
+ for v in obj:
145
+ yield from _strings(v)
146
+
147
+
148
+ def export(cfg: Config, out: Path, release: str | None = None,
149
+ web_dist: Path | None = None) -> dict:
150
+ rels = sorted((cfg.out_dir / "releases").glob("*.json"), key=lambda p: p.stat().st_mtime)
151
+ if not rels:
152
+ raise ExportError("no release: run `prooflens build` first")
153
+ relrec = read_json(cfg.out_dir / "releases" / f"{release}.json") if release \
154
+ else read_json(rels[-1])
155
+ record = ev.load(cfg.out_dir, relrec["evidence"])
156
+ index, chunks, store = mf.build(record, relrec["release"])
157
+ pubs = {p.id: p.publish for p in cfg.papers}
158
+
159
+ if out.exists():
160
+ shutil.rmtree(out)
161
+ if web_dist and web_dist.exists():
162
+ shutil.copytree(web_dist, out)
163
+ data = out / "data"
164
+ pidx = _pick(index, ["schema", "release", "evidence", "created", "tool", "lean", "summary",
165
+ "classes", "gates", "search"])
166
+ pidx["issues"], pidx["site_url"] = cfg.issues, cfg.site_url
167
+ pidx["project"] = _pick(index["project"], ["name", "commit", "dirty", "toolchain",
168
+ "repository", "repository_private",
169
+ "source_prefix", "source_roots",
170
+ "lake_manifest_sha256", "dependencies"])
171
+ from .pipeline import source_info
172
+ pidx["project"].update(source_info(cfg))
173
+ pidx["papers"] = []
174
+ for p in index["papers"]:
175
+ pub = pubs[p["id"]]
176
+ q = _pick(p, ["id", "title", "mode", "revision", "pdf_sha256", "page_size", "note"])
177
+ q["root"] = p.get("root") if pub.source_text else None
178
+ q["pdf"] = p["pdf"] if pub.pdf else None
179
+ pidx["papers"].append(q)
180
+ if pub.pdf and p["pdf"]:
181
+ src = cfg.out_dir / "site" / "data" / p["pdf"]
182
+ (data / "papers").mkdir(parents=True, exist_ok=True)
183
+ shutil.copyfile(src, data / p["pdf"])
184
+ pidx["findings"] = {"total": index["findings"]["total"],
185
+ "general": [_pick(f, ["id", "tier", "code", "subject", "message"])
186
+ for f in index["findings"]["general"]]}
187
+ pidx["claims"] = [_pick(c, ["id", "paper", "number", "title", "kind", "scope", "status",
188
+ "class", "anchor", "counts", "file"]) for c in index["claims"]]
189
+ write_json(data / "index.json", pidx, indent=None)
190
+ for name, chunk in chunks.items():
191
+ write_json(data / "claims" / name, public_claim(chunk, pubs[chunk["paper"]]),
192
+ indent=None)
193
+ # Declarations are shared between papers: their Lean text and review texts are exported only
194
+ # when every paper allows it. A redacted record is a different record, so it gets its own key.
195
+ lean_ok = all(p.publish.lean for p in cfg.papers)
196
+ rev_ok = all(p.publish.reviews for p in cfg.papers)
197
+ rekey = {}
198
+ for key, d in store.items():
199
+ q = _pick(d, DECL_KEYS_LEAN if lean_ok else DECL_KEYS_MIN)
200
+ q["review"] = _reviews(d.get("review"), rev_ok)
201
+ new = mf.record_key(q)
202
+ rekey[key] = new
203
+ write_json(data / "decls" / f"{new}.json", q, indent=None)
204
+ for name in chunks:
205
+ path = data / "claims" / name
206
+ c = json.loads(path.read_text(encoding="utf-8"))
207
+ c["decls"] = {n: rekey[k] for n, k in c["decls"].items()}
208
+ write_json(path, c, indent=None)
209
+ # The search index, with only what each paper allows.
210
+ srch = mf.search_index(index, chunks)
211
+ pub_search = {"claims": [], "decls": srch["decls"] if lean_ok else []}
212
+ for c in srch["claims"]:
213
+ pub = pubs[c["paper"]]
214
+ pub_search["claims"].append({**c, "statement": c["statement"] if pub.source_text else "",
215
+ "lean": c["lean"] if pub.lean else []})
216
+ write_json(data / "search.json", pub_search, indent=None)
217
+ from . import audit
218
+ arx = [p.source["arxiv"] for p in cfg.papers if p.source.get("arxiv")]
219
+ write_json(out / audit.NAME, audit.descriptor(
220
+ pidx, cfg.lean_root, author=cfg.author,
221
+ paper_url=f"https://arxiv.org/abs/{arx[0]}" if len(cfg.papers) == 1 and arx else None))
222
+ texts, hashes = private_inputs(cfg)
223
+ leaks = scan(out, texts, hashes)
224
+ if leaks:
225
+ shutil.rmtree(out)
226
+ raise ExportError("export blocked; private material found:\n " + "\n ".join(leaks))
227
+ return {"release": relrec["release"], "evidence": relrec["evidence"], "out": out}
@@ -0,0 +1,106 @@
1
+ """Reading a project's `formalization.yaml` (the mathlib-initiative standard).
2
+
3
+ The file is the project's own account of itself: its sources, its main results, how it was made,
4
+ its review status, and an `alignment` between the source's statements and Lean declarations. The
5
+ alignment is free-form in the standard; the shape read here is its suggested one,
6
+ `statements: [{source, lean, module, status}]`, with `lean` a name or a list of names.
7
+
8
+ A mapping read from the file is a link the project's authors declared, shown as such; the
9
+ file's review status is the project's own description of its review, shown as such. Neither
10
+ changes what Lean is checked to establish.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ from pathlib import Path
17
+
18
+ NAMES = ("formalization.yaml", "formalization.yml")
19
+ KINDS = ("theorem", "lemma", "proposition", "corollary", "definition", "remark", "example",
20
+ "conjecture", "claim", "assumption", "hypothesis", "convention")
21
+ REF = re.compile(r"\b(" + "|".join(KINDS) + r")\s+([A-Z]?\d+(?:\.\d+)*[a-z]?)", re.I)
22
+
23
+
24
+ def find(root: Path) -> Path | None:
25
+ return next((root / n for n in NAMES if (root / n).is_file()), None)
26
+
27
+
28
+ def load(root: Path) -> dict | None:
29
+ """The project's formalization.yaml, parsed, or None. A file that cannot be read is
30
+ reported as `{"error": ...}`."""
31
+ p = find(root)
32
+ if p is None:
33
+ return None
34
+ try:
35
+ import yaml
36
+ except ImportError:
37
+ return {"error": "formalization.yaml found, but PyYAML is not installed"}
38
+ try:
39
+ data = yaml.safe_load(p.read_text(encoding="utf-8"))
40
+ except Exception as e: # malformed YAML is the file's problem, not a crash
41
+ return {"error": f"formalization.yaml could not be read: {e}"}
42
+ return data if isinstance(data, dict) else {"error": "formalization.yaml is not a mapping"}
43
+
44
+
45
+ def _tokens(s: str) -> set[str]:
46
+ return {t for t in re.split(r"[^\w]+", (s or "").lower()) if len(t) > 2}
47
+
48
+
49
+ def statements(fy: dict) -> list[dict]:
50
+ """The alignment's statements in the suggested shape: each with the claim it names
51
+ (`kind`, `number`), the words that say which source it is in, and its Lean names."""
52
+ al = fy.get("alignment") if isinstance(fy, dict) else None
53
+ rows = al.get("statements") if isinstance(al, dict) else al if isinstance(al, list) else None
54
+ out = []
55
+ for r in rows or []:
56
+ if not isinstance(r, dict):
57
+ continue
58
+ lean = r.get("lean") or r.get("declaration") or r.get("declarations")
59
+ names = [lean] if isinstance(lean, str) else [n for n in lean or [] if isinstance(n, str)]
60
+ src = str(r.get("source") or "")
61
+ m = REF.search(src)
62
+ if not names or not m:
63
+ continue
64
+ rest = src[m.end():]
65
+ out.append({"source": src, "kind": m.group(1).lower(), "number": m.group(2),
66
+ "hint": _tokens(rest), "lean": names, "status": r.get("status")})
67
+ return out
68
+
69
+
70
+ def formalized_sources(fy: dict) -> list[dict]:
71
+ return [s for s in fy.get("sources") or [] if isinstance(s, dict)
72
+ and s.get("relationship", "formalizes") == "formalizes"]
73
+
74
+
75
+ def links_for(fy: dict, claim_kind: str, claim_number: str, paper_title: str) -> list[dict]:
76
+ """The alignment's statements that name this claim of this paper. When the file formalizes
77
+ several sources, the paper is the source whose title it shares most words with, and a
78
+ statement must name it by a word only that source's title has ("(Euler)")."""
79
+ num = claim_number.split()[-1] if claim_number else ""
80
+ rows = [st for st in statements(fy) if st["kind"] == claim_kind.lower()
81
+ and st["number"] == num]
82
+ srcs = [_tokens(s.get("title", "")) for s in formalized_sources(fy)]
83
+ if len(srcs) <= 1 or not rows:
84
+ return rows
85
+ title = _tokens(paper_title)
86
+ best = max(range(len(srcs)), key=lambda i: len(srcs[i] & title))
87
+ if not srcs[best] & title:
88
+ return []
89
+ own = srcs[best] - set().union(*(t for i, t in enumerate(srcs) if i != best))
90
+ return [st for st in rows if st["hint"] & own]
91
+
92
+
93
+ def summary(fy: dict | None) -> dict | None:
94
+ """What the reader shows about the file: the project's own review status and methods."""
95
+ if not fy:
96
+ return None
97
+ if "error" in fy:
98
+ return {"error": fy["error"]}
99
+ rv = fy.get("review") if isinstance(fy.get("review"), dict) else {}
100
+ auto = fy.get("automation") if isinstance(fy.get("automation"), dict) else {}
101
+ return {"review_status": rv.get("status"), "reviewers": rv.get("reviewers") or [],
102
+ "methods": [m.get("method") for m in auto.get("methods") or []
103
+ if isinstance(m, dict) and m.get("method")],
104
+ "sources": [{"title": s.get("title"), "id": s.get("id")}
105
+ for s in formalized_sources(fy)],
106
+ "version": fy.get("version")}
prooflens/manifest.py ADDED
@@ -0,0 +1,296 @@
1
+ """The presentation manifest, derived from one evidence record.
2
+
3
+ `index.json` lists the papers and claims; `claims/<file>.json` holds one claim with everything
4
+ its passport shows. Nothing here decides a status: statuses and findings are copied from the
5
+ assessment in the evidence, and the evidence ladder is a fixed reading of the clause checks.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from collections import Counter
12
+
13
+ from .assess import line_location, match_anchor
14
+ from .util import content_id, sha256_text
15
+
16
+ SCHEMA = "prooflens/manifest@0"
17
+
18
+
19
+ def claim_file(cid: str) -> str:
20
+ return re.sub(r"[^A-Za-z0-9._-]", "_", cid) + "-" + sha256_text(cid)[:8] + ".json"
21
+
22
+
23
+ def status_class(status: str) -> str:
24
+ """A coarse class for colouring; the status text is always shown verbatim beside it."""
25
+ if status in ("formal: accepted", "definition: reviewed"):
26
+ return "accepted"
27
+ if status.startswith("formal: proved") or status == "definition: author-reviewed":
28
+ return "proved"
29
+ if status.startswith("refuted"):
30
+ return "refuted"
31
+ if status in ("formal: open", "definition: review pending"):
32
+ return "open"
33
+ if status.endswith("malformed"):
34
+ return "malformed"
35
+ if status.startswith("out of scope"):
36
+ return "out-of-scope"
37
+ return "unspecified"
38
+
39
+
40
+ def top_of(anchor: dict) -> tuple[int, float]:
41
+ """Page and top of a located claim: a line height above the baseline of its first line when
42
+ that is recorded, else the position where the environment opened."""
43
+ if anchor.get("head"):
44
+ return anchor["head"][0], round(anchor["head"][2] - 11, 2)
45
+ return anchor["begin"][0], anchor["begin"][2]
46
+
47
+
48
+ def ladder(cl: dict, anchor: dict | None, mode: str | None, src_ok: bool | None) -> list[dict]:
49
+ """The rungs of the evidence ladder of one clause, each with its own state."""
50
+ ch = cl.get("checks", {})
51
+ proof = ch.get("accept") or ch.get("refute")
52
+ rungs = []
53
+ if cl["state"] == "unchecked":
54
+ rungs.append({"rung": "Lean", "state": "none",
55
+ "detail": "the Lean project was not checked in this build"})
56
+ elif cl.get("defs") and not cl.get("spec"):
57
+ rungs.append({"rung": "stated", "state": "pass" if cl["state"] == "stated" else "fail",
58
+ "detail": ", ".join(cl["defs"])})
59
+ else:
60
+ spec = ch.get("spec")
61
+ rungs.append({"rung": "locked statement",
62
+ "state": "none" if not cl.get("spec") else
63
+ ("pass" if spec and spec["ok"] else "fail"),
64
+ "detail": cl.get("spec") or "no proposition written"})
65
+ if proof is None:
66
+ rungs.append({"rung": "exact statement", "state": "none", "detail": "no proof"})
67
+ else:
68
+ type_errs = [e for e in proof["errors"] if not e.startswith("axioms=")]
69
+ rungs.append({"rung": "exact statement",
70
+ "state": "pass" if not type_errs and not proof["sorry"] else "fail",
71
+ "detail": ("a theorem has exactly the negation as its type"
72
+ if ch.get("refute") else
73
+ "a theorem has exactly this proposition as its type"),
74
+ "errors": proof["errors"]})
75
+ rungs.append({"rung": "axioms", "state": "pass" if not any(
76
+ e.startswith("axioms=") for e in proof["errors"]) and not proof["sorry"]
77
+ else "fail", "detail": ", ".join(proof["axioms"]) or "none"})
78
+ rp = ch.get("replay") or {"state": "not-run", "detail": "no Comparator log"}
79
+ rungs.append({"rung": "kernel replay", "state": rp["state"], "detail": rp["detail"]})
80
+ w = ch.get("witness")
81
+ rungs.append({"rung": "witness",
82
+ "state": "n/a" if w is None else ("pass" if w["ok"] else "open"),
83
+ "detail": cl.get("witness") or ("not needed" if w is None else "missing")})
84
+ regs = cl.get("regimes", [])
85
+ rungs.append({"rung": "regimes",
86
+ "state": "n/a" if not regs else
87
+ ("pass" if all(r["discharged"] for r in regs) else "open"),
88
+ "detail": f"{sum(r['discharged'] for r in regs)}/{len(regs)} covered"
89
+ if regs else "none declared"})
90
+ rev = cl.get("review", "unknown")
91
+ kinds = sorted({r["kind"] for r in (cl.get("spec_review") or {}).get("current", [])})
92
+ rungs.append({"rung": "review",
93
+ "state": {"reviewed": "pass", "author-reviewed": "author",
94
+ "pending": "open"}.get(rev, "none"),
95
+ "detail": rev + (f" ({', '.join(kinds)})" if kinds else "")})
96
+ if anchor and anchor.get("kind") == "ambiguous":
97
+ rungs.append({"rung": "source", "state": "open" if src_ok is not False else "fail",
98
+ "detail": f"{anchor['file']}:{anchor['line']} is typeset on pages "
99
+ f"{', '.join(map(str, anchor['pages']))}: ambiguous, not placed "
100
+ "(place it by hand in anchors.json)"})
101
+ return rungs
102
+ rungs.append({"rung": "source",
103
+ "state": "pdf-only" if mode == "pdf" else
104
+ ("none" if anchor is None else ("pass" if src_ok is not False else "fail")),
105
+ "detail": "PDF only: statement not hashed from source" if mode == "pdf" else
106
+ ("not located in the compiled paper" if anchor is None else
107
+ f"{anchor.get('file', '')}:{anchor.get('line', '')}" +
108
+ {"approximate": " (located approximately, by SyncTeX)",
109
+ "manual": " (placed by hand)"}.get(anchor.get("kind"), ""))})
110
+ return rungs
111
+
112
+
113
+ def build(evidence: dict, release: str, display: dict | None = None
114
+ ) -> tuple[dict, dict[str, dict]]:
115
+ """The presentation manifest of `evidence`. `display` (the project's display overlay) gives
116
+ per claim a TeX `title_tex` and a printed `number` to show in place of the inventory's; it
117
+ changes what is shown, not what is assessed."""
118
+ body = evidence["body"]
119
+ shown = (display or {}).get("claims", {})
120
+ clause_tex = {n: v["title_tex"] for n, v in (display or {}).get("clauses", {}).items()
121
+ if v.get("title_tex")}
122
+ inv = body["inventory"]["claims"]
123
+ lean = body.get("lean") or {}
124
+ tex = body.get("tex") or {}
125
+ assessed = {c["id"]: c for c in body["assessment"]["claims"]}
126
+ findings = body["assessment"]["findings"]
127
+ by_subject: dict[str, list] = {}
128
+ for f in findings:
129
+ by_subject.setdefault(f["subject"].split("#")[0], []).append(f)
130
+
131
+ papers = []
132
+ for pid, prov in body["provenance"]["papers"].items():
133
+ t = tex.get(pid, {})
134
+ papers.append({"id": pid, "title": prov.get("title", pid), "mode": prov["mode"],
135
+ "revision": prov.get("revision"), "root": prov.get("root"),
136
+ "pdf": f"papers/{pid}.pdf" if prov.get("pdf_sha256") else None,
137
+ "pdf_sha256": prov.get("pdf_sha256"), "page_size": t.get("page_size"),
138
+ "note": prov.get("note", "")})
139
+
140
+ store = decl_records(body, lean)
141
+ keys = {n: record_key(r) for n, r in store.items()}
142
+ index_claims, chunks, summary = [], {}, {}
143
+ for c in inv:
144
+ a = assessed[c["id"]]
145
+ t = tex.get(c["paper"], {})
146
+ anchor = match_anchor(c, t.get("anchors", [])) if t.get("mode") == "latex" else None
147
+ src_ok = None
148
+ if anchor:
149
+ anchor = {**anchor, "kind": "exact"}
150
+ if c.get("source_sha256") and anchor.get("source_sha256"):
151
+ src_ok = anchor["source_sha256"] == c["source_sha256"]
152
+ elif line_location(c) is not None and t.get("mode") == "latex":
153
+ got = (t.get("lines") or {}).get(c["location"])
154
+ if c.get("source_sha256") and got:
155
+ src_ok = got == c["source_sha256"]
156
+ la = (t.get("line_anchors") or {}).get(c["location"])
157
+ if la and la.get("kind") == "ambiguous":
158
+ path, _, line = c["location"].rpartition(":")
159
+ anchor = {"kind": "ambiguous", "file": path, "line": int(line),
160
+ "pages": la["pages"]}
161
+ elif la and la.get("kind", "approximate") == "approximate":
162
+ path, _, line = c["location"].rpartition(":")
163
+ anchor = {"kind": "approximate", "file": path, "line": int(line),
164
+ "begin": [la["page"], 0, la["y0"]], "end": [la["page"], 0, la["y1"]]}
165
+ manual = (t.get("manual_anchors") or {}).get(c["id"])
166
+ if manual:
167
+ anchor = {**(anchor or {}), "kind": "manual", "note": manual.get("note", ""),
168
+ "by": manual.get("by", ""), "head": None,
169
+ "begin": [manual["page"], 0, manual["y0"]],
170
+ "end": [manual["page"], 0, manual["y1"]]}
171
+ # The title the paper prints (`\\begin{theorem}[Efficiency]`), as TeX; the inventory's
172
+ # own description is kept as `title` and shown only as such.
173
+ heading = (anchor or {}).get("title") if (anchor or {}).get("kind") == "exact" else None
174
+ over = shown.get(c["id"], {})
175
+ number = over.get("number") or c.get("number", "")
176
+ description = over["title_tex"] if "title_tex" in over else c.get("title", "")
177
+ fs = by_subject.get(c["id"], [])
178
+ counts = Counter(f["tier"] for f in fs if not f.get("dismissed"))
179
+ summary.setdefault(c["paper"], Counter())[a["status"]] += 1
180
+ fname = claim_file(c["id"])
181
+ index_claims.append({
182
+ "id": c["id"], "paper": c["paper"], "number": number,
183
+ "title": description, "heading": heading, "kind": c.get("kind", ""),
184
+ "scope": c["scope"], "status": a["status"], "class": status_class(a["status"]),
185
+ "anchor": {"kind": anchor["kind"], "page": top_of(anchor)[0], "y0": top_of(anchor)[1],
186
+ "y1": anchor["end"][2] if anchor.get("end") and
187
+ anchor["end"][0] == anchor["begin"][0] else None,
188
+ "page_end": anchor["end"][0] if anchor.get("end") else None}
189
+ if anchor and anchor.get("begin") else None,
190
+ "unplaced": anchor.get("kind") if anchor and not anchor.get("begin") else None,
191
+ "counts": dict(counts), "file": fname})
192
+ clauses = []
193
+ for cl in a["clauses"]:
194
+ spec = lean.get("specs", {}).get(cl.get("spec") or "")
195
+ proofs = {}
196
+ for kind in ("accept", "refute"):
197
+ if cl.get(kind):
198
+ proofs[kind] = lean.get("proofs", {}).get(f"{kind}:{cl[kind]}:{cl['spec']}")
199
+ wit = lean.get("proofs", {}).get(f"witness:{cl.get('witness')}:") \
200
+ if cl.get("witness") else None
201
+ clauses.append({**cl, "label": clause_tex.get(cl["name"], cl["name"]),
202
+ "corrects_label": clause_tex.get(cl.get("corrects") or "",
203
+ cl.get("corrects")),
204
+ "statement": {
205
+ "binders": spec.get("binders", []), "conclusion": spec.get("conclusion"),
206
+ "axioms": spec.get("axioms", []), "closure": spec.get("closure", []),
207
+ "direct": spec.get("direct", []), "legacy_hash": spec.get("legacy_hash"),
208
+ "fingerprint": spec.get("fingerprint"),
209
+ "decl": lean.get("decls", {}).get(cl["spec"])} if spec else None,
210
+ "proofs": proofs, "witness_record": wit,
211
+ "ladder": ladder(cl, anchor, t.get("mode"), src_ok)})
212
+ names = set()
213
+ for cl in clauses:
214
+ st = cl.get("statement") or {}
215
+ names.update(st.get("closure", []))
216
+ names.update(cl.get("defs", []))
217
+ names.update((cl.get("def_reviews") or {}).keys())
218
+ # A definition's review belongs to the definition (shared store); the clause keeps
219
+ # its state and reviewer.
220
+ cl["def_reviews"] = {n: {"state": r["state"],
221
+ "by": [x["by"] for x in r.get("current", [])],
222
+ "kind": [x["kind"] for x in r.get("current", [])]}
223
+ for n, r in (cl.get("def_reviews") or {}).items()}
224
+ if st:
225
+ st.pop("decl", None)
226
+ decls = {n: keys[n] for n in sorted(names) if n in keys}
227
+ chunks[fname] = {
228
+ "schema": SCHEMA, "release": release, "id": c["id"], "paper": c["paper"],
229
+ "number": number, "number_note": over.get("number_note"), "title": description,
230
+ "heading": heading,
231
+ "kind": c.get("kind", ""), "scope": c["scope"], "scope_note": c.get("scope_note", ""),
232
+ "scope_reviewed": c.get("scope_reviewed"), "location": c.get("location"),
233
+ "source_sha256": c.get("source_sha256"), "status": a["status"],
234
+ "class": status_class(a["status"]), "anchor": anchor, "source_matches": src_ok,
235
+ "statement_tex": (anchor or {}).get("statement"),
236
+ "blockers": [shown_names(b, clause_tex) for b in a.get("blockers", [])],
237
+ "clauses": clauses, "decls": decls, "findings": fs}
238
+
239
+ prov = body["provenance"]
240
+ index = {"schema": SCHEMA, "release": release, "evidence": evidence["id"],
241
+ "created": evidence["created"], "project": prov["project"],
242
+ "lean": prov.get("lean"), "tool": prov["tool"], "papers": papers,
243
+ "summary": {p: dict(s) for p, s in summary.items()},
244
+ "classes": {st: status_class(st) for s in summary.values() for st in s},
245
+ "gates": body["assessment"].get("gates", {}),
246
+ "findings": {"total": dict(Counter(f["tier"] for f in findings
247
+ if not f.get("dismissed"))),
248
+ "general": [f for f in findings
249
+ if f["subject"].split("#")[0] not in
250
+ {c["id"] for c in inv}]},
251
+ "claims": index_claims}
252
+ index["search"] = "data/search.json"
253
+ return index, chunks, {keys[n]: r for n, r in store.items()}
254
+
255
+
256
+ def shown_names(text: str, clause_tex: dict[str, str]) -> str:
257
+ """`text` with the clause names it quotes in their displayed (TeX) form."""
258
+ for n in sorted(clause_tex, key=len, reverse=True):
259
+ if n in text:
260
+ text = text.replace(n, clause_tex[n])
261
+ return text
262
+
263
+
264
+ def search_index(index: dict, chunks: dict) -> dict:
265
+ """What the reader searches: every claim (number, title, id, status, printed statement, Lean
266
+ names of its clauses) and every declaration (with the claims whose closure holds it)."""
267
+ claims, decls = [], {}
268
+ for c in index["claims"]:
269
+ ch = chunks[c["file"]]
270
+ lean = [n for cl in ch["clauses"] for n in (cl.get("spec"), cl.get("accept"),
271
+ cl.get("refute"), *cl.get("defs", [])) if n]
272
+ claims.append({"id": c["id"], "paper": c["paper"], "number": c.get("number", ""),
273
+ "title": c.get("title", ""), "heading": c.get("heading"),
274
+ "status": c["status"],
275
+ "statement": ch.get("statement_tex") or "", "lean": lean})
276
+ for n in ch.get("decls", {}):
277
+ decls.setdefault(n, []).append(c["id"])
278
+ return {"claims": claims, "decls": [{"name": n, "claims": ids} for n, ids in
279
+ sorted(decls.items())]}
280
+
281
+
282
+ def record_key(record: dict) -> str:
283
+ """The key of a declaration record: the first 24 hex digits of the SHA-256 of its canonical
284
+ JSON. The record holds the declaration's name, kind and fingerprint, so different
285
+ declarations never share a key, and a record never changes under its key."""
286
+ return content_id(record)[:24]
287
+
288
+
289
+ def decl_records(body: dict, lean: dict) -> dict[str, dict]:
290
+ """Every declaration once, with its current review."""
291
+ reviews: dict[str, dict] = {}
292
+ for c in body["assessment"]["claims"]:
293
+ for cl in c["clauses"]:
294
+ for n, r in (cl.get("def_reviews") or {}).items():
295
+ reviews.setdefault(n, r)
296
+ return {n: {**d, "review": reviews.get(n)} for n, d in lean.get("decls", {}).items()}