proof-engine-wiki 1.33.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,11 @@
1
+ proof_engine_wiki/__init__.py,sha256=MZbvZcYrJAKKkCAS5EFzX8-qr_QTPjcuVb7T1YZcbbI,96
2
+ proof_engine_wiki/cli.py,sha256=5hH6Riu7k9tFAeGr752MpF2nSPFsGODavQuXG45Ng4I,3168
3
+ proof_engine_wiki/ingest.py,sha256=-s_aPZhPkvVWx1evZf3BFzPOGKhzMPikM51iC7Jll7I,4199
4
+ proof_engine_wiki/lint.py,sha256=UtlhvzIXbGMCTGB7sB2c5_IuluyJkTybhtZXbrgu63E,2056
5
+ proof_engine_wiki/markers.py,sha256=jEM0aEPKvoudsA7dBGowxcJ197Qdk6tzn8MIL_vPUDA,3363
6
+ proof_engine_wiki-1.33.0.dist-info/licenses/LICENSE,sha256=zTTaGO6alQLutSENMjseDqP5xEDqs69DylIBhe2KsEE,1068
7
+ proof_engine_wiki-1.33.0.dist-info/METADATA,sha256=H09i-yKN9-p-P32878joczOA9duhUgPzqq4vRl5piPs,3304
8
+ proof_engine_wiki-1.33.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
+ proof_engine_wiki-1.33.0.dist-info/entry_points.txt,sha256=gRAOocT4ISwdYYQiCx5qaExzcmFjApfIbfOuEclwfck,65
10
+ proof_engine_wiki-1.33.0.dist-info/top_level.txt,sha256=RiCV_bxWBQsfFbAbnFHYyd7uJbs4gj32OyPGqqxjHbY,18
11
+ proof_engine_wiki-1.33.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ proof-engine-wiki = proof_engine_wiki.cli:main
@@ -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 @@
1
+ proof_engine_wiki