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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """proof-engine-wiki: attach Proof Engine proofs to LLM-wiki claims."""
2
+
3
+ __version__ = "1.33.0"
@@ -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"![proof]({hit.badge_url.replace('.json', '.svg')})"
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,2 @@
1
+ [console_scripts]
2
+ proof-engine-wiki = proof_engine_wiki.cli:main
@@ -0,0 +1,3 @@
1
+ proof-citations>=0.1
2
+ proof-engine-registry>=0.1
3
+ requests>=2.31
@@ -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
+ "![proof](https://proofengine.info/proofs/purchasing-power-decline/badge.svg)."
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"]