proof-engine-wiki 1.33.0__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.
- proof_engine_wiki-1.33.0/LICENSE +21 -0
- proof_engine_wiki-1.33.0/PKG-INFO +83 -0
- proof_engine_wiki-1.33.0/README.md +42 -0
- proof_engine_wiki-1.33.0/pyproject.toml +34 -0
- proof_engine_wiki-1.33.0/setup.cfg +4 -0
- proof_engine_wiki-1.33.0/src/proof_engine_wiki/__init__.py +3 -0
- proof_engine_wiki-1.33.0/src/proof_engine_wiki/cli.py +101 -0
- proof_engine_wiki-1.33.0/src/proof_engine_wiki/ingest.py +138 -0
- proof_engine_wiki-1.33.0/src/proof_engine_wiki/lint.py +76 -0
- proof_engine_wiki-1.33.0/src/proof_engine_wiki/markers.py +90 -0
- proof_engine_wiki-1.33.0/src/proof_engine_wiki.egg-info/PKG-INFO +83 -0
- proof_engine_wiki-1.33.0/src/proof_engine_wiki.egg-info/SOURCES.txt +18 -0
- proof_engine_wiki-1.33.0/src/proof_engine_wiki.egg-info/dependency_links.txt +1 -0
- proof_engine_wiki-1.33.0/src/proof_engine_wiki.egg-info/entry_points.txt +2 -0
- proof_engine_wiki-1.33.0/src/proof_engine_wiki.egg-info/requires.txt +3 -0
- proof_engine_wiki-1.33.0/src/proof_engine_wiki.egg-info/top_level.txt +1 -0
- proof_engine_wiki-1.33.0/tests/test_cli.py +39 -0
- proof_engine_wiki-1.33.0/tests/test_ingest.py +88 -0
- proof_engine_wiki-1.33.0/tests/test_lint.py +35 -0
- proof_engine_wiki-1.33.0/tests/test_markers.py +90 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yaniv Golan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: proof-engine-wiki
|
|
3
|
+
Version: 1.33.0
|
|
4
|
+
Summary: Attach verified Proof Engine proofs to LLM-wiki claims.
|
|
5
|
+
Author-email: Yaniv Golan <yaniv@lool.vc>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Yaniv Golan
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://proofengine.info
|
|
29
|
+
Project-URL: Repository, https://github.com/yaniv-golan/proof-engine
|
|
30
|
+
Project-URL: Documentation, https://github.com/yaniv-golan/proof-engine/blob/main/packages/proof-engine-wiki/README.md
|
|
31
|
+
Project-URL: Issues, https://github.com/yaniv-golan/proof-engine/issues
|
|
32
|
+
Project-URL: Changelog, https://github.com/yaniv-golan/proof-engine/blob/main/CHANGELOG.md
|
|
33
|
+
Project-URL: Source, https://github.com/yaniv-golan/proof-engine/tree/main/packages/proof-engine-wiki
|
|
34
|
+
Requires-Python: >=3.11
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
License-File: LICENSE
|
|
37
|
+
Requires-Dist: proof-citations>=0.1
|
|
38
|
+
Requires-Dist: proof-engine-registry>=0.1
|
|
39
|
+
Requires-Dist: requests>=2.31
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# proof-engine-wiki
|
|
43
|
+
|
|
44
|
+
Attach Proof Engine proofs to LLM-wiki claims.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
pip install proof-engine-wiki
|
|
49
|
+
|
|
50
|
+
## Marker syntax
|
|
51
|
+
|
|
52
|
+
Authors mark claims they want proven:
|
|
53
|
+
|
|
54
|
+
The US dollar has {{prove: lost 15% of its purchasing power since 2020}}.
|
|
55
|
+
|
|
56
|
+
## Ingest
|
|
57
|
+
|
|
58
|
+
proof-engine-wiki ingest PAGE.md
|
|
59
|
+
|
|
60
|
+
Extracts all `{{prove:}}` markers, looks up configured registries,
|
|
61
|
+
commissions a new proof only for misses, rewrites the file with inline
|
|
62
|
+
badges and links.
|
|
63
|
+
|
|
64
|
+
## Lint
|
|
65
|
+
|
|
66
|
+
proof-engine-wiki lint WIKI_DIR/
|
|
67
|
+
|
|
68
|
+
Scans every `.md` file under the directory and reports:
|
|
69
|
+
|
|
70
|
+
- `unresolved_marker` — `{{prove:}}` markers in the page that haven't
|
|
71
|
+
been resolved yet (run `ingest` to resolve them).
|
|
72
|
+
- `stale_proof` — embedded proof URLs that no longer respond to a HEAD
|
|
73
|
+
request (the proof was retracted or the registry moved).
|
|
74
|
+
|
|
75
|
+
Pass `--skip-network` to suppress URL reachability checks (faster CI;
|
|
76
|
+
catches only `unresolved_marker`).
|
|
77
|
+
|
|
78
|
+
## Registry-only mode
|
|
79
|
+
|
|
80
|
+
Add `--registry-only` to `ingest` to skip new-proof commissioning
|
|
81
|
+
entirely. Useful for CI: if every `{{prove:}}` claim already has a
|
|
82
|
+
registered proof, ingest runs offline and quickly. Misses are reported,
|
|
83
|
+
not commissioned.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# proof-engine-wiki
|
|
2
|
+
|
|
3
|
+
Attach Proof Engine proofs to LLM-wiki claims.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
pip install proof-engine-wiki
|
|
8
|
+
|
|
9
|
+
## Marker syntax
|
|
10
|
+
|
|
11
|
+
Authors mark claims they want proven:
|
|
12
|
+
|
|
13
|
+
The US dollar has {{prove: lost 15% of its purchasing power since 2020}}.
|
|
14
|
+
|
|
15
|
+
## Ingest
|
|
16
|
+
|
|
17
|
+
proof-engine-wiki ingest PAGE.md
|
|
18
|
+
|
|
19
|
+
Extracts all `{{prove:}}` markers, looks up configured registries,
|
|
20
|
+
commissions a new proof only for misses, rewrites the file with inline
|
|
21
|
+
badges and links.
|
|
22
|
+
|
|
23
|
+
## Lint
|
|
24
|
+
|
|
25
|
+
proof-engine-wiki lint WIKI_DIR/
|
|
26
|
+
|
|
27
|
+
Scans every `.md` file under the directory and reports:
|
|
28
|
+
|
|
29
|
+
- `unresolved_marker` — `{{prove:}}` markers in the page that haven't
|
|
30
|
+
been resolved yet (run `ingest` to resolve them).
|
|
31
|
+
- `stale_proof` — embedded proof URLs that no longer respond to a HEAD
|
|
32
|
+
request (the proof was retracted or the registry moved).
|
|
33
|
+
|
|
34
|
+
Pass `--skip-network` to suppress URL reachability checks (faster CI;
|
|
35
|
+
catches only `unresolved_marker`).
|
|
36
|
+
|
|
37
|
+
## Registry-only mode
|
|
38
|
+
|
|
39
|
+
Add `--registry-only` to `ingest` to skip new-proof commissioning
|
|
40
|
+
entirely. Useful for CI: if every `{{prove:}}` claim already has a
|
|
41
|
+
registered proof, ingest runs offline and quickly. Misses are reported,
|
|
42
|
+
not commissioned.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "proof-engine-wiki"
|
|
7
|
+
version = "1.33.0"
|
|
8
|
+
description = "Attach verified Proof Engine proofs to LLM-wiki claims."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
authors = [{ name = "Yaniv Golan", email = "yaniv@lool.vc" }]
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"proof-citations>=0.1",
|
|
15
|
+
"proof-engine-registry>=0.1",
|
|
16
|
+
"requests>=2.31",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
proof-engine-wiki = "proof_engine_wiki.cli:main"
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://proofengine.info"
|
|
24
|
+
Repository = "https://github.com/yaniv-golan/proof-engine"
|
|
25
|
+
Documentation = "https://github.com/yaniv-golan/proof-engine/blob/main/packages/proof-engine-wiki/README.md"
|
|
26
|
+
Issues = "https://github.com/yaniv-golan/proof-engine/issues"
|
|
27
|
+
Changelog = "https://github.com/yaniv-golan/proof-engine/blob/main/CHANGELOG.md"
|
|
28
|
+
Source = "https://github.com/yaniv-golan/proof-engine/tree/main/packages/proof-engine-wiki"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["src"]
|
|
32
|
+
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""proof-engine-wiki CLI: ingest | lint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import dataclasses
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from proof_engine_wiki import __version__
|
|
13
|
+
except ImportError:
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
|
|
16
|
+
from proof_engine_wiki.ingest import ingest_page
|
|
17
|
+
from proof_engine_wiki.lint import lint_wiki
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _cmd_ingest(args) -> int:
|
|
21
|
+
result = ingest_page(
|
|
22
|
+
Path(args.path),
|
|
23
|
+
registry_only=args.registry_only,
|
|
24
|
+
dry_run=args.dry_run,
|
|
25
|
+
model=args.model,
|
|
26
|
+
)
|
|
27
|
+
payload = {
|
|
28
|
+
"path": str(result.path),
|
|
29
|
+
"markers": len(result.markers),
|
|
30
|
+
"resolved_from_registry": result.resolved_from_registry,
|
|
31
|
+
"generated": result.generated,
|
|
32
|
+
"misses": result.misses,
|
|
33
|
+
"errors": result.errors,
|
|
34
|
+
}
|
|
35
|
+
if args.json:
|
|
36
|
+
sys.stdout.write(json.dumps(payload, indent=2 if args.pretty else None) + "\n")
|
|
37
|
+
else:
|
|
38
|
+
sys.stdout.write(
|
|
39
|
+
f"{result.path}: {len(result.markers)} markers, "
|
|
40
|
+
f"{result.resolved_from_registry} resolved, "
|
|
41
|
+
f"{result.generated} generated, "
|
|
42
|
+
f"{result.misses} misses\n"
|
|
43
|
+
)
|
|
44
|
+
return 0 if result.misses == 0 and not result.errors else 1
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _cmd_lint(args) -> int:
|
|
48
|
+
findings = lint_wiki(Path(args.path), skip_network=args.skip_network)
|
|
49
|
+
payload = {
|
|
50
|
+
"path": str(args.path),
|
|
51
|
+
"findings": [
|
|
52
|
+
{
|
|
53
|
+
"path": str(f.path), "line": f.line,
|
|
54
|
+
"kind": f.kind, "message": f.message, "detail": f.detail,
|
|
55
|
+
}
|
|
56
|
+
for f in findings
|
|
57
|
+
],
|
|
58
|
+
}
|
|
59
|
+
if args.json:
|
|
60
|
+
sys.stdout.write(json.dumps(payload, indent=2 if args.pretty else None) + "\n")
|
|
61
|
+
else:
|
|
62
|
+
for f in findings:
|
|
63
|
+
sys.stdout.write(f"{f.path}:{f.line}: [{f.kind}] {f.message}\n")
|
|
64
|
+
if not findings:
|
|
65
|
+
sys.stdout.write("clean\n")
|
|
66
|
+
return 0 if not findings else 1
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
70
|
+
p = argparse.ArgumentParser(prog="proof-engine-wiki")
|
|
71
|
+
p.add_argument("--version", action="version", version=__version__)
|
|
72
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
73
|
+
|
|
74
|
+
ing = sub.add_parser("ingest", help="Extract {{prove:}} markers and resolve them.")
|
|
75
|
+
ing.add_argument("path")
|
|
76
|
+
ing.add_argument("--registry-only", action="store_true")
|
|
77
|
+
ing.add_argument("--dry-run", action="store_true")
|
|
78
|
+
ing.add_argument("--model", default="sonnet")
|
|
79
|
+
ing.add_argument("--json", action="store_true")
|
|
80
|
+
ing.add_argument("--pretty", action="store_true")
|
|
81
|
+
ing.set_defaults(func=_cmd_ingest)
|
|
82
|
+
|
|
83
|
+
lnt = sub.add_parser("lint", help="Scan a wiki for unresolved markers and stale proofs.")
|
|
84
|
+
lnt.add_argument("path")
|
|
85
|
+
lnt.add_argument("--skip-network", action="store_true",
|
|
86
|
+
help="Skip URL reachability checks.")
|
|
87
|
+
lnt.add_argument("--json", action="store_true")
|
|
88
|
+
lnt.add_argument("--pretty", action="store_true")
|
|
89
|
+
lnt.set_defaults(func=_cmd_lint)
|
|
90
|
+
|
|
91
|
+
return p
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def main(argv=None) -> int:
|
|
95
|
+
parser = build_parser()
|
|
96
|
+
args = parser.parse_args(argv)
|
|
97
|
+
return args.func(args)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Ingest: extract {{prove:}} markers, resolve or commission, rewrite page."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from proof_engine_registry.client import RegistryClient, LookupHit
|
|
12
|
+
from proof_engine_registry.config import Registry, load_registries
|
|
13
|
+
|
|
14
|
+
from proof_engine_wiki.markers import Marker, find_markers, replace_markers
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class IngestResult:
|
|
19
|
+
path: Path
|
|
20
|
+
markers: list[Marker]
|
|
21
|
+
resolved_from_registry: int = 0
|
|
22
|
+
generated: int = 0
|
|
23
|
+
misses: int = 0
|
|
24
|
+
errors: list[str] = field(default_factory=list)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _hit_to_embed(hit: LookupHit, claim_text: str) -> str:
|
|
28
|
+
"""Format a successful lookup as inline Markdown: link + badge."""
|
|
29
|
+
return (
|
|
30
|
+
f"[{claim_text}]({hit.proof_url}) "
|
|
31
|
+
f"})"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def ingest_page(
|
|
36
|
+
path: Path,
|
|
37
|
+
registries: Optional[list[Registry]] = None,
|
|
38
|
+
*,
|
|
39
|
+
registry_only: bool = False,
|
|
40
|
+
dry_run: bool = False,
|
|
41
|
+
model: str = "sonnet",
|
|
42
|
+
proof_output_base: Optional[Path] = None,
|
|
43
|
+
) -> IngestResult:
|
|
44
|
+
path = Path(path)
|
|
45
|
+
original_text = path.read_text()
|
|
46
|
+
markers = find_markers(original_text)
|
|
47
|
+
|
|
48
|
+
result = IngestResult(path=path, markers=markers)
|
|
49
|
+
if not markers:
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
regs = registries if registries is not None else load_registries()
|
|
53
|
+
client = RegistryClient(regs) if regs else None
|
|
54
|
+
|
|
55
|
+
replacements: dict[tuple[int, int], str] = {}
|
|
56
|
+
|
|
57
|
+
for m in markers:
|
|
58
|
+
hit = client.lookup(m.claim) if client else None
|
|
59
|
+
if hit is not None:
|
|
60
|
+
replacements[m.span] = _hit_to_embed(hit, m.claim)
|
|
61
|
+
result.resolved_from_registry += 1
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
if registry_only:
|
|
65
|
+
result.misses += 1
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
# Commission a new proof via the Proof Engine verify CLI.
|
|
69
|
+
if proof_output_base is None:
|
|
70
|
+
proof_output_base = path.parent / ".proofs"
|
|
71
|
+
proof_output_base.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
output_dir = proof_output_base / _slugify(m.claim)
|
|
73
|
+
|
|
74
|
+
verdict = _invoke_verify(m.claim, model, output_dir)
|
|
75
|
+
if verdict is None:
|
|
76
|
+
result.errors.append(f"verify failed for: {m.claim[:80]}")
|
|
77
|
+
result.misses += 1
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
# After generation, the proof isn't yet in any registry; produce a
|
|
81
|
+
# local link to the output dir until the user publishes.
|
|
82
|
+
embed = (
|
|
83
|
+
f"[{m.claim}]({output_dir.as_posix()}/proof.md) "
|
|
84
|
+
f"<!-- Proof generated; run `proof-engine-wiki publish "
|
|
85
|
+
f"{output_dir}` to add to your registry. -->"
|
|
86
|
+
)
|
|
87
|
+
replacements[m.span] = embed
|
|
88
|
+
result.generated += 1
|
|
89
|
+
|
|
90
|
+
if replacements and not dry_run:
|
|
91
|
+
new_text = replace_markers(original_text, replacements)
|
|
92
|
+
path.write_text(new_text)
|
|
93
|
+
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _slugify(text: str) -> str:
|
|
98
|
+
import re
|
|
99
|
+
s = re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
|
|
100
|
+
return s[:60] or "claim"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _invoke_verify(claim: str, model: str, output_dir: Path) -> Optional[dict]:
|
|
104
|
+
"""Call `proof-engine verify` and parse the JSON verdict."""
|
|
105
|
+
import json
|
|
106
|
+
script = _find_verify_cli()
|
|
107
|
+
if script is None:
|
|
108
|
+
return None
|
|
109
|
+
proc = subprocess.run(
|
|
110
|
+
[sys.executable, str(script),
|
|
111
|
+
"--claim", claim,
|
|
112
|
+
"--model", model,
|
|
113
|
+
"--output-dir", str(output_dir),
|
|
114
|
+
"--json"],
|
|
115
|
+
capture_output=True, text=True,
|
|
116
|
+
)
|
|
117
|
+
if not proc.stdout:
|
|
118
|
+
return None
|
|
119
|
+
try:
|
|
120
|
+
return json.loads(proc.stdout)
|
|
121
|
+
except json.JSONDecodeError:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _find_verify_cli() -> Optional[Path]:
|
|
126
|
+
"""Locate the verify_cli.py script. Search the same repo first."""
|
|
127
|
+
import os
|
|
128
|
+
env_path = os.environ.get("PROOF_ENGINE_VERIFY_CLI")
|
|
129
|
+
if env_path:
|
|
130
|
+
p = Path(env_path)
|
|
131
|
+
if p.exists():
|
|
132
|
+
return p
|
|
133
|
+
# Walk up from this file looking for tools/verify_cli.py in the repo.
|
|
134
|
+
for parent in Path(__file__).resolve().parents:
|
|
135
|
+
candidate = parent / "tools" / "verify_cli.py"
|
|
136
|
+
if candidate.exists():
|
|
137
|
+
return candidate
|
|
138
|
+
return None
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Lint a wiki directory: unresolved markers, stale proofs, broken badges."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from proof_engine_wiki.markers import find_markers
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_PROOF_LINK_RE = re.compile(
|
|
13
|
+
r"\[([^\]]+)\]\((?P<url>https?://[^)]+/proofs/[^)]+/)\)"
|
|
14
|
+
)
|
|
15
|
+
_BADGE_IMG_RE = re.compile(
|
|
16
|
+
r"!\[proof\]\((?P<url>https?://[^)]+/badge\.svg)\)"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class LintFinding:
|
|
22
|
+
path: Path
|
|
23
|
+
line: int
|
|
24
|
+
kind: str # unresolved_marker | stale_proof | badge_unreachable
|
|
25
|
+
message: str
|
|
26
|
+
detail: str = ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def lint_wiki(
|
|
30
|
+
root: Path,
|
|
31
|
+
*,
|
|
32
|
+
skip_network: bool = False,
|
|
33
|
+
) -> list[LintFinding]:
|
|
34
|
+
root = Path(root)
|
|
35
|
+
findings: list[LintFinding] = []
|
|
36
|
+
for md in sorted(root.rglob("*.md")):
|
|
37
|
+
findings.extend(_lint_file(md, skip_network=skip_network))
|
|
38
|
+
return findings
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _lint_file(path: Path, *, skip_network: bool) -> list[LintFinding]:
|
|
42
|
+
text = path.read_text()
|
|
43
|
+
out: list[LintFinding] = []
|
|
44
|
+
|
|
45
|
+
# Unresolved markers.
|
|
46
|
+
for m in find_markers(text):
|
|
47
|
+
line = text[: m.span[0]].count("\n") + 1
|
|
48
|
+
out.append(LintFinding(
|
|
49
|
+
path=path, line=line,
|
|
50
|
+
kind="unresolved_marker",
|
|
51
|
+
message=f"unresolved {{{{prove:}}}} marker",
|
|
52
|
+
detail=m.claim,
|
|
53
|
+
))
|
|
54
|
+
|
|
55
|
+
# Optionally verify each embedded proof link is still reachable.
|
|
56
|
+
if not skip_network:
|
|
57
|
+
for m in _PROOF_LINK_RE.finditer(text):
|
|
58
|
+
url = m.group("url")
|
|
59
|
+
if not _url_alive(url):
|
|
60
|
+
line = text[: m.start()].count("\n") + 1
|
|
61
|
+
out.append(LintFinding(
|
|
62
|
+
path=path, line=line,
|
|
63
|
+
kind="stale_proof",
|
|
64
|
+
message=f"proof URL not reachable: {url}",
|
|
65
|
+
))
|
|
66
|
+
|
|
67
|
+
return out
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _url_alive(url: str, timeout: float = 5.0) -> bool:
|
|
71
|
+
import requests
|
|
72
|
+
try:
|
|
73
|
+
r = requests.head(url, timeout=timeout, allow_redirects=True)
|
|
74
|
+
return r.status_code < 400
|
|
75
|
+
except Exception:
|
|
76
|
+
return False
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Parser and rewriter for `{{prove: ...}}` markers in wiki prose.
|
|
2
|
+
|
|
3
|
+
Markers are the explicit signal from the author to attach a proof to a claim.
|
|
4
|
+
They are regex-greppable by design — no NLP needed at this layer.
|
|
5
|
+
|
|
6
|
+
Markers inside fenced code blocks (``` ... ```), inline HTML comments
|
|
7
|
+
(<!-- ... -->), and YAML frontmatter at the top of the file are masked
|
|
8
|
+
before matching. Wiki authors who write prose ABOUT the marker syntax
|
|
9
|
+
(documenting it, showing examples) should not accidentally commission a proof.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Non-greedy match, allows any content between `prove:` and `}}`, including
|
|
19
|
+
# punctuation. Does NOT cross paragraph boundaries (no `\n\n`).
|
|
20
|
+
_PATTERN = re.compile(
|
|
21
|
+
r"\{\{\s*prove\s*:\s*(?P<claim>(?:(?!\}\}|\n\n).)+?)\s*\}\}",
|
|
22
|
+
re.DOTALL,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Masking patterns — each matches a region in which markers should be ignored.
|
|
26
|
+
_CODE_FENCE = re.compile(r"```.*?```", re.DOTALL)
|
|
27
|
+
_INLINE_CODE = re.compile(r"`[^`\n]+`")
|
|
28
|
+
_HTML_COMMENT = re.compile(r"<!--.*?-->", re.DOTALL)
|
|
29
|
+
# YAML frontmatter: leading `---\n...\n---` at file start.
|
|
30
|
+
_FRONTMATTER = re.compile(r"\A---\n.*?\n---\n", re.DOTALL)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class Marker:
|
|
35
|
+
claim: str
|
|
36
|
+
# Half-open range in the source text: text[span[0]:span[1]] equals the
|
|
37
|
+
# full `{{prove: ...}}` marker. Matches re.Match.start()/end() semantics.
|
|
38
|
+
span: tuple[int, int]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _mask_excluded_regions(text: str) -> str:
|
|
42
|
+
"""Return text with excluded regions replaced by spaces of the same length.
|
|
43
|
+
|
|
44
|
+
Same-length replacement preserves offsets so downstream span arithmetic
|
|
45
|
+
still matches the original text.
|
|
46
|
+
"""
|
|
47
|
+
out = list(text)
|
|
48
|
+
for pat in (_FRONTMATTER, _CODE_FENCE, _INLINE_CODE, _HTML_COMMENT):
|
|
49
|
+
for m in pat.finditer(text):
|
|
50
|
+
for i in range(m.start(), m.end()):
|
|
51
|
+
# Keep newlines to preserve line numbers in lint output.
|
|
52
|
+
if out[i] != "\n":
|
|
53
|
+
out[i] = " "
|
|
54
|
+
return "".join(out)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def find_markers(text: str) -> list[Marker]:
|
|
58
|
+
"""Find `{{prove: ...}}` markers, skipping code blocks, comments, frontmatter."""
|
|
59
|
+
masked = _mask_excluded_regions(text)
|
|
60
|
+
out: list[Marker] = []
|
|
61
|
+
for m in _PATTERN.finditer(masked):
|
|
62
|
+
# Claim text must come from the ORIGINAL string, not the masked copy —
|
|
63
|
+
# a marker that straddles into an inline code span would be masked,
|
|
64
|
+
# but a marker fully outside gives the correct claim text from `text`.
|
|
65
|
+
claim = text[m.start():m.end()]
|
|
66
|
+
# Re-parse the claim out of the full marker with the same pattern.
|
|
67
|
+
inner = _PATTERN.fullmatch(claim)
|
|
68
|
+
if inner is None:
|
|
69
|
+
continue
|
|
70
|
+
out.append(Marker(
|
|
71
|
+
claim=inner.group("claim").strip(),
|
|
72
|
+
span=(m.start(), m.end()),
|
|
73
|
+
))
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def replace_markers(text: str, replacements: dict[tuple[int, int], str]) -> str:
|
|
78
|
+
"""Replace each (start, end) span with the given string.
|
|
79
|
+
|
|
80
|
+
Spans must not overlap. Iterates back-to-front so earlier spans aren't
|
|
81
|
+
invalidated by replacements.
|
|
82
|
+
"""
|
|
83
|
+
if not replacements:
|
|
84
|
+
return text
|
|
85
|
+
sorted_spans = sorted(replacements.keys(), key=lambda s: s[0], reverse=True)
|
|
86
|
+
out = text
|
|
87
|
+
for span in sorted_spans:
|
|
88
|
+
start, end = span
|
|
89
|
+
out = out[:start] + replacements[span] + out[end:]
|
|
90
|
+
return out
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: proof-engine-wiki
|
|
3
|
+
Version: 1.33.0
|
|
4
|
+
Summary: Attach verified Proof Engine proofs to LLM-wiki claims.
|
|
5
|
+
Author-email: Yaniv Golan <yaniv@lool.vc>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Yaniv Golan
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://proofengine.info
|
|
29
|
+
Project-URL: Repository, https://github.com/yaniv-golan/proof-engine
|
|
30
|
+
Project-URL: Documentation, https://github.com/yaniv-golan/proof-engine/blob/main/packages/proof-engine-wiki/README.md
|
|
31
|
+
Project-URL: Issues, https://github.com/yaniv-golan/proof-engine/issues
|
|
32
|
+
Project-URL: Changelog, https://github.com/yaniv-golan/proof-engine/blob/main/CHANGELOG.md
|
|
33
|
+
Project-URL: Source, https://github.com/yaniv-golan/proof-engine/tree/main/packages/proof-engine-wiki
|
|
34
|
+
Requires-Python: >=3.11
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
License-File: LICENSE
|
|
37
|
+
Requires-Dist: proof-citations>=0.1
|
|
38
|
+
Requires-Dist: proof-engine-registry>=0.1
|
|
39
|
+
Requires-Dist: requests>=2.31
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# proof-engine-wiki
|
|
43
|
+
|
|
44
|
+
Attach Proof Engine proofs to LLM-wiki claims.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
pip install proof-engine-wiki
|
|
49
|
+
|
|
50
|
+
## Marker syntax
|
|
51
|
+
|
|
52
|
+
Authors mark claims they want proven:
|
|
53
|
+
|
|
54
|
+
The US dollar has {{prove: lost 15% of its purchasing power since 2020}}.
|
|
55
|
+
|
|
56
|
+
## Ingest
|
|
57
|
+
|
|
58
|
+
proof-engine-wiki ingest PAGE.md
|
|
59
|
+
|
|
60
|
+
Extracts all `{{prove:}}` markers, looks up configured registries,
|
|
61
|
+
commissions a new proof only for misses, rewrites the file with inline
|
|
62
|
+
badges and links.
|
|
63
|
+
|
|
64
|
+
## Lint
|
|
65
|
+
|
|
66
|
+
proof-engine-wiki lint WIKI_DIR/
|
|
67
|
+
|
|
68
|
+
Scans every `.md` file under the directory and reports:
|
|
69
|
+
|
|
70
|
+
- `unresolved_marker` — `{{prove:}}` markers in the page that haven't
|
|
71
|
+
been resolved yet (run `ingest` to resolve them).
|
|
72
|
+
- `stale_proof` — embedded proof URLs that no longer respond to a HEAD
|
|
73
|
+
request (the proof was retracted or the registry moved).
|
|
74
|
+
|
|
75
|
+
Pass `--skip-network` to suppress URL reachability checks (faster CI;
|
|
76
|
+
catches only `unresolved_marker`).
|
|
77
|
+
|
|
78
|
+
## Registry-only mode
|
|
79
|
+
|
|
80
|
+
Add `--registry-only` to `ingest` to skip new-proof commissioning
|
|
81
|
+
entirely. Useful for CI: if every `{{prove:}}` claim already has a
|
|
82
|
+
registered proof, ingest runs offline and quickly. Misses are reported,
|
|
83
|
+
not commissioned.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/proof_engine_wiki/__init__.py
|
|
5
|
+
src/proof_engine_wiki/cli.py
|
|
6
|
+
src/proof_engine_wiki/ingest.py
|
|
7
|
+
src/proof_engine_wiki/lint.py
|
|
8
|
+
src/proof_engine_wiki/markers.py
|
|
9
|
+
src/proof_engine_wiki.egg-info/PKG-INFO
|
|
10
|
+
src/proof_engine_wiki.egg-info/SOURCES.txt
|
|
11
|
+
src/proof_engine_wiki.egg-info/dependency_links.txt
|
|
12
|
+
src/proof_engine_wiki.egg-info/entry_points.txt
|
|
13
|
+
src/proof_engine_wiki.egg-info/requires.txt
|
|
14
|
+
src/proof_engine_wiki.egg-info/top_level.txt
|
|
15
|
+
tests/test_cli.py
|
|
16
|
+
tests/test_ingest.py
|
|
17
|
+
tests/test_lint.py
|
|
18
|
+
tests/test_markers.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
proof_engine_wiki
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_cli_help():
|
|
9
|
+
r = subprocess.run(
|
|
10
|
+
[sys.executable, "-m", "proof_engine_wiki.cli", "--help"],
|
|
11
|
+
capture_output=True, text=True,
|
|
12
|
+
)
|
|
13
|
+
assert r.returncode == 0
|
|
14
|
+
assert "ingest" in r.stdout
|
|
15
|
+
assert "lint" in r.stdout
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_cli_lint_empty_dir(tmp_path):
|
|
19
|
+
r = subprocess.run(
|
|
20
|
+
[sys.executable, "-m", "proof_engine_wiki.cli", "lint", str(tmp_path), "--json"],
|
|
21
|
+
capture_output=True, text=True,
|
|
22
|
+
)
|
|
23
|
+
assert r.returncode == 0
|
|
24
|
+
payload = json.loads(r.stdout)
|
|
25
|
+
assert payload["findings"] == []
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_cli_lint_finds_unresolved_marker(tmp_path):
|
|
29
|
+
(tmp_path / "page.md").write_text("X {{prove: claim}} Y")
|
|
30
|
+
r = subprocess.run(
|
|
31
|
+
[sys.executable, "-m", "proof_engine_wiki.cli", "lint", str(tmp_path),
|
|
32
|
+
"--skip-network", "--json"],
|
|
33
|
+
capture_output=True, text=True,
|
|
34
|
+
)
|
|
35
|
+
# Findings present → exit code 1 (lint failed).
|
|
36
|
+
assert r.returncode == 1
|
|
37
|
+
payload = json.loads(r.stdout)
|
|
38
|
+
kinds = [f["kind"] for f in payload["findings"]]
|
|
39
|
+
assert "unresolved_marker" in kinds
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import http.server
|
|
2
|
+
import os
|
|
3
|
+
import socketserver
|
|
4
|
+
import threading
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from proof_engine_registry.config import Registry
|
|
11
|
+
from proof_engine_registry.emit import emit_registry_files
|
|
12
|
+
|
|
13
|
+
from proof_engine_wiki.ingest import ingest_page, IngestResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
REG_FIXTURES = Path(__file__).resolve().parents[2] / "proof-engine-registry" / "tests" / "fixtures" / "proofs"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@contextmanager
|
|
20
|
+
def _local_registry(tmp_path):
|
|
21
|
+
out = tmp_path / "registry"
|
|
22
|
+
emit_registry_files(
|
|
23
|
+
proofs_dir=REG_FIXTURES, output_dir=out,
|
|
24
|
+
base_url="http://127.0.0.1:0", registry_name="Test",
|
|
25
|
+
)
|
|
26
|
+
original = Path.cwd()
|
|
27
|
+
os.chdir(out)
|
|
28
|
+
try:
|
|
29
|
+
with socketserver.TCPServer(("127.0.0.1", 0),
|
|
30
|
+
http.server.SimpleHTTPRequestHandler) as httpd:
|
|
31
|
+
port = httpd.server_address[1]
|
|
32
|
+
t = threading.Thread(target=httpd.serve_forever, daemon=True)
|
|
33
|
+
t.start()
|
|
34
|
+
try:
|
|
35
|
+
yield [Registry(name="local", url=f"http://127.0.0.1:{port}")]
|
|
36
|
+
finally:
|
|
37
|
+
httpd.shutdown()
|
|
38
|
+
finally:
|
|
39
|
+
os.chdir(original)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_ingest_registry_only_hits_use_existing_proof(tmp_path):
|
|
43
|
+
# Claim text must match the Phase 1b registry fixture's claim_natural
|
|
44
|
+
# exactly ("The sky is blue.") so the lookup is a guaranteed hit.
|
|
45
|
+
page = tmp_path / "claims.md"
|
|
46
|
+
page.write_text("The sky is {{prove: The sky is blue.}}.")
|
|
47
|
+
|
|
48
|
+
with _local_registry(tmp_path) as registries:
|
|
49
|
+
result = ingest_page(
|
|
50
|
+
page,
|
|
51
|
+
registries=registries,
|
|
52
|
+
registry_only=True,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
assert isinstance(result, IngestResult)
|
|
56
|
+
assert len(result.markers) == 1
|
|
57
|
+
assert result.resolved_from_registry == 1
|
|
58
|
+
assert result.generated == 0
|
|
59
|
+
assert result.misses == 0
|
|
60
|
+
# Rewritten content should contain a link and an image (badge).
|
|
61
|
+
rewritten = page.read_text()
|
|
62
|
+
assert "](http://127.0.0.1" in rewritten # link to registry proof URL
|
|
63
|
+
assert "badge.svg" in rewritten
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_ingest_registry_only_reports_misses(tmp_path):
|
|
67
|
+
page = tmp_path / "claims.md"
|
|
68
|
+
page.write_text("A claim: {{prove: definitely not in registry}}.")
|
|
69
|
+
with _local_registry(tmp_path) as registries:
|
|
70
|
+
result = ingest_page(
|
|
71
|
+
page, registries=registries, registry_only=True,
|
|
72
|
+
)
|
|
73
|
+
assert result.misses == 1
|
|
74
|
+
assert result.resolved_from_registry == 0
|
|
75
|
+
# Page is unchanged on miss in registry-only mode — the marker stays.
|
|
76
|
+
assert "{{prove: definitely not in registry}}" in page.read_text()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_ingest_dry_run_does_not_write(tmp_path):
|
|
80
|
+
page = tmp_path / "claims.md"
|
|
81
|
+
page.write_text("X {{prove: The sky is blue.}} Y")
|
|
82
|
+
original = page.read_text()
|
|
83
|
+
with _local_registry(tmp_path) as registries:
|
|
84
|
+
result = ingest_page(
|
|
85
|
+
page, registries=registries, registry_only=True, dry_run=True,
|
|
86
|
+
)
|
|
87
|
+
assert result.resolved_from_registry == 1
|
|
88
|
+
assert page.read_text() == original # untouched
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from proof_engine_wiki.lint import lint_wiki, LintFinding
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_lint_reports_unresolved_markers(tmp_path):
|
|
9
|
+
p = tmp_path / "page.md"
|
|
10
|
+
p.write_text(
|
|
11
|
+
"Some claim {{prove: needs proof}} and another "
|
|
12
|
+
"{{prove: also needs proof}}."
|
|
13
|
+
)
|
|
14
|
+
findings = lint_wiki(tmp_path)
|
|
15
|
+
unresolved = [f for f in findings if f.kind == "unresolved_marker"]
|
|
16
|
+
assert len(unresolved) == 2
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_lint_reports_stale_proof_urls(tmp_path):
|
|
20
|
+
p = tmp_path / "page.md"
|
|
21
|
+
p.write_text(
|
|
22
|
+
"Statement [cited](https://proofengine.info/proofs/purchasing-power-decline/) "
|
|
23
|
+
"."
|
|
24
|
+
)
|
|
25
|
+
# We don't actually fetch in this unit test — lint_wiki supports
|
|
26
|
+
# `skip_network=True` for test purposes.
|
|
27
|
+
findings = lint_wiki(tmp_path, skip_network=True)
|
|
28
|
+
# With skip_network=True, we don't issue stale findings, just confirm
|
|
29
|
+
# that the function completes and returns a list.
|
|
30
|
+
assert isinstance(findings, list)
|
|
31
|
+
assert all(isinstance(f, LintFinding) for f in findings)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_lint_empty_dir_is_clean(tmp_path):
|
|
35
|
+
assert lint_wiki(tmp_path) == []
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from proof_engine_wiki.markers import (
|
|
2
|
+
Marker, find_markers, replace_markers,
|
|
3
|
+
)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_find_single_marker():
|
|
7
|
+
text = "The sky is {{prove: blue during the day}}."
|
|
8
|
+
markers = find_markers(text)
|
|
9
|
+
assert len(markers) == 1
|
|
10
|
+
assert markers[0].claim == "blue during the day"
|
|
11
|
+
# span = (start of `{{`, end-exclusive of `}}`)
|
|
12
|
+
# "The sky is " = 0..10 (11 chars), `{{prove: blue during the day}}` = 30 chars → (11, 41)
|
|
13
|
+
assert markers[0].span == (11, 41)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_find_multiple_markers():
|
|
17
|
+
text = (
|
|
18
|
+
"Revenue grew {{prove: 10% YoY in 2024}}, and losses "
|
|
19
|
+
"narrowed {{prove: by 30% over the same period}}."
|
|
20
|
+
)
|
|
21
|
+
markers = find_markers(text)
|
|
22
|
+
assert len(markers) == 2
|
|
23
|
+
assert markers[0].claim == "10% YoY in 2024"
|
|
24
|
+
assert markers[1].claim == "by 30% over the same period"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_marker_ignores_plain_braces():
|
|
28
|
+
text = "The set {a, b, c} is finite. {{not_a_marker}} leaves it alone."
|
|
29
|
+
assert find_markers(text) == []
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_marker_allows_leading_and_trailing_whitespace():
|
|
33
|
+
text = "{{prove: the claim }}"
|
|
34
|
+
m = find_markers(text)
|
|
35
|
+
assert m[0].claim == "the claim"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_replace_markers_preserves_surrounding_text():
|
|
39
|
+
text = "X {{prove: A}} Y {{prove: B}} Z"
|
|
40
|
+
markers = find_markers(text)
|
|
41
|
+
replacements = {
|
|
42
|
+
markers[0].span: "[A](http://a)",
|
|
43
|
+
markers[1].span: "[B](http://b)",
|
|
44
|
+
}
|
|
45
|
+
rewritten = replace_markers(text, replacements)
|
|
46
|
+
assert rewritten == "X [A](http://a) Y [B](http://b) Z"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_replace_markers_is_idempotent_on_unchanged_input():
|
|
50
|
+
text = "no markers here"
|
|
51
|
+
assert replace_markers(text, {}) == text
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_markers_inside_fenced_code_blocks_are_ignored():
|
|
55
|
+
text = (
|
|
56
|
+
"Real marker: {{prove: this one counts}}.\n"
|
|
57
|
+
"\n"
|
|
58
|
+
"```\n"
|
|
59
|
+
"Example syntax: {{prove: this is documentation}}\n"
|
|
60
|
+
"```\n"
|
|
61
|
+
"\n"
|
|
62
|
+
"And another real one: {{prove: also counts}}."
|
|
63
|
+
)
|
|
64
|
+
claims = [m.claim for m in find_markers(text)]
|
|
65
|
+
assert claims == ["this one counts", "also counts"]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_markers_inside_inline_code_are_ignored():
|
|
69
|
+
text = "Use the `{{prove: x}}` syntax. Real: {{prove: real}}."
|
|
70
|
+
claims = [m.claim for m in find_markers(text)]
|
|
71
|
+
assert claims == ["real"]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_markers_inside_html_comments_are_ignored():
|
|
75
|
+
text = "<!-- {{prove: hidden}} --> visible: {{prove: seen}}."
|
|
76
|
+
claims = [m.claim for m in find_markers(text)]
|
|
77
|
+
assert claims == ["seen"]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_markers_inside_yaml_frontmatter_are_ignored():
|
|
81
|
+
text = (
|
|
82
|
+
"---\n"
|
|
83
|
+
"title: Example\n"
|
|
84
|
+
"note: '{{prove: metadata is not prose}}'\n"
|
|
85
|
+
"---\n"
|
|
86
|
+
"\n"
|
|
87
|
+
"Body: {{prove: body claim}}."
|
|
88
|
+
)
|
|
89
|
+
claims = [m.claim for m in find_markers(text)]
|
|
90
|
+
assert claims == ["body claim"]
|