machinechecked 0.1.3__tar.gz → 0.1.5__tar.gz
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.3 → machinechecked-0.1.5}/PKG-INFO +1 -1
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/machinechecked.egg-info/PKG-INFO +1 -1
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/machinechecked.egg-info/SOURCES.txt +3 -1
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/cli.py +53 -8
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/data/schema/manifest-index.schema.json +2 -1
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/formalization.py +2 -1
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/manifest.py +12 -0
- machinechecked-0.1.5/engine/python/prooflens/paper.py +103 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/pdfclaims.py +3 -1
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/pipeline.py +4 -4
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/simple.py +80 -34
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/sources.py +28 -4
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/util.py +32 -0
- machinechecked-0.1.3/engine/python/prooflens/web/assets/index-CEQVnsCC.js → machinechecked-0.1.5/engine/python/prooflens/web/assets/index-D0aKe720.js +29 -29
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/web/index.html +1 -1
- machinechecked-0.1.5/engine/python/prooflens/web/theme.css +82 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/pyproject.toml +1 -1
- {machinechecked-0.1.3 → machinechecked-0.1.5}/LICENSE +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/README.md +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/machinechecked.egg-info/dependency_links.txt +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/machinechecked.egg-info/entry_points.txt +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/machinechecked.egg-info/requires.txt +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/machinechecked.egg-info/top_level.txt +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/__init__.py +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/__main__.py +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/assess.py +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/audit.py +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/bundle.py +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/config.py +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/data/ProofLensProbe.lean +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/data/prooflens.sty +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/data/schema/evidence.schema.json +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/data/schema/inventory.schema.json +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/data/schema/manifest-claim.schema.json +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/evidence.py +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/export.py +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/probe.py +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/promote.py +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/review_queue.py +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/reviews.py +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/schemas.py +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/server.py +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/tex.py +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/web/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/web/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/web/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/web/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/web/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/web/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/web/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/web/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/web/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/web/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/web/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/web/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/web/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/web/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/web/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/web/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/web/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/web/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/web/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/web/assets/index-T9UdK0vf.css +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/prooflens/web/assets/pdf.worker.min-yatZIOMy.mjs +0 -0
- {machinechecked-0.1.3 → machinechecked-0.1.5}/setup.cfg +0 -0
{machinechecked-0.1.3 → machinechecked-0.1.5}/engine/python/machinechecked.egg-info/SOURCES.txt
RENAMED
|
@@ -18,6 +18,7 @@ engine/python/prooflens/evidence.py
|
|
|
18
18
|
engine/python/prooflens/export.py
|
|
19
19
|
engine/python/prooflens/formalization.py
|
|
20
20
|
engine/python/prooflens/manifest.py
|
|
21
|
+
engine/python/prooflens/paper.py
|
|
21
22
|
engine/python/prooflens/pdfclaims.py
|
|
22
23
|
engine/python/prooflens/pipeline.py
|
|
23
24
|
engine/python/prooflens/probe.py
|
|
@@ -37,6 +38,7 @@ engine/python/prooflens/data/schema/inventory.schema.json
|
|
|
37
38
|
engine/python/prooflens/data/schema/manifest-claim.schema.json
|
|
38
39
|
engine/python/prooflens/data/schema/manifest-index.schema.json
|
|
39
40
|
engine/python/prooflens/web/index.html
|
|
41
|
+
engine/python/prooflens/web/theme.css
|
|
40
42
|
engine/python/prooflens/web/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2
|
|
41
43
|
engine/python/prooflens/web/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2
|
|
42
44
|
engine/python/prooflens/web/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2
|
|
@@ -56,6 +58,6 @@ engine/python/prooflens/web/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2
|
|
|
56
58
|
engine/python/prooflens/web/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2
|
|
57
59
|
engine/python/prooflens/web/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2
|
|
58
60
|
engine/python/prooflens/web/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2
|
|
59
|
-
engine/python/prooflens/web/assets/index-
|
|
61
|
+
engine/python/prooflens/web/assets/index-D0aKe720.js
|
|
60
62
|
engine/python/prooflens/web/assets/index-T9UdK0vf.css
|
|
61
63
|
engine/python/prooflens/web/assets/pdf.worker.min-yatZIOMy.mjs
|
|
@@ -8,7 +8,8 @@ Everyday commands, run from anywhere inside a Lean project:
|
|
|
8
8
|
|
|
9
9
|
A project with a `machinechecked.toml` (an acceptance layer: locked statements, reviews) gets
|
|
10
10
|
the full assessment; any other project gets the zero-configuration view, whose only file in the
|
|
11
|
-
repository is `machinechecked.links.json
|
|
11
|
+
repository is `machinechecked.links.json` (its working files, a downloaded paper among them, are
|
|
12
|
+
in `.machinechecked/`, which Git ignores). The other commands are for that full assessment.
|
|
12
13
|
`prooflens` is another name for the same command.
|
|
13
14
|
"""
|
|
14
15
|
|
|
@@ -149,7 +150,8 @@ def _fetch_paper(root: Path, url: str, title: str | None = None) -> Path:
|
|
|
149
150
|
repository), with its address and title beside it."""
|
|
150
151
|
import hashlib
|
|
151
152
|
import urllib.request
|
|
152
|
-
|
|
153
|
+
from .util import prepare_work_dir
|
|
154
|
+
d = prepare_work_dir(root / cfgmod.work_dir_name(root)) / "papers"
|
|
153
155
|
name = re.sub(r"[^\w.-]+", "_", url.rstrip("/").rsplit("/", 1)[-1]) or "paper"
|
|
154
156
|
if not name.lower().endswith(".pdf"):
|
|
155
157
|
name += ".pdf"
|
|
@@ -357,7 +359,8 @@ def cmd_export(a):
|
|
|
357
359
|
from .export import ExportError, export
|
|
358
360
|
cfg = cfgmod.load(_config_arg(a))
|
|
359
361
|
try:
|
|
360
|
-
res = export(cfg, Path(a.out).resolve(), a.release,
|
|
362
|
+
res = export(cfg, Path(a.out).resolve(), a.release,
|
|
363
|
+
None if a.data_only else _web_dist(a.web))
|
|
361
364
|
except ExportError as e:
|
|
362
365
|
sys.exit(str(e))
|
|
363
366
|
print(f"exported release {res['release']} to {res['out']}")
|
|
@@ -471,7 +474,8 @@ def main(argv=None):
|
|
|
471
474
|
b.set_defaults(func=cmd_build)
|
|
472
475
|
|
|
473
476
|
k = sub.add_parser("check", help="report what the Lean side establishes (exit 1 if a "
|
|
474
|
-
"linked declaration is missing or unproved
|
|
477
|
+
"linked declaration is missing or unproved, 2 if the check "
|
|
478
|
+
"could not run)")
|
|
475
479
|
k.add_argument("paper", nargs="?")
|
|
476
480
|
k.add_argument("--json", action="store_true",
|
|
477
481
|
help="print every claim's status and links as JSON (for scripts and agents)")
|
|
@@ -504,6 +508,9 @@ def main(argv=None):
|
|
|
504
508
|
e.add_argument("--release")
|
|
505
509
|
e.add_argument("--out", required=True)
|
|
506
510
|
e.add_argument("--web")
|
|
511
|
+
e.add_argument("--data-only", action="store_true",
|
|
512
|
+
help="write the audit's data only, without the reader (for a site that "
|
|
513
|
+
"reads it with its own reader, such as machinechecked.org)")
|
|
507
514
|
e.set_defaults(func=cmd_export)
|
|
508
515
|
|
|
509
516
|
s = sub.add_parser("serve")
|
|
@@ -531,11 +538,49 @@ def main(argv=None):
|
|
|
531
538
|
a = p.parse_args(argv)
|
|
532
539
|
try:
|
|
533
540
|
a.func(a)
|
|
541
|
+
except SystemExit as e:
|
|
542
|
+
# `sys.exit("message")` is a command that could not do its job; an integer is its result.
|
|
543
|
+
if isinstance(e.code, str):
|
|
544
|
+
_could_not_run(a, "error", e.code.removeprefix("machinechecked: "))
|
|
545
|
+
raise
|
|
534
546
|
except FileNotFoundError as e:
|
|
535
|
-
|
|
536
|
-
if
|
|
537
|
-
|
|
538
|
-
|
|
547
|
+
tool = Path(str(e.filename or "")).name
|
|
548
|
+
if tool in MISSING:
|
|
549
|
+
_could_not_run(a, "missing-tool", f"`{tool}` not found: {MISSING[tool]}", tool=tool)
|
|
550
|
+
_could_not_run(a, "internal", _crash(e))
|
|
551
|
+
except (RuntimeError, ValueError, OSError) as e:
|
|
552
|
+
# The engine's own errors (the Lean probe, TeX, the paper's sources, the configuration,
|
|
553
|
+
# stored evidence) say what went wrong in their message.
|
|
554
|
+
if type(e).__module__.startswith("prooflens"):
|
|
555
|
+
_could_not_run(a, "error", str(e))
|
|
556
|
+
_could_not_run(a, "internal", _crash(e))
|
|
557
|
+
except Exception as e:
|
|
558
|
+
_could_not_run(a, "internal", _crash(e))
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
# Exit codes: 0 the command did its job (and `check` found nothing wrong), 1 `check` (or
|
|
562
|
+
# `build --fail-on-violation`) found a problem, 2 the command could not run.
|
|
563
|
+
COULD_NOT_RUN = 2
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _crash(e: Exception) -> str:
|
|
567
|
+
"""An unexpected error: its traceback on stderr, for a bug report, and a one-line message."""
|
|
568
|
+
import traceback
|
|
569
|
+
traceback.print_exc()
|
|
570
|
+
return f"unexpected error ({type(e).__name__}: {e}); please report it with the traceback above"
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _could_not_run(a, kind: str, message: str, **extra):
|
|
574
|
+
"""Stop with exit 2: the message on stderr and, for `check --json`, one JSON document on
|
|
575
|
+
stdout saying the check did not run."""
|
|
576
|
+
print(f"machinechecked: {message}", file=sys.stderr)
|
|
577
|
+
if getattr(a, "func", None) is cmd_check and getattr(a, "json", False):
|
|
578
|
+
import json
|
|
579
|
+
print(json.dumps({"schema": "machinechecked/check@1", "ok": False,
|
|
580
|
+
"error": {"kind": kind, "message": message, **extra},
|
|
581
|
+
"note": "The check did not run; nothing is known about the claims."},
|
|
582
|
+
indent=1))
|
|
583
|
+
raise SystemExit(COULD_NOT_RUN)
|
|
539
584
|
|
|
540
585
|
|
|
541
586
|
if __name__ == "__main__":
|
|
@@ -18,7 +18,8 @@ from pathlib import Path
|
|
|
18
18
|
NAMES = ("formalization.yaml", "formalization.yml")
|
|
19
19
|
KINDS = ("theorem", "lemma", "proposition", "corollary", "definition", "remark", "example",
|
|
20
20
|
"conjecture", "claim", "assumption", "hypothesis", "convention")
|
|
21
|
-
REF = re.compile(r"\b(" + "|".join(KINDS) + r")\s+([A-Z]?\d+(?:\.\d+)*[a-z]?)",
|
|
21
|
+
REF = re.compile(r"\b(" + "|".join(KINDS) + r")\s+((?:[A-Z](?:\.\d+)+|[A-Z]?\d+(?:\.\d+)*)[a-z]?)",
|
|
22
|
+
re.I)
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
def find(root: Path) -> Path | None:
|
|
@@ -45,6 +45,18 @@ def top_of(anchor: dict) -> tuple[int, float]:
|
|
|
45
45
|
return anchor["begin"][0], anchor["begin"][2]
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
def index_anchor(anchor: dict | None, kind: str) -> dict | None:
|
|
49
|
+
"""A located claim's place as the index lists it (for the contents and the margin): its
|
|
50
|
+
page and top, where it ends on that page, and the page it ends on."""
|
|
51
|
+
if not anchor or not anchor.get("begin"):
|
|
52
|
+
return None
|
|
53
|
+
page, y0 = top_of({**anchor, "kind": kind})
|
|
54
|
+
end = anchor.get("end")
|
|
55
|
+
return {"kind": kind, "page": page, "y0": y0,
|
|
56
|
+
"y1": end[2] if end and end[0] == anchor["begin"][0] else None,
|
|
57
|
+
"page_end": end[0] if end else None}
|
|
58
|
+
|
|
59
|
+
|
|
48
60
|
def ladder(cl: dict, anchor: dict | None, mode: str | None, src_ok: bool | None) -> list[dict]:
|
|
49
61
|
"""The rungs of the evidence ladder of one clause, each with its own state."""
|
|
50
62
|
ch = cl.get("checks", {})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""A paper's page: the paper's PDF, the statements found in it, and for each Lean project that
|
|
2
|
+
states it formalises the paper, the declarations its `formalization.yaml` maps them to.
|
|
3
|
+
|
|
4
|
+
It is written in the reader's standard data format (`data/index.json`, `data/claims/`), so a
|
|
5
|
+
paper page is read by the same code as a view or an audit. Nothing here runs Lean, so a mapped
|
|
6
|
+
statement reads "linked: Lean not checked here (no audit published)": the link says which
|
|
7
|
+
declaration the project names, not what Lean establishes. Each project's links and statuses are
|
|
8
|
+
kept apart (`by_project`); the reader shows one project at a time and never combines them.
|
|
9
|
+
|
|
10
|
+
No statement's text is written: only its number, heading title and place on the page.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from . import formalization as fz
|
|
19
|
+
from . import manifest as mf
|
|
20
|
+
from .util import content_id
|
|
21
|
+
|
|
22
|
+
SCHEMA = "prooflens/manifest@0"
|
|
23
|
+
NOT_CHECKED = "linked: Lean not checked here (no audit published)"
|
|
24
|
+
NOT_LINKED = "not linked"
|
|
25
|
+
NAME = re.compile(r"[\w.'«»]+")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def paper_claims(anchors: list[dict], title: str,
|
|
29
|
+
alignments: list[tuple[dict, dict | None]]) -> list[dict]:
|
|
30
|
+
"""The statements found in the paper (anchors from `pdfclaims.claims_from_pdf`), each with
|
|
31
|
+
the links the projects' `formalization.yaml` files declare for it. `alignments` pairs each
|
|
32
|
+
project (`repository`, `commit`) with its parsed `formalization.yaml`, or None when it has
|
|
33
|
+
none or it could not be read."""
|
|
34
|
+
out = []
|
|
35
|
+
for a in anchors:
|
|
36
|
+
number = f"{a['env'].capitalize()} {a['number']}"
|
|
37
|
+
links = []
|
|
38
|
+
for f, fy in alignments:
|
|
39
|
+
for row in fz.links_for(fy, a["env"], number, title) if fy else []:
|
|
40
|
+
links += [{"name": n, "repository": f["repository"], "commit": f["commit"],
|
|
41
|
+
"source": "formalization.yaml"}
|
|
42
|
+
for n in row["lean"] if NAME.fullmatch(n)]
|
|
43
|
+
out.append({"key": f"{a['env']}-{a['number']}", "number": number, "kind": a["env"],
|
|
44
|
+
"title": a.get("title"),
|
|
45
|
+
"anchor": {k: a[k] for k in ("begin", "end", "head") if a.get(k)},
|
|
46
|
+
"links": links})
|
|
47
|
+
return out
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _state(links: list[dict]) -> tuple[str, str]:
|
|
51
|
+
return (NOT_CHECKED, "declared") if links else (NOT_LINKED, "unspecified")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def write_paper_site(site: Path, paper: dict, claims: list[dict],
|
|
55
|
+
page_size: list | tuple | None, pdf_url: str | None,
|
|
56
|
+
web_dist: Path | None = None) -> dict:
|
|
57
|
+
"""Write the paper page's data under `site/data/` (and the reader, given its built
|
|
58
|
+
directory). `paper` is the paper's registry entry (its identifier, title, authors, links and
|
|
59
|
+
formalisations), which the reader's panel beside the paper shows; `claims` are from
|
|
60
|
+
`paper_claims`. Replaces whatever is at `site`."""
|
|
61
|
+
from .pipeline import write_preview
|
|
62
|
+
projects = [f["repository"] for f in paper.get("formalisations", [])]
|
|
63
|
+
index_claims, chunks = [], {}
|
|
64
|
+
for c in claims:
|
|
65
|
+
cid = f"P:{c['key']}"
|
|
66
|
+
fname = mf.claim_file(cid)
|
|
67
|
+
anchor = {**c["anchor"], "kind": "approximate"} if c.get("anchor") else None
|
|
68
|
+
by_project = {}
|
|
69
|
+
for r in projects:
|
|
70
|
+
status, cls = _state([l for l in c["links"] if l["repository"] == r])
|
|
71
|
+
by_project[r] = {"status": status, "class": cls}
|
|
72
|
+
status, cls = _state([l for l in c["links"]
|
|
73
|
+
if not projects or l["repository"] == projects[0]])
|
|
74
|
+
common = {"id": cid, "paper": "P", "number": c["number"], "title": c.get("title"),
|
|
75
|
+
"heading": c.get("title") or None, "kind": c["kind"], "scope": "formal",
|
|
76
|
+
"status": status, "class": cls, "by_project": by_project}
|
|
77
|
+
index_claims.append({**common, "anchor": mf.index_anchor(anchor, "approximate"),
|
|
78
|
+
"counts": {}, "file": fname, "linked": len(c["links"]),
|
|
79
|
+
"suggested": 0})
|
|
80
|
+
chunks[fname] = {
|
|
81
|
+
**common, "schema": SCHEMA, "mode": "simple", "release": "paper", "key": c["key"],
|
|
82
|
+
"scope_note": "", "scope_reviewed": True, "anchor": anchor,
|
|
83
|
+
"source_matches": None, "statement_tex": None, "blockers": [],
|
|
84
|
+
"links": [{**l, "status": NOT_CHECKED, "class": "declared", "why": None,
|
|
85
|
+
"statement": None, "path": None} for l in c["links"]],
|
|
86
|
+
"suggestions": [], "clauses": [], "decls": {}, "findings": []}
|
|
87
|
+
summary = {}
|
|
88
|
+
for c in index_claims:
|
|
89
|
+
summary[c["status"]] = summary.get(c["status"], 0) + 1
|
|
90
|
+
# The page has no Lean evidence; its identifier is that of what it shows.
|
|
91
|
+
eid = content_id({"paper": paper, "claims": claims, "pdf": pdf_url})
|
|
92
|
+
index = {"schema": SCHEMA, "mode": "paper", "release": "paper", "evidence": eid,
|
|
93
|
+
"tool": {"name": "machinechecked"}, "project": {}, "projects": projects,
|
|
94
|
+
"paper": paper, "notes": [],
|
|
95
|
+
"papers": [{"id": "P", "title": paper.get("title", ""), "mode": "pdf", "pdf": None,
|
|
96
|
+
"pdf_url": pdf_url, "page_size": list(page_size) if page_size else None,
|
|
97
|
+
"note": ""}],
|
|
98
|
+
"summary": {"P": summary},
|
|
99
|
+
"classes": {c["status"]: c["class"] for c in index_claims},
|
|
100
|
+
"findings": {"total": {}, "general": []}, "gates": {}, "claims": index_claims,
|
|
101
|
+
"search": "data/search.json"}
|
|
102
|
+
write_preview(site, index, chunks, {}, {}, web_dist)
|
|
103
|
+
return index
|
|
@@ -20,7 +20,9 @@ from .util import norm_ws, sha256_text
|
|
|
20
20
|
|
|
21
21
|
KINDS = ("Theorem", "Lemma", "Proposition", "Corollary", "Definition", "Remark", "Example",
|
|
22
22
|
"Assumption", "Conjecture", "Claim", "Hypothesis", "Convention")
|
|
23
|
-
|
|
23
|
+
# A statement's number: "3", "8.6", "A1", "4.2a", or an appendix's "A.1", "B.10".
|
|
24
|
+
NUMBER = r"(?:[A-Z](?:\.\d+)+|[A-Z]?\d+(?:\.\d+)*)[a-z]?"
|
|
25
|
+
HEAD = re.compile(r"^(" + "|".join(KINDS) + r")\s+(" + NUMBER + r")\s*(\.|\(|$)")
|
|
24
26
|
STOP = re.compile(r"^(Proof\b|\d+(\.\d+)*\s+[A-Z])")
|
|
25
27
|
|
|
26
28
|
|
|
@@ -20,9 +20,8 @@ from .reviews import load_dismissals, load_reviews
|
|
|
20
20
|
from .schemas import validate
|
|
21
21
|
from .sources import acquire
|
|
22
22
|
from .tex import DEFAULT_ENVS, TexError, anchors_for, theorem_envs
|
|
23
|
-
from .util import (canonical_json, git_dirty, git_head,
|
|
24
|
-
sha256_text,
|
|
25
|
-
write_json)
|
|
23
|
+
from .util import (canonical_json, git_dirty, git_head, link_or_copy, norm_ws, prepare_work_dir,
|
|
24
|
+
read_json, sha256_file, sha256_text, write_json)
|
|
26
25
|
|
|
27
26
|
|
|
28
27
|
def tool_version() -> str:
|
|
@@ -49,6 +48,7 @@ def project_provenance(cfg: Config) -> dict:
|
|
|
49
48
|
def build(cfg: Config, release: str | None = None, skip_lean: bool = False,
|
|
50
49
|
skip_tex: bool = False, lake: str = "lake", web_dist: Path | None = None,
|
|
51
50
|
lean_build: bool = True, reuse_lean: bool = False, log=print) -> dict:
|
|
51
|
+
prepare_work_dir(cfg.out_dir)
|
|
52
52
|
inv = read_json(cfg.inventory)
|
|
53
53
|
validate(inv, "inventory")
|
|
54
54
|
reviews, review_hashes = load_reviews(cfg.reviews, cfg.ai_reviewers, cfg.root)
|
|
@@ -328,4 +328,4 @@ def write_preview(site: Path, index: dict, chunks: dict, store: dict, pdfs: dict
|
|
|
328
328
|
write_json(data / "review-queue.json", queue, indent=None)
|
|
329
329
|
for pid, pdf in pdfs.items():
|
|
330
330
|
(data / "papers").mkdir(parents=True, exist_ok=True)
|
|
331
|
-
|
|
331
|
+
link_or_copy(pdf, data / "papers" / f"{pid}.pdf")
|
|
@@ -29,7 +29,7 @@ from .probe import fingerprint
|
|
|
29
29
|
from .sources import SourceError, acquire
|
|
30
30
|
from .tex import DEFAULT_ENVS, TexError, anchors_for, theorem_envs
|
|
31
31
|
from . import formalization as fz
|
|
32
|
-
from .util import content_id, git_head, read_json, sha256_file, write_json
|
|
32
|
+
from .util import content_id, git_head, prepare_work_dir, read_json, sha256_file, write_json
|
|
33
33
|
|
|
34
34
|
LINKS = "machinechecked.links.json"
|
|
35
35
|
OLD_LINKS = "prooflens.links.json"
|
|
@@ -275,11 +275,36 @@ def save_settings(root: Path, issues: str | None = None, site_url: str | None =
|
|
|
275
275
|
|
|
276
276
|
# -- the Lean side ---------------------------------------------------------------------------
|
|
277
277
|
|
|
278
|
+
# What an index record keeps: the reader lists a declaration's statement only to this length,
|
|
279
|
+
# and suggestions read a docstring only for the statement numbers it cites.
|
|
280
|
+
INDEX_TYPE_CHARS, INDEX_DOC_CHARS = 300, 4000
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _record(line: str) -> dict:
|
|
284
|
+
"""One probe record, as kept in memory: an index record trimmed to what is used, any other
|
|
285
|
+
with its canonical form replaced by its fingerprint."""
|
|
286
|
+
rec = json.loads(line)
|
|
287
|
+
if rec.get("record") == "index":
|
|
288
|
+
return {"record": "index", "name": rec["name"], "kind": rec["kind"],
|
|
289
|
+
"module": rec.get("module"), "type": (rec.get("type") or "")[:INDEX_TYPE_CHARS],
|
|
290
|
+
"doc": (rec.get("doc") or "")[:INDEX_DOC_CHARS] or None}
|
|
291
|
+
for d in [rec, rec.get("decl")]:
|
|
292
|
+
if isinstance(d, dict) and "canon" in d:
|
|
293
|
+
d["fingerprint"] = fingerprint(d.pop("canon"))
|
|
294
|
+
return rec
|
|
295
|
+
|
|
296
|
+
|
|
278
297
|
def run_simple_probe(root: Path, modules: list[str], libs: list[str], requests: list[dict],
|
|
279
|
-
lake: str = "lake", timeout: int = 3600
|
|
298
|
+
lake: str = "lake", timeout: int = 3600,
|
|
299
|
+
work: Path | None = None) -> list[dict]:
|
|
300
|
+
"""Run the Lean probe on `modules` in one session. Its output is read one record at a time
|
|
301
|
+
(it can be gigabytes for a large project) from a file in `work` (the working directory's
|
|
302
|
+
build folder, rather than a system temporary folder that may be in memory)."""
|
|
280
303
|
body = resources.files("prooflens").joinpath("data/ProofLensProbe.lean").read_text()
|
|
281
304
|
header = "import Lean\n" + "".join(f"import {m}\n" for m in modules) + "\n"
|
|
282
|
-
|
|
305
|
+
if work is not None:
|
|
306
|
+
work.mkdir(parents=True, exist_ok=True)
|
|
307
|
+
with tempfile.TemporaryDirectory(prefix="prooflens-view-", dir=work) as tmp:
|
|
283
308
|
tmp = Path(tmp)
|
|
284
309
|
(tmp / "Probe.lean").write_text(header + body)
|
|
285
310
|
(tmp / "query.json").write_text(json.dumps({
|
|
@@ -292,12 +317,8 @@ def run_simple_probe(root: Path, modules: list[str], libs: list[str], requests:
|
|
|
292
317
|
out = tmp / "out.jsonl"
|
|
293
318
|
if r.returncode != 0 or not out.exists():
|
|
294
319
|
raise ViewError(f"the Lean probe failed:\n{r.stdout[-3000:]}\n{r.stderr[-3000:]}")
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
for d in [rec, rec.get("decl")]:
|
|
298
|
-
if isinstance(d, dict) and "canon" in d:
|
|
299
|
-
d["fingerprint"] = fingerprint(d.pop("canon"))
|
|
300
|
-
return recs
|
|
320
|
+
with out.open(encoding="utf-8") as f:
|
|
321
|
+
return [_record(l) for l in f if l.strip()]
|
|
301
322
|
|
|
302
323
|
|
|
303
324
|
def library_of(module: str, libs: list[str]) -> str:
|
|
@@ -307,7 +328,8 @@ def library_of(module: str, libs: list[str]) -> str:
|
|
|
307
328
|
|
|
308
329
|
|
|
309
330
|
def probe_modules(root: Path, modules: list[str], libs: list[str], requests: list[dict],
|
|
310
|
-
lake: str = "lake", log=print
|
|
331
|
+
lake: str = "lake", log=print,
|
|
332
|
+
work: Path | None = None) -> tuple[list[dict], dict[str, str]]:
|
|
311
333
|
"""Probe the built modules without assuming they can all be imported into one Lean session.
|
|
312
334
|
|
|
313
335
|
A project may build modules that do not belong together: two libraries declaring the same
|
|
@@ -320,7 +342,7 @@ def probe_modules(root: Path, modules: list[str], libs: list[str], requests: lis
|
|
|
320
342
|
|
|
321
343
|
def run(group: list[str], label: str):
|
|
322
344
|
try:
|
|
323
|
-
out = run_simple_probe(root, group, libs, requests, lake)
|
|
345
|
+
out = run_simple_probe(root, group, libs, requests, lake, work=work)
|
|
324
346
|
except ViewError as e:
|
|
325
347
|
if len(group) == 1:
|
|
326
348
|
failed[group[0]] = str(e)[-600:]
|
|
@@ -497,6 +519,7 @@ def paper_title(p: Path) -> str:
|
|
|
497
519
|
def build_view(root: Path, paper_path: Path, out_dir: Path, lake: str = "lake",
|
|
498
520
|
log=print) -> dict:
|
|
499
521
|
"""Everything the reader shows for `paper_path` in the Lean project at `root`."""
|
|
522
|
+
prepare_work_dir(out_dir)
|
|
500
523
|
libs = lib_names(root)
|
|
501
524
|
built, unbuilt = built_modules(root, libs) if libs else ([], [])
|
|
502
525
|
cfg = view_config(root, paper_path, out_dir, libs)
|
|
@@ -569,41 +592,69 @@ def build_view(root: Path, paper_path: Path, out_dir: Path, lake: str = "lake",
|
|
|
569
592
|
if unbuilt:
|
|
570
593
|
notes.append(f"{len(unbuilt)} module(s) of the project are not built and are not "
|
|
571
594
|
f"indexed")
|
|
572
|
-
# The
|
|
573
|
-
|
|
595
|
+
# The names linked before any suggestion (confirmed links, \lean tags, formalization.yaml),
|
|
596
|
+
# less those whose module is known not to be built: Lean cannot answer for those.
|
|
597
|
+
for c in claims:
|
|
598
|
+
c["link_names"] = linked_names(c, links.get(c["key"], {}))
|
|
599
|
+
unbuilt_set = set(unbuilt)
|
|
600
|
+
linked = {n for c in claims for n in c["link_names"]
|
|
601
|
+
if c.get("declared_modules", {}).get(n) not in unbuilt_set}
|
|
602
|
+
work = out_dir / "build"
|
|
603
|
+
|
|
604
|
+
def take(recs):
|
|
605
|
+
for r in recs:
|
|
606
|
+
if r["record"] == "link":
|
|
607
|
+
linkrecs.setdefault(r["name"], []).append(r)
|
|
608
|
+
elif r["record"] == "decl":
|
|
609
|
+
decls.setdefault(r["name"], r)
|
|
610
|
+
|
|
611
|
+
# The index is reused while no Lean input of the project changed. Built afresh, it is
|
|
612
|
+
# made in the same sessions as the linked names' records, so the modules load once.
|
|
613
|
+
icache = work / "index-cache.json"
|
|
574
614
|
idigest = lean_digest(root)
|
|
575
615
|
cached = read_json(icache) if icache.exists() else {}
|
|
576
616
|
failed = {}
|
|
577
617
|
if cached.get("digest") == idigest:
|
|
578
618
|
index, failed = cached["index"], cached.get("failed", {})
|
|
579
619
|
else:
|
|
580
|
-
log(f"lean: indexing {len(built)} built module(s)"
|
|
581
|
-
|
|
582
|
-
|
|
620
|
+
log(f"lean: indexing {len(built)} built module(s)"
|
|
621
|
+
+ (f" and checking {len(linked)} linked declaration(s)" if linked else ""))
|
|
622
|
+
recs, failed = probe_modules(root, built, libs, [{"kind": "index", "name": ""}]
|
|
623
|
+
+ [{"kind": "link", "name": n} for n in sorted(linked)],
|
|
624
|
+
lake, log, work)
|
|
583
625
|
index = [r for r in recs if r["record"] == "index"]
|
|
626
|
+
take(recs)
|
|
584
627
|
write_json(icache, {"digest": idigest, "index": index, "failed": failed},
|
|
585
628
|
indent=None)
|
|
586
|
-
wanted = set()
|
|
629
|
+
wanted = set(linked)
|
|
587
630
|
for c in claims:
|
|
588
631
|
entry = links.get(c["key"], {})
|
|
589
|
-
c["link_names"] = linked_names(c, entry)
|
|
590
632
|
c["suggestions"] = [s for s in suggest(c, index)
|
|
591
633
|
if s["name"] not in entry.get("rejected", [])
|
|
592
634
|
and s["name"] not in c["link_names"]]
|
|
593
|
-
wanted |= set(c["link_names"])
|
|
594
635
|
wanted |= {s["name"] for s in c["suggestions"][:1] if s.get("kind")}
|
|
636
|
+
wanted -= set(linkrecs)
|
|
595
637
|
if wanted:
|
|
638
|
+
# Only the modules that declare the names are loaded (with what they import). A name
|
|
639
|
+
# the index does not hold (from a dependency, say) is looked for in every loadable
|
|
640
|
+
# module, unless its source shows it in a module that is not built.
|
|
596
641
|
log(f"lean: checking {len(wanted)} linked or suggested declaration(s)")
|
|
597
642
|
loadable = [m for m in built if m not in failed]
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
for
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
643
|
+
where: dict[str, set[str]] = {}
|
|
644
|
+
for r in index:
|
|
645
|
+
if r["name"] in wanted and r.get("module") in loadable:
|
|
646
|
+
where.setdefault(r["name"], set()).add(r["module"])
|
|
647
|
+
elsewhere = sorted(n for n in wanted if n not in where
|
|
648
|
+
and not declaring_modules(root, unbuilt, n))
|
|
649
|
+
groups = [(sorted(set().union(*where.values())), sorted(where))] if where else []
|
|
650
|
+
if elsewhere:
|
|
651
|
+
groups.append((loadable, elsewhere))
|
|
652
|
+
for mods, names in groups:
|
|
653
|
+
recs, more = probe_modules(root, mods, libs,
|
|
654
|
+
[{"kind": "link", "name": n} for n in names],
|
|
655
|
+
lake, log, work)
|
|
656
|
+
failed.update(more)
|
|
657
|
+
take(recs)
|
|
607
658
|
if failed:
|
|
608
659
|
notes.append(f"{len(failed)} built module(s) could not be loaded by MachineChecked "
|
|
609
660
|
f"and are not indexed: {', '.join(sorted(failed)[:5])}"
|
|
@@ -679,16 +730,11 @@ def write_view_site(view: dict, site: Path, web_dist: Path | None,
|
|
|
679
730
|
if (a.get("label") or f"{a['env']}-{a.get('number')}") == c["key"]), None)
|
|
680
731
|
fname = mf.claim_file(c["id"])
|
|
681
732
|
akind = "approximate" if anchor and anchor.get("kind") == "pdf" else "exact"
|
|
682
|
-
top = mf.top_of({**anchor, "kind": akind}) if anchor and anchor.get("begin") else None
|
|
683
733
|
index_claims.append({
|
|
684
734
|
"id": c["id"], "paper": c["paper"], "number": c["number"], "title": c["title"],
|
|
685
735
|
"heading": c["title"] or None, "kind": c["kind"], "scope": c["scope"],
|
|
686
736
|
"status": c["status"], "class": c["class"],
|
|
687
|
-
"anchor":
|
|
688
|
-
"y1": anchor["end"][2] if anchor.get("end") and
|
|
689
|
-
anchor["end"][0] == anchor["begin"][0] else None,
|
|
690
|
-
"page_end": anchor["end"][0] if anchor.get("end") else None}
|
|
691
|
-
if top else None,
|
|
737
|
+
"anchor": mf.index_anchor(anchor, akind),
|
|
692
738
|
"counts": {}, "file": fname, "linked": len(c["links"]),
|
|
693
739
|
"suggested": len(c.get("suggestions", []))})
|
|
694
740
|
names = set()
|
|
@@ -16,6 +16,7 @@ from dataclasses import dataclass
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
|
|
18
18
|
from .config import Config, Paper
|
|
19
|
+
from .util import link_or_copy
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class SourceError(RuntimeError):
|
|
@@ -49,6 +50,24 @@ def _latex_roots(d: Path) -> list[str]:
|
|
|
49
50
|
return roots
|
|
50
51
|
|
|
51
52
|
|
|
53
|
+
SKIP = (".git", ".lake", ".machinechecked", ".prooflens")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _has_latex_root(d: Path) -> bool:
|
|
57
|
+
"""Whether a copy of `d` (without the directories a snapshot leaves out) has a LaTeX root."""
|
|
58
|
+
for top, dirs, files in os.walk(d):
|
|
59
|
+
dirs[:] = [x for x in dirs if x not in SKIP]
|
|
60
|
+
for f in files:
|
|
61
|
+
if f.endswith(".tex"):
|
|
62
|
+
try:
|
|
63
|
+
if DOCCLASS.search((Path(top) / f).read_text(encoding="utf-8",
|
|
64
|
+
errors="replace")):
|
|
65
|
+
return True
|
|
66
|
+
except OSError:
|
|
67
|
+
continue
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
|
|
52
71
|
def _pdfs(d: Path) -> list[str]:
|
|
53
72
|
return sorted(str(p.relative_to(d)) for p in d.rglob("*.pdf"))
|
|
54
73
|
|
|
@@ -105,10 +124,15 @@ def acquire(cfg: Config, paper: Paper, dest: Path) -> Snapshot:
|
|
|
105
124
|
base = (cfg.root / src["path"]).resolve()
|
|
106
125
|
if not base.is_dir():
|
|
107
126
|
raise SourceError(f"paper {paper.id}: {base} is not a directory")
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
127
|
+
if paper.pdf and not paper.root and (base / paper.pdf).is_file() \
|
|
128
|
+
and not _has_latex_root(base):
|
|
129
|
+
# A paper that is only a PDF needs only that file, not the directory it is in (the
|
|
130
|
+
# source would be read as the same PDF either way).
|
|
131
|
+
link_or_copy(base / paper.pdf, dest / paper.pdf)
|
|
132
|
+
else:
|
|
133
|
+
shutil.copytree(base, dest, dirs_exist_ok=True, copy_function=shutil.copy2,
|
|
134
|
+
ignore_dangling_symlinks=True,
|
|
135
|
+
ignore=shutil.ignore_patterns(*SKIP, "*.aux", "*.log", "*.fls"))
|
|
112
136
|
kind, rev, commit_time = "path", None, None
|
|
113
137
|
elif "git" in src:
|
|
114
138
|
rev = src.get("rev")
|
|
@@ -4,7 +4,9 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import json
|
|
7
|
+
import os
|
|
7
8
|
import re
|
|
9
|
+
import shutil
|
|
8
10
|
import subprocess
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
|
|
@@ -64,3 +66,33 @@ def write_json(path: str | Path, obj, indent: int | None = 1) -> None:
|
|
|
64
66
|
|
|
65
67
|
def read_json(path: str | Path):
|
|
66
68
|
return json.loads(Path(path).read_text(encoding="utf-8"))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def link_or_copy(src: str | Path, dst: str | Path) -> None:
|
|
72
|
+
"""Put `src` at `dst` as a hard link (the file is then stored once), else as a copy."""
|
|
73
|
+
dst = Path(dst)
|
|
74
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
dst.unlink(missing_ok=True)
|
|
76
|
+
try:
|
|
77
|
+
os.link(src, dst)
|
|
78
|
+
except OSError:
|
|
79
|
+
shutil.copy2(src, dst)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def prepare_work_dir(d: str | Path) -> Path:
|
|
83
|
+
"""Create the working directory (`.machinechecked/`) and have Git ignore it, as Lake does
|
|
84
|
+
`.lake/`: a `.gitignore` of `*` inside it, unless it has one or Git already tracks files in
|
|
85
|
+
it."""
|
|
86
|
+
d = Path(d)
|
|
87
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
ignore = d / ".gitignore"
|
|
89
|
+
if not ignore.exists():
|
|
90
|
+
try:
|
|
91
|
+
tracked = subprocess.run(["git", "ls-files", "--", "."], cwd=d, capture_output=True,
|
|
92
|
+
text=True).stdout.strip()
|
|
93
|
+
except OSError: # no Git: nothing can be tracked
|
|
94
|
+
tracked = ""
|
|
95
|
+
if not tracked:
|
|
96
|
+
ignore.write_text("# MachineChecked's working files, never committed.\n*\n")
|
|
97
|
+
return d
|
|
98
|
+
|