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.
- machinechecked-0.1.0.dist-info/METADATA +108 -0
- machinechecked-0.1.0.dist-info/RECORD +58 -0
- machinechecked-0.1.0.dist-info/WHEEL +5 -0
- machinechecked-0.1.0.dist-info/entry_points.txt +3 -0
- machinechecked-0.1.0.dist-info/licenses/LICENSE +201 -0
- machinechecked-0.1.0.dist-info/top_level.txt +1 -0
- prooflens/__init__.py +1 -0
- prooflens/__main__.py +3 -0
- prooflens/assess.py +504 -0
- prooflens/audit.py +115 -0
- prooflens/bundle.py +70 -0
- prooflens/cli.py +542 -0
- prooflens/config.py +145 -0
- prooflens/data/ProofLensProbe.lean +469 -0
- prooflens/data/prooflens.sty +75 -0
- prooflens/data/schema/evidence.schema.json +24 -0
- prooflens/data/schema/inventory.schema.json +55 -0
- prooflens/data/schema/manifest-claim.schema.json +126 -0
- prooflens/data/schema/manifest-index.schema.json +128 -0
- prooflens/evidence.py +60 -0
- prooflens/export.py +227 -0
- prooflens/formalization.py +106 -0
- prooflens/manifest.py +296 -0
- prooflens/pdfclaims.py +129 -0
- prooflens/pipeline.py +331 -0
- prooflens/probe.py +126 -0
- prooflens/promote.py +204 -0
- prooflens/review_queue.py +138 -0
- prooflens/reviews.py +105 -0
- prooflens/schemas.py +19 -0
- prooflens/server.py +63 -0
- prooflens/simple.py +628 -0
- prooflens/sources.py +164 -0
- prooflens/tex.py +408 -0
- prooflens/util.py +66 -0
- prooflens/web/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- prooflens/web/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- prooflens/web/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- prooflens/web/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- prooflens/web/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- prooflens/web/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- prooflens/web/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- prooflens/web/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- prooflens/web/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- prooflens/web/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- prooflens/web/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- prooflens/web/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- prooflens/web/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- prooflens/web/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- prooflens/web/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- prooflens/web/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- prooflens/web/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- prooflens/web/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- prooflens/web/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- prooflens/web/assets/index-B2GDDbsW.css +1 -0
- prooflens/web/assets/index-DN7uLe7p.js +285 -0
- prooflens/web/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
- prooflens/web/index.html +14 -0
prooflens/bundle.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""One self-contained HTML file of a site, readable straight from disk.
|
|
2
|
+
|
|
3
|
+
Browsers refuse `fetch` from pages opened as files, so the bundle carries everything in the page:
|
|
4
|
+
the app's script and styles (fonts as data URIs, WOFF2 only), the pdf.js worker's code, every
|
|
5
|
+
data file of the site in `window.__PROOFLENS__` (keyed by the path the app would fetch), and the
|
|
6
|
+
PDFs, base64, in `window.__PROOFLENS_PDF__`. The data scripts come first, so they are defined
|
|
7
|
+
before the app (a deferred module) runs.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import base64
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _script_safe(text: str) -> str:
|
|
19
|
+
"""Text that cannot close the <script> element it sits in."""
|
|
20
|
+
return text.replace("</", "<\\/")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _inline_css(css: str, assets: Path) -> str:
|
|
24
|
+
def font(m):
|
|
25
|
+
name = m.group(1)
|
|
26
|
+
f = assets / name
|
|
27
|
+
if not f.exists():
|
|
28
|
+
return m.group(0)
|
|
29
|
+
b64 = base64.b64encode(f.read_bytes()).decode()
|
|
30
|
+
return f'url(data:font/woff2;base64,{b64}) format("woff2")'
|
|
31
|
+
# Keep only the WOFF2 source of each font face, inlined.
|
|
32
|
+
css = re.sub(r',\s*url\([^)]*\.(?:woff|ttf)\)\s*format\("(?:woff|truetype)"\)', "", css)
|
|
33
|
+
return re.sub(r'url\((?:\./)?(?:assets/)?([^)]+\.woff2)\)\s*format\("woff2"\)', font, css)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def bundle(site: Path, out: Path, title: str | None = None) -> dict:
|
|
37
|
+
html = (site / "index.html").read_text(encoding="utf-8")
|
|
38
|
+
assets = site / "assets"
|
|
39
|
+
js_src = re.search(r'<script type="module"[^>]*src="\./(assets/[^"]+\.js)"[^>]*></script>', html)
|
|
40
|
+
css_href = re.search(r'<link rel="stylesheet"[^>]*href="\./(assets/[^"]+\.css)"[^>]*>', html)
|
|
41
|
+
if not js_src or not css_href:
|
|
42
|
+
raise RuntimeError("the site's index.html does not reference its script and styles")
|
|
43
|
+
js = (site / js_src.group(1)).read_text(encoding="utf-8")
|
|
44
|
+
css = _inline_css((site / css_href.group(1)).read_text(encoding="utf-8"), assets)
|
|
45
|
+
worker = next(assets.glob("pdf.worker*.mjs"), None)
|
|
46
|
+
|
|
47
|
+
data = {}
|
|
48
|
+
for f in sorted((site / "data").rglob("*.json")):
|
|
49
|
+
data[str(f.relative_to(site))] = json.loads(f.read_text(encoding="utf-8"))
|
|
50
|
+
pdfs = {str(f.relative_to(site)): base64.b64encode(f.read_bytes()).decode()
|
|
51
|
+
for f in sorted((site / "data").rglob("*.pdf"))}
|
|
52
|
+
|
|
53
|
+
head = ["<script>window.__PROOFLENS__ = " +
|
|
54
|
+
_script_safe(json.dumps(data, ensure_ascii=False, separators=(",", ":"))) +
|
|
55
|
+
";</script>",
|
|
56
|
+
"<script>window.__PROOFLENS_PDF__ = " + json.dumps(pdfs) + ";</script>"]
|
|
57
|
+
if worker:
|
|
58
|
+
head.append("<script>window.__PROOFLENS_WORKER__ = " +
|
|
59
|
+
_script_safe(json.dumps(worker.read_text(encoding="utf-8"))) + ";</script>")
|
|
60
|
+
head.append("<style>" + css + "</style>")
|
|
61
|
+
html = html.replace(css_href.group(0), "")
|
|
62
|
+
html = html.replace(js_src.group(0), "")
|
|
63
|
+
html = html.replace("</head>", "\n".join(head) + "\n</head>", 1)
|
|
64
|
+
html = html.replace("</body>", '<script type="module">' + _script_safe(js) +
|
|
65
|
+
"</script>\n</body>", 1)
|
|
66
|
+
if title:
|
|
67
|
+
html = re.sub(r"<title>.*?</title>", f"<title>{title}</title>", html, count=1)
|
|
68
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
out.write_text(html, encoding="utf-8")
|
|
70
|
+
return {"out": out, "size": out.stat().st_size, "files": len(data), "pdfs": len(pdfs)}
|
prooflens/cli.py
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
"""`machinechecked`: read a paper beside its Lean formalisation.
|
|
2
|
+
|
|
3
|
+
Everyday commands, run from anywhere inside a Lean project:
|
|
4
|
+
|
|
5
|
+
machinechecked view [paper] open the reader (the paper is found if not given)
|
|
6
|
+
machinechecked build [paper] write the reader as one HTML file to share
|
|
7
|
+
machinechecked check [paper] report what the Lean side establishes, for CI
|
|
8
|
+
|
|
9
|
+
A project with a `machinechecked.toml` (an acceptance layer: locked statements, reviews) gets
|
|
10
|
+
the full assessment; any other project gets the zero-configuration view, whose only file in the
|
|
11
|
+
repository is `machinechecked.links.json`. The other commands are for that full assessment.
|
|
12
|
+
`prooflens` is another name for the same command.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import functools
|
|
19
|
+
import http.server
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from . import config as cfgmod
|
|
26
|
+
from .schemas import validate
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _web_dist(arg: str | None) -> Path | None:
|
|
30
|
+
"""The built web app: given with --web, else the one shipped in the package."""
|
|
31
|
+
if arg:
|
|
32
|
+
return Path(arg).resolve()
|
|
33
|
+
shipped = Path(__file__).resolve().parent / "web"
|
|
34
|
+
return shipped if (shipped / "index.html").exists() else None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
MISSING = {
|
|
38
|
+
"lake": "install elan (https://github.com/leanprover/elan) and put ~/.elan/bin on PATH",
|
|
39
|
+
"latexmk": "install TeX Live with latexmk, or point ProofLens at the paper's PDF",
|
|
40
|
+
"synctex": "install TeX Live (it provides synctex)",
|
|
41
|
+
"pdftohtml": "install poppler (poppler-utils) to read a PDF, or give the paper's LaTeX source",
|
|
42
|
+
"pdfinfo": "install poppler (poppler-utils) to read a PDF, or give the paper's LaTeX source",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# -- mode selection ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
def _found_config() -> Path | None:
|
|
49
|
+
return next((Path(n) for n in cfgmod.CONFIG_NAMES if Path(n).exists()), None)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _config_arg(a) -> Path:
|
|
53
|
+
"""The configuration of the full assessment: given with -c, else the one in the current
|
|
54
|
+
directory."""
|
|
55
|
+
c = getattr(a, "config", None) or _found_config()
|
|
56
|
+
if c is None:
|
|
57
|
+
sys.exit("no machinechecked.toml here (pass one with -c)")
|
|
58
|
+
return Path(c)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _assurance_config(a) -> Path | None:
|
|
62
|
+
"""The configuration that selects the full assessment: given with -c, or in the current
|
|
63
|
+
directory."""
|
|
64
|
+
if getattr(a, "config", None):
|
|
65
|
+
return Path(a.config)
|
|
66
|
+
return _found_config()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _simple_target(a):
|
|
70
|
+
from .simple import find_lake_root, find_papers
|
|
71
|
+
root = find_lake_root(Path.cwd().resolve())
|
|
72
|
+
if root is None:
|
|
73
|
+
sys.exit("no Lean project here: run machinechecked inside a directory with a lakefile")
|
|
74
|
+
if a.paper and re.match(r"https?://", a.paper):
|
|
75
|
+
from . import formalization as fz
|
|
76
|
+
fy = fz.load(root) or {}
|
|
77
|
+
title = next((s.get("title") for s in fz.formalized_sources(fy) if "error" not in fy
|
|
78
|
+
and s.get("id") == a.paper), None)
|
|
79
|
+
paper = _fetch_paper(root, a.paper, title)
|
|
80
|
+
elif a.paper:
|
|
81
|
+
paper = Path(a.paper).resolve()
|
|
82
|
+
if not paper.exists():
|
|
83
|
+
sys.exit(f"{a.paper}: no such file")
|
|
84
|
+
else:
|
|
85
|
+
found = find_papers(root)
|
|
86
|
+
if not found:
|
|
87
|
+
from . import formalization as fz
|
|
88
|
+
fy = fz.load(root) or {}
|
|
89
|
+
urls = [s for s in fz.formalized_sources(fy) if "error" not in fy
|
|
90
|
+
and re.match(r"https?://", str(s.get("id") or ""))]
|
|
91
|
+
if not urls:
|
|
92
|
+
sys.exit("no paper found (no .tex with \\documentclass, no .pdf): pass its path "
|
|
93
|
+
"or URL")
|
|
94
|
+
print("The paper is not in the repository; its formalization.yaml names:")
|
|
95
|
+
for i, s in enumerate(urls, 1):
|
|
96
|
+
print(f" {i}. {s.get('title') or s['id']}\n {s['id']}")
|
|
97
|
+
if len(urls) == 1:
|
|
98
|
+
k = "1"
|
|
99
|
+
elif not sys.stdin.isatty():
|
|
100
|
+
sys.exit("pass the one to use: machinechecked view <URL>")
|
|
101
|
+
else:
|
|
102
|
+
k = input("Which one? ")
|
|
103
|
+
try:
|
|
104
|
+
s = urls[int(k) - 1]
|
|
105
|
+
except (ValueError, IndexError):
|
|
106
|
+
sys.exit("no paper chosen")
|
|
107
|
+
return root, _fetch_paper(root, s["id"], s.get("title"))
|
|
108
|
+
if len(found) > 1:
|
|
109
|
+
print("Several papers found:")
|
|
110
|
+
for i, f in enumerate(found, 1):
|
|
111
|
+
print(f" {i}. {f.relative_to(root)}")
|
|
112
|
+
if not sys.stdin.isatty():
|
|
113
|
+
sys.exit("pass the one to use: machinechecked view <paper>")
|
|
114
|
+
k = input("Which one? ")
|
|
115
|
+
try:
|
|
116
|
+
paper = found[int(k) - 1]
|
|
117
|
+
except (ValueError, IndexError):
|
|
118
|
+
sys.exit("no paper chosen")
|
|
119
|
+
else:
|
|
120
|
+
paper = found[0]
|
|
121
|
+
try:
|
|
122
|
+
paper.relative_to(root)
|
|
123
|
+
except ValueError:
|
|
124
|
+
sys.exit(f"{paper} is not inside the Lean project {root}")
|
|
125
|
+
return root, paper
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _settings(a, root: Path | None):
|
|
129
|
+
"""Record --issues and --site-url: in the links file (zero configuration); a full
|
|
130
|
+
assessment sets them in its configuration."""
|
|
131
|
+
issues, url = getattr(a, "issues", None), getattr(a, "site_url", None)
|
|
132
|
+
author = getattr(a, "author", None)
|
|
133
|
+
private = getattr(a, "private_repository", None)
|
|
134
|
+
if not issues and not url and not author and private is None:
|
|
135
|
+
return
|
|
136
|
+
if root is None:
|
|
137
|
+
sys.exit("set `issues`, `site_url`, `author` and `repository_private` under [project] in "
|
|
138
|
+
"machinechecked.toml")
|
|
139
|
+
from .simple import save_settings
|
|
140
|
+
try:
|
|
141
|
+
save_settings(root, issues=cfgmod.issue_repo(issues, "--issues") if issues else None,
|
|
142
|
+
site_url=url, author=author, repository_private=private)
|
|
143
|
+
except cfgmod.ConfigError as e:
|
|
144
|
+
sys.exit(str(e))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _fetch_paper(root: Path, url: str, title: str | None = None) -> Path:
|
|
148
|
+
"""A paper given by URL, downloaded once into the working directory (never into the
|
|
149
|
+
repository), with its address and title beside it."""
|
|
150
|
+
import hashlib
|
|
151
|
+
import urllib.request
|
|
152
|
+
d = root / cfgmod.work_dir_name(root) / "papers"
|
|
153
|
+
name = re.sub(r"[^\w.-]+", "_", url.rstrip("/").rsplit("/", 1)[-1]) or "paper"
|
|
154
|
+
if not name.lower().endswith(".pdf"):
|
|
155
|
+
name += ".pdf"
|
|
156
|
+
out = d / f"{hashlib.sha256(url.encode()).hexdigest()[:10]}-{name}"
|
|
157
|
+
if not out.exists():
|
|
158
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
print(f"downloading {url}")
|
|
160
|
+
try:
|
|
161
|
+
with urllib.request.urlopen(url, timeout=120) as r:
|
|
162
|
+
data = r.read(200_000_001)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
sys.exit(f"could not download {url}: {e}")
|
|
165
|
+
if len(data) > 200_000_000 or not data.startswith(b"%PDF"):
|
|
166
|
+
sys.exit(f"{url} is not a PDF (or is larger than 200 MB)")
|
|
167
|
+
out.write_bytes(data)
|
|
168
|
+
out.with_suffix(".json").write_text(json.dumps({"url": url, "title": title}))
|
|
169
|
+
return out
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _simple_site(root: Path, paper: Path, web: str | None):
|
|
173
|
+
from .simple import build_view, write_view_site
|
|
174
|
+
out = root / cfgmod.work_dir_name(root)
|
|
175
|
+
view = build_view(root, paper, out, log=lambda m: print(" " + m))
|
|
176
|
+
site = out / "site"
|
|
177
|
+
index = write_view_site(view, site, _web_dist(web), root)
|
|
178
|
+
return site, index, view
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _print_simple(index):
|
|
182
|
+
for n in index.get("notes", []):
|
|
183
|
+
print(f"note: {n}")
|
|
184
|
+
for s, k in sorted(index["summary"]["P"].items()):
|
|
185
|
+
print(f" {k:4d} {s}")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# -- everyday commands ------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
def cmd_view(a):
|
|
191
|
+
if _assurance_config(a):
|
|
192
|
+
from .pipeline import build
|
|
193
|
+
from .server import serve
|
|
194
|
+
_settings(a, None)
|
|
195
|
+
cfg = cfgmod.load(_assurance_config(a))
|
|
196
|
+
try:
|
|
197
|
+
res = build(cfg, web_dist=_web_dist(a.web), reuse_lean=True)
|
|
198
|
+
except RuntimeError:
|
|
199
|
+
res = build(cfg, web_dist=_web_dist(a.web))
|
|
200
|
+
serve(res["site"], a.port, None, not a.no_browser)
|
|
201
|
+
return
|
|
202
|
+
from .server import serve
|
|
203
|
+
from .simple import save_link
|
|
204
|
+
root, paper = _simple_target(a)
|
|
205
|
+
_settings(a, root)
|
|
206
|
+
print(f"MachineChecked: {paper.relative_to(root)} in {root.name}")
|
|
207
|
+
site, index, view = _simple_site(root, paper, a.web)
|
|
208
|
+
_print_simple(index)
|
|
209
|
+
key = view["body"]["paper"]
|
|
210
|
+
|
|
211
|
+
def on_link(req):
|
|
212
|
+
save_link(root, key, req["key"], req.get("lean"), req.get("reject"))
|
|
213
|
+
_simple_site(root, paper, a.web)
|
|
214
|
+
|
|
215
|
+
serve(site, a.port, on_link, not a.no_browser)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def cmd_build_simple(a):
|
|
219
|
+
from .bundle import bundle
|
|
220
|
+
root, paper = _simple_target(a)
|
|
221
|
+
_settings(a, root)
|
|
222
|
+
site, index, _ = _simple_site(root, paper, a.web)
|
|
223
|
+
_print_simple(index)
|
|
224
|
+
if a.site:
|
|
225
|
+
import shutil
|
|
226
|
+
dest = Path(a.site).resolve()
|
|
227
|
+
if dest == root.resolve() or root.resolve().is_relative_to(dest):
|
|
228
|
+
sys.exit(f"--site {a.site}: choose a new directory, not the project")
|
|
229
|
+
if dest.exists():
|
|
230
|
+
if not (dest / "audit.json").exists() and any(dest.iterdir()):
|
|
231
|
+
sys.exit(f"--site {a.site}: not empty and not a previous site; choose another")
|
|
232
|
+
shutil.rmtree(dest)
|
|
233
|
+
shutil.copytree(site, dest)
|
|
234
|
+
print(f"wrote the reader to {dest}: publish this directory as it is "
|
|
235
|
+
"(audit.json describes it)")
|
|
236
|
+
return
|
|
237
|
+
out = Path(a.out or (root / "MachineChecked.html")).resolve()
|
|
238
|
+
res = bundle(site, out, f"MachineChecked — {index['papers'][0]['title']}")
|
|
239
|
+
print(f"wrote {res['out']} ({res['size'] / 1e6:.1f} MB): open it in a browser")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def cmd_check(a):
|
|
243
|
+
if getattr(a, "json", False):
|
|
244
|
+
return _check_json(a)
|
|
245
|
+
if _assurance_config(a):
|
|
246
|
+
a.config = str(_assurance_config(a))
|
|
247
|
+
a.release = None; a.skip_lean = a.skip_tex = False; a.lake = "lake"
|
|
248
|
+
a.no_lean_build = False; a.reuse_lean = False; a.fail_on_violation = True
|
|
249
|
+
return cmd_build(a)
|
|
250
|
+
root, paper = _simple_target(a)
|
|
251
|
+
site, index, view = _simple_site(root, paper, a.web)
|
|
252
|
+
_print_simple(index)
|
|
253
|
+
bad = [(c["number"], l["name"], l["status"]) for c in view["body"]["claims"]
|
|
254
|
+
for l in c["links"] if l["class"] != "proved"]
|
|
255
|
+
for b in bad:
|
|
256
|
+
print(f"FAIL {b[0]} -> {b[1]}: {b[2]}")
|
|
257
|
+
if bad:
|
|
258
|
+
sys.exit(1)
|
|
259
|
+
n = sum(len(c["links"]) for c in view["body"]["claims"])
|
|
260
|
+
if not n:
|
|
261
|
+
print("no claim is linked yet: confirm links in `machinechecked view`")
|
|
262
|
+
return
|
|
263
|
+
print(f"all {n} linked declaration(s) are checked by Lean (whether each says what the paper "
|
|
264
|
+
"says is not reviewed by this check)")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
CHECK_NOTE = ("Statuses say what Lean establishes about the linked declarations. Whether a Lean "
|
|
268
|
+
"statement says what the paper says is not checked here; that takes a reviewer.")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _check_json(a):
|
|
272
|
+
"""`check --json`: every claim with its status and links, as one JSON document on stdout
|
|
273
|
+
(progress goes to stderr). Exit 1 if a linked declaration is missing or not proved."""
|
|
274
|
+
import contextlib
|
|
275
|
+
import json
|
|
276
|
+
with contextlib.redirect_stdout(sys.stderr):
|
|
277
|
+
if _assurance_config(a):
|
|
278
|
+
from .pipeline import build
|
|
279
|
+
cfg = cfgmod.load(_assurance_config(a))
|
|
280
|
+
idx = build(cfg, web_dist=_web_dist(a.web))["index"]
|
|
281
|
+
claims = [{"id": c["id"], "paper": c["paper"], "number": c.get("number"),
|
|
282
|
+
"title": c.get("title"), "status": c["status"], "class": c["class"]}
|
|
283
|
+
for c in idx["claims"]]
|
|
284
|
+
out = {"mode": "full", "release": idx["release"],
|
|
285
|
+
"project": idx.get("project", {}).get("commit"),
|
|
286
|
+
"findings": idx["findings"]["total"]}
|
|
287
|
+
failed = bool(idx["findings"]["total"].get("violation"))
|
|
288
|
+
else:
|
|
289
|
+
root, paper = _simple_target(a)
|
|
290
|
+
site, idx, view = _simple_site(root, paper, a.web)
|
|
291
|
+
body = view["body"]
|
|
292
|
+
claims = [{"id": c["id"], "number": c["number"], "title": c["title"],
|
|
293
|
+
"status": c["status"], "class": c["class"],
|
|
294
|
+
"links": [{"name": l["name"], "status": l["status"], "class": l["class"],
|
|
295
|
+
"source": l.get("source")} for l in c["links"]],
|
|
296
|
+
"suggestions": [{"name": x["name"], "why": x.get("why")}
|
|
297
|
+
for x in c.get("suggestions", [])]}
|
|
298
|
+
for c in body["claims"]]
|
|
299
|
+
out = {"mode": "simple", "paper": body["paper"], "title": body["title"],
|
|
300
|
+
"release": idx["release"], "commit": body.get("commit"),
|
|
301
|
+
"modules": {"built": body["project"]["built"],
|
|
302
|
+
"not_built": body["project"]["unbuilt"]},
|
|
303
|
+
"notes": body["notes"]}
|
|
304
|
+
failed = any(l["class"] != "proved" for c in claims for l in c["links"])
|
|
305
|
+
summary = {}
|
|
306
|
+
for c in claims:
|
|
307
|
+
summary[c["status"]] = summary.get(c["status"], 0) + 1
|
|
308
|
+
print(json.dumps({"schema": "machinechecked/check@1", **out, "summary": summary,
|
|
309
|
+
"claims": claims, "ok": not failed, "note": CHECK_NOTE}, indent=1))
|
|
310
|
+
if failed:
|
|
311
|
+
sys.exit(1)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def cmd_build(a):
|
|
315
|
+
if not _assurance_config(a):
|
|
316
|
+
return cmd_build_simple(a)
|
|
317
|
+
from .pipeline import build
|
|
318
|
+
cfg = cfgmod.load(_config_arg(a))
|
|
319
|
+
res = build(cfg, release=a.release, skip_lean=a.skip_lean, skip_tex=a.skip_tex, lake=a.lake,
|
|
320
|
+
web_dist=_web_dist(a.web), lean_build=not a.no_lean_build,
|
|
321
|
+
reuse_lean=a.reuse_lean)
|
|
322
|
+
idx = res["index"]
|
|
323
|
+
for pid, s in idx["summary"].items():
|
|
324
|
+
print(f"paper {pid}:")
|
|
325
|
+
for k, v in sorted(s.items()):
|
|
326
|
+
print(f" {v:4d} {k}")
|
|
327
|
+
print("findings:", ", ".join(f"{v} {k}" for k, v in sorted(idx["findings"]["total"].items()))
|
|
328
|
+
or "none")
|
|
329
|
+
if a.fail_on_violation and idx["findings"]["total"].get("violation"):
|
|
330
|
+
sys.exit(1)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def cmd_site(a):
|
|
334
|
+
from .pipeline import rebuild_site
|
|
335
|
+
cfg = cfgmod.load(_config_arg(a))
|
|
336
|
+
res = rebuild_site(cfg, _web_dist(a.web), a.release)
|
|
337
|
+
print(f"site of release {res['release']} written to {res['site']}")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def cmd_reassess(a):
|
|
341
|
+
from .pipeline import reassess
|
|
342
|
+
cfg = cfgmod.load(_config_arg(a))
|
|
343
|
+
res = reassess(cfg, _web_dist(a.web), a.release)
|
|
344
|
+
print(f"site of release {res['release']} written to {res['site']}")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def cmd_bundle(a):
|
|
348
|
+
from .bundle import bundle
|
|
349
|
+
cfg = cfgmod.load(_config_arg(a))
|
|
350
|
+
site = Path(a.site).resolve() if a.site else cfg.out_dir / "site"
|
|
351
|
+
res = bundle(site, Path(a.out).resolve(), a.title)
|
|
352
|
+
print(f"wrote {res['out']}: {res['size'] / 1e6:.1f} MB, {res['files']} data files, "
|
|
353
|
+
f"{res['pdfs']} PDFs; open it in a browser, no server needed")
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def cmd_export(a):
|
|
357
|
+
from .export import ExportError, export
|
|
358
|
+
cfg = cfgmod.load(_config_arg(a))
|
|
359
|
+
try:
|
|
360
|
+
res = export(cfg, Path(a.out).resolve(), a.release, _web_dist(a.web))
|
|
361
|
+
except ExportError as e:
|
|
362
|
+
sys.exit(str(e))
|
|
363
|
+
print(f"exported release {res['release']} to {res['out']}")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def cmd_serve(a):
|
|
367
|
+
cfg = cfgmod.load(_config_arg(a))
|
|
368
|
+
site = Path(a.dir).resolve() if a.dir else cfg.out_dir / "site"
|
|
369
|
+
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=str(site))
|
|
370
|
+
print(f"serving {site} on http://127.0.0.1:{a.port}")
|
|
371
|
+
http.server.ThreadingHTTPServer(("127.0.0.1", a.port), handler).serve_forever()
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def cmd_review(a):
|
|
375
|
+
import shlex
|
|
376
|
+
import subprocess
|
|
377
|
+
from . import promote as pr
|
|
378
|
+
from .pipeline import reassess
|
|
379
|
+
cfg = cfgmod.load(_config_arg(a))
|
|
380
|
+
try:
|
|
381
|
+
steps = pr.plan(cfg, pr.load_reports([Path(f) for f in a.reports]),
|
|
382
|
+
a.accept_same_family)
|
|
383
|
+
except pr.PromoteError as e:
|
|
384
|
+
sys.exit(f"machinechecked: {e}")
|
|
385
|
+
ok = []
|
|
386
|
+
for r, cur, why in steps:
|
|
387
|
+
u = r.get("unit", {})
|
|
388
|
+
head = f"{u.get('name')} ({u.get('kind')}) by {r.get('reviewer', {}).get('id')}: " \
|
|
389
|
+
f"{r.get('verdict', {}).get('value')}"
|
|
390
|
+
if why:
|
|
391
|
+
print(f"REFUSED {head}")
|
|
392
|
+
for w in why:
|
|
393
|
+
print(f" - {w}")
|
|
394
|
+
else:
|
|
395
|
+
print(f"OK {head}\n {pr.marker(r, cur)}")
|
|
396
|
+
ok.append((r, cur))
|
|
397
|
+
if not ok:
|
|
398
|
+
sys.exit("nothing to promote")
|
|
399
|
+
if not a.yes:
|
|
400
|
+
if not sys.stdin.isatty():
|
|
401
|
+
sys.exit("pass --yes to write the markers without a prompt")
|
|
402
|
+
if input(f"Write {len(ok)} marker(s) and archive the report(s)? [y/N] ").strip().lower() \
|
|
403
|
+
not in ("y", "yes"):
|
|
404
|
+
sys.exit("nothing written")
|
|
405
|
+
done = pr.apply(cfg, ok)
|
|
406
|
+
for d in done:
|
|
407
|
+
print(f"wrote {d['marker']} to {d['file']}")
|
|
408
|
+
old = {c["id"]: c["status"] for c in pr.latest_record(cfg)["body"]["assessment"]["claims"]}
|
|
409
|
+
res = reassess(cfg, _web_dist(None))
|
|
410
|
+
for c in res["index"]["claims"]:
|
|
411
|
+
if old.get(c["id"]) != c["status"]:
|
|
412
|
+
print(f" {c['id']}: {old.get(c['id'])} -> {c['status']}")
|
|
413
|
+
if cfg.review_check:
|
|
414
|
+
print(f"running {cfg.review_check}")
|
|
415
|
+
r = subprocess.run(shlex.split(cfg.review_check), cwd=cfg.root)
|
|
416
|
+
if r.returncode:
|
|
417
|
+
sys.exit(r.returncode)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def cmd_validate(a):
|
|
421
|
+
validate(json.loads(Path(a.file).read_text()), a.schema)
|
|
422
|
+
print(f"{a.file}: valid {a.schema}")
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def main(argv=None):
|
|
426
|
+
name = Path(sys.argv[0]).name
|
|
427
|
+
p = argparse.ArgumentParser(prog=name if name in ("machinechecked", "prooflens")
|
|
428
|
+
else "machinechecked", description=__doc__.split("\n\n")[0],
|
|
429
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
430
|
+
epilog="\n\n".join(__doc__.split("\n\n")[1:]))
|
|
431
|
+
sub = p.add_subparsers(dest="cmd", required=True, metavar="{view,build,check}")
|
|
432
|
+
|
|
433
|
+
w = sub.add_parser("view", help="open the reader")
|
|
434
|
+
w.add_argument("paper", nargs="?")
|
|
435
|
+
w.add_argument("--issues", metavar="OWNER/REPO",
|
|
436
|
+
help="the GitHub repository where the reader's Raise issue opens reports")
|
|
437
|
+
w.add_argument("--site-url", help="the public address of the published reader, "
|
|
438
|
+
"for links to claims")
|
|
439
|
+
w.add_argument("--author", help="the name the audit is published under (in audit.json)")
|
|
440
|
+
w.add_argument("--private-repository", action=argparse.BooleanOptionalAction, default=None,
|
|
441
|
+
help="the Lean repository is private: the reader does not show its name "
|
|
442
|
+
"(its source links still name it, and open only for people with access)")
|
|
443
|
+
w.add_argument("-c", "--config", help=argparse.SUPPRESS)
|
|
444
|
+
w.add_argument("--port", type=int, default=8017)
|
|
445
|
+
w.add_argument("--no-browser", action="store_true")
|
|
446
|
+
w.add_argument("--web", help=argparse.SUPPRESS)
|
|
447
|
+
w.set_defaults(func=cmd_view)
|
|
448
|
+
|
|
449
|
+
b = sub.add_parser("build", help="write the reader as one HTML file to share")
|
|
450
|
+
b.add_argument("paper", nargs="?")
|
|
451
|
+
b.add_argument("--issues", metavar="OWNER/REPO",
|
|
452
|
+
help="the GitHub repository where the reader's Raise issue opens reports")
|
|
453
|
+
b.add_argument("--site-url", help="the public address of the published reader, "
|
|
454
|
+
"for links to claims")
|
|
455
|
+
b.add_argument("--author", help="the name the audit is published under (in audit.json)")
|
|
456
|
+
b.add_argument("--private-repository", action=argparse.BooleanOptionalAction, default=None,
|
|
457
|
+
help="the Lean repository is private: the reader does not show its name "
|
|
458
|
+
"(its source links still name it, and open only for people with access)")
|
|
459
|
+
b.add_argument("--out", help="the HTML file (default: MachineChecked.html at the project root)")
|
|
460
|
+
b.add_argument("--site", metavar="DIR", help="write the reader as a directory to publish "
|
|
461
|
+
"(for example on GitHub Pages) instead of one HTML file")
|
|
462
|
+
b.add_argument("-c", "--config", help=argparse.SUPPRESS)
|
|
463
|
+
b.add_argument("--release", help=argparse.SUPPRESS)
|
|
464
|
+
b.add_argument("--skip-lean", action="store_true", help=argparse.SUPPRESS)
|
|
465
|
+
b.add_argument("--skip-tex", action="store_true", help=argparse.SUPPRESS)
|
|
466
|
+
b.add_argument("--lake", default="lake", help=argparse.SUPPRESS)
|
|
467
|
+
b.add_argument("--no-lean-build", action="store_true", help=argparse.SUPPRESS)
|
|
468
|
+
b.add_argument("--reuse-lean", action="store_true", help=argparse.SUPPRESS)
|
|
469
|
+
b.add_argument("--web", help=argparse.SUPPRESS)
|
|
470
|
+
b.add_argument("--fail-on-violation", action="store_true", help=argparse.SUPPRESS)
|
|
471
|
+
b.set_defaults(func=cmd_build)
|
|
472
|
+
|
|
473
|
+
k = sub.add_parser("check", help="report what the Lean side establishes (exit 1 if a "
|
|
474
|
+
"linked declaration is missing or unproved)")
|
|
475
|
+
k.add_argument("paper", nargs="?")
|
|
476
|
+
k.add_argument("--json", action="store_true",
|
|
477
|
+
help="print every claim's status and links as JSON (for scripts and agents)")
|
|
478
|
+
k.add_argument("-c", "--config", help=argparse.SUPPRESS)
|
|
479
|
+
k.add_argument("--web", help=argparse.SUPPRESS)
|
|
480
|
+
k.set_defaults(func=cmd_check)
|
|
481
|
+
|
|
482
|
+
# The full assessment's own operations; not listed.
|
|
483
|
+
t = sub.add_parser("site")
|
|
484
|
+
t.add_argument("-c", "--config", default=None)
|
|
485
|
+
t.add_argument("--release")
|
|
486
|
+
t.add_argument("--web")
|
|
487
|
+
t.set_defaults(func=cmd_site)
|
|
488
|
+
|
|
489
|
+
r = sub.add_parser("reassess")
|
|
490
|
+
r.add_argument("-c", "--config", default=None)
|
|
491
|
+
r.add_argument("--release")
|
|
492
|
+
r.add_argument("--web")
|
|
493
|
+
r.set_defaults(func=cmd_reassess)
|
|
494
|
+
|
|
495
|
+
n = sub.add_parser("bundle")
|
|
496
|
+
n.add_argument("-c", "--config", default=None)
|
|
497
|
+
n.add_argument("--site")
|
|
498
|
+
n.add_argument("--out", required=True)
|
|
499
|
+
n.add_argument("--title")
|
|
500
|
+
n.set_defaults(func=cmd_bundle)
|
|
501
|
+
|
|
502
|
+
e = sub.add_parser("export")
|
|
503
|
+
e.add_argument("-c", "--config", default=None)
|
|
504
|
+
e.add_argument("--release")
|
|
505
|
+
e.add_argument("--out", required=True)
|
|
506
|
+
e.add_argument("--web")
|
|
507
|
+
e.set_defaults(func=cmd_export)
|
|
508
|
+
|
|
509
|
+
s = sub.add_parser("serve")
|
|
510
|
+
s.add_argument("-c", "--config", default=None)
|
|
511
|
+
s.add_argument("--dir")
|
|
512
|
+
s.add_argument("--port", type=int, default=8017)
|
|
513
|
+
s.set_defaults(func=cmd_serve)
|
|
514
|
+
|
|
515
|
+
rv = sub.add_parser("review")
|
|
516
|
+
rvs = rv.add_subparsers(dest="action", required=True)
|
|
517
|
+
pm = rvs.add_parser("promote", help="turn review reports into review markers (maintainer)")
|
|
518
|
+
pm.add_argument("reports", nargs="+", help="files exported from the review queue")
|
|
519
|
+
pm.add_argument("-c", "--config", default=None)
|
|
520
|
+
pm.add_argument("--yes", action="store_true", help="write without asking")
|
|
521
|
+
pm.add_argument("--accept-same-family", action="store_true",
|
|
522
|
+
help="accept a model reviewer of the author's family, or of unknown family")
|
|
523
|
+
pm.set_defaults(func=cmd_review)
|
|
524
|
+
|
|
525
|
+
v = sub.add_parser("validate")
|
|
526
|
+
v.add_argument("file")
|
|
527
|
+
v.add_argument("--schema", required=True,
|
|
528
|
+
choices=["inventory", "evidence", "manifest-index", "manifest-claim"])
|
|
529
|
+
v.set_defaults(func=cmd_validate)
|
|
530
|
+
|
|
531
|
+
a = p.parse_args(argv)
|
|
532
|
+
try:
|
|
533
|
+
a.func(a)
|
|
534
|
+
except FileNotFoundError as e:
|
|
535
|
+
hint = MISSING.get(Path(str(e.filename or "")).name)
|
|
536
|
+
if not hint:
|
|
537
|
+
raise
|
|
538
|
+
sys.exit(f"machinechecked: `{Path(e.filename).name}` not found: {hint}")
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
if __name__ == "__main__":
|
|
542
|
+
main()
|