forge-proof 0.1.0a5__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,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: forge-proof
3
+ Version: 0.1.0a5
4
+ Summary: Forge reference proof tooling: forge proof check/derive/replay/bundle — a Proof-Standard evidence bundle from events.log alone. Stdlib-only core; optional matplotlib+networkx for graph.png.
5
+ Author-email: Sanjay Davis <psanjuknl@gmail.com>
6
+ License-Expression: MIT
7
+ Keywords: forge,proof,evidence,sdk,autonomous-software
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+
15
+ # forge-proof
16
+
17
+ Reference proof evidence tooling for [Forge](https://github.com/SanjayDavis/Forge):
18
+ a stdlib-only, kernel-free CLI that turns a raw `events.log` into a complete
19
+ [Proof Standard](https://github.com/SanjayDavis/Forge/blob/master/proofs/PROOF_SPEC.md)
20
+ artifact bundle — no manual steps, no LLM in the loop.
21
+
22
+ ```
23
+ forge proof check <dir> validate a bundle against the §6 checklist
24
+ forge proof derive <dir> derive graph.json/metrics.json/replay facts from events.log (§5)
25
+ forge proof replay <dir> render replay.md (Goal/Outcome/Timeline/Turning points)
26
+ forge proof bundle <dir> emit the full bundle + validate (net-new artifacts only;
27
+ curated README/replay.md/media are never clobbered,
28
+ derived artifacts are verified byte-identical)
29
+ ```
30
+
31
+ Installing this package registers the `forge proof` command through the
32
+ `forge.commands` entry-point group — same mechanism as `forge plan` from
33
+ forge-planner.
34
+
35
+ Design constraints (Proof Standard §7 / repo plan):
36
+ - **stdlib-only**: no runtime dependencies; `graph.png` rendering uses
37
+ matplotlib+networkx when present in the invoking python, otherwise a
38
+ clear hint instead of a crash.
39
+ - **kernel-free**: never imports `forge`; the event log is read as raw
40
+ JSON lines, so the proof pipeline cannot disturb project state.
41
+ - **reproducible**: derived artifacts are a pure function of `events.log`
42
+ — byte-identical across runs and byte-identical to the canonical
43
+ `tools/proof-derive.py` (pinned by tests).
@@ -0,0 +1,29 @@
1
+ # forge-proof
2
+
3
+ Reference proof evidence tooling for [Forge](https://github.com/SanjayDavis/Forge):
4
+ a stdlib-only, kernel-free CLI that turns a raw `events.log` into a complete
5
+ [Proof Standard](https://github.com/SanjayDavis/Forge/blob/master/proofs/PROOF_SPEC.md)
6
+ artifact bundle — no manual steps, no LLM in the loop.
7
+
8
+ ```
9
+ forge proof check <dir> validate a bundle against the §6 checklist
10
+ forge proof derive <dir> derive graph.json/metrics.json/replay facts from events.log (§5)
11
+ forge proof replay <dir> render replay.md (Goal/Outcome/Timeline/Turning points)
12
+ forge proof bundle <dir> emit the full bundle + validate (net-new artifacts only;
13
+ curated README/replay.md/media are never clobbered,
14
+ derived artifacts are verified byte-identical)
15
+ ```
16
+
17
+ Installing this package registers the `forge proof` command through the
18
+ `forge.commands` entry-point group — same mechanism as `forge plan` from
19
+ forge-planner.
20
+
21
+ Design constraints (Proof Standard §7 / repo plan):
22
+ - **stdlib-only**: no runtime dependencies; `graph.png` rendering uses
23
+ matplotlib+networkx when present in the invoking python, otherwise a
24
+ clear hint instead of a crash.
25
+ - **kernel-free**: never imports `forge`; the event log is read as raw
26
+ JSON lines, so the proof pipeline cannot disturb project state.
27
+ - **reproducible**: derived artifacts are a pure function of `events.log`
28
+ — byte-identical across runs and byte-identical to the canonical
29
+ `tools/proof-derive.py` (pinned by tests).
@@ -0,0 +1,12 @@
1
+ """forge-proof — the reference proof evidence pipeline.
2
+
3
+ `forge proof check/derive/replay/bundle` over a Proof-Standard bundle
4
+ (proofs/PROOF_SPEC.md): derive machine-readable artifacts from
5
+ events.log alone (§5), render replay.md and graph.png, scaffold README,
6
+ and validate the full §6 conformance checklist. Stdlib-only; the kernel
7
+ stays out of scope — this package is a client of the artifacts, never
8
+ of the event API.
9
+ """
10
+ __version__ = "0.1.0a5"
11
+
12
+ __all__ = ["__version__", "check", "derive", "replay", "bundle"]
@@ -0,0 +1,259 @@
1
+ """`forge proof bundle` — emit a complete Proof-Standard artifact bundle.
2
+
3
+ On a live project dir (events.log present, nothing else): derives
4
+ graph.json/metrics.json/demo/_replay_facts.md, renders replay.md and
5
+ graph.png, scaffolds README.md, then runs the full §6 conformance
6
+ checklist. Run-captured media that a machine cannot synthesize
7
+ (screenshots/, demo.mp4 without a transcript, run.py) are reported as
8
+ gaps, not fabricated.
9
+
10
+ On an existing proof dir (examples/swarm): NEVER clobbers. The derived
11
+ artifacts are re-derived in a temp copy and verified byte-identical (the
12
+ §5 reproducibility rule); the curated README/replay.md/demo.mp4 are left
13
+ untouched; the bundle ends with the conformance verdict.
14
+
15
+ Deps: core derivation is stdlib-only; graph.png rendering additionally
16
+ needs matplotlib+networkx (optional — bundle still succeeds without
17
+ them, graph.png is reported as a gap).
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import hashlib
22
+ import json
23
+ import shutil
24
+ import subprocess
25
+ import sys
26
+ import tempfile
27
+ from pathlib import Path
28
+
29
+ from . import check as proof_check
30
+ from . import derive as proof_derive
31
+ from . import replay as proof_replay
32
+
33
+ _TOOLS = Path(__file__).resolve().parents[3] / "tools"
34
+
35
+
36
+ # ------------------------------------------------------------------ graph.png
37
+ def _render_graph(root: Path) -> bool:
38
+ """Render graph.png from graph.json (in-process, optional matplotlib)."""
39
+ try:
40
+ import matplotlib
41
+ matplotlib.use("Agg")
42
+ import matplotlib.pyplot as plt
43
+ import networkx as nx
44
+ from matplotlib.patches import FancyBboxPatch
45
+ except Exception as e:
46
+ print(f" hint: graph.png not rendered — install matplotlib+networkx ({e})")
47
+ return False
48
+ try:
49
+ data = json.loads((root / "graph.json").read_text(encoding="utf-8"))
50
+ G = nx.DiGraph()
51
+ for t in data["tasks"]:
52
+ G.add_node(t["id"], status=t["status"],
53
+ subsystem=t.get("subsystem", "other"), title=t["title"])
54
+ for e in data["dependencies"]:
55
+ G.add_edge(e["depends_on"], e["task"])
56
+ rank = {}
57
+ for n in G.nodes:
58
+ rank[n] = 0
59
+ changed = True
60
+ while changed:
61
+ changed = False
62
+ for u, v in G.edges:
63
+ if rank[v] < rank[u] + 1:
64
+ rank[v] = rank[u] + 1
65
+ changed = True
66
+ levels = {}
67
+ for n, r in rank.items():
68
+ levels.setdefault(r, []).append(n)
69
+ pos = {}
70
+ for r, nodes in levels.items():
71
+ for i, n in enumerate(sorted(nodes)):
72
+ pos[n] = (i - (len(nodes) - 1) / 2, -r)
73
+ depth = max(rank.values(), default=0)
74
+ fig, ax = plt.subplots(figsize=(11, 6 + 1.1 * depth))
75
+ ax.set_axis_off()
76
+ ax.set_title(f"{data['proof']} — final dependency graph\n"
77
+ f"{data.get('forge_version', '')} · "
78
+ f"{len(data['tasks'])} tasks · {len(data['dependencies'])} edges",
79
+ fontsize=13)
80
+ nx.draw_networkx_edges(G, pos, ax=ax, arrows=True, arrowstyle="-|>",
81
+ arrowsize=13, edge_color="#888888",
82
+ connectionstyle="arc3,rad=0.08")
83
+ colors = {"done": "#2e7d32", "in_progress": "#f9a825",
84
+ "needs_revision": "#c62828", "todo": "#9e9e9e",
85
+ "other": "#37474f"}
86
+ for n, d in G.nodes(data=True):
87
+ fill = colors.get(d["status"], "#9e9e9e")
88
+ x, y = pos[n]
89
+ w = max(len(n), len(d["title"][:26])) * 0.075 + 0.3
90
+ h = 0.78
91
+ ax.add_patch(FancyBboxPatch((x - w / 2, y - h / 2), w, h,
92
+ boxstyle="round,pad=0.02,rounding_size=0.08",
93
+ fc=fill, ec="#37474f", lw=2.2))
94
+ ax.annotate(n, (x, y + 0.14), ha="center", va="center", fontsize=9,
95
+ color="white", fontweight="bold")
96
+ ax.annotate(d["title"][:26], (x, y - 0.3), ha="center", va="center",
97
+ fontsize=6.4, color="#222222")
98
+ handles = [plt.Line2D([0], [0], marker="s", ls="", markersize=11,
99
+ markerfacecolor=c, markeredgecolor="#000",
100
+ label=str(s)) for s, c in colors.items()]
101
+ ax.legend(handles=handles, loc="upper left", bbox_to_anchor=(1.0, 1.0),
102
+ frameon=True, fontsize=8, title="status")
103
+ fig.tight_layout()
104
+ fig.savefig(root / "graph.png", dpi=150, bbox_inches="tight",
105
+ facecolor="white")
106
+ plt.close(fig)
107
+ return True
108
+ except Exception as e:
109
+ print(f" hint: graph.png render failed: {e}")
110
+ return False
111
+
112
+
113
+ # ------------------------------------------------------------------- README.md
114
+ _README_SECTIONS = [
115
+ ("What was built", "_(write one paragraph — what ran and what it produced)_"),
116
+ ("Why this proof exists", "_(the claim this proof answers; list Claim IDs — see proposal.json)_"),
117
+ ("Final architecture", "_(subsystem breakdown with dependency arrows — see graph.png)_"),
118
+ ("Commands", "- `forge proof bundle .` (this bundle was assembled by it)\n- build/test commands used during the run"),
119
+ ("Reproduce", "_(seed prompt, planner/executor/verifier used, Forge version)_"),
120
+ ("Artifact index", "- `events.log` — raw Forge history (single source of truth)\n- `graph.json` / `graph.png` — final dependency graph\n- `replay.md` — seq-cited timeline\n- `metrics.json` — comparable numbers\n- `screenshots/` — at least 2 captures\n- `demo.mp4` — <= 2 min of the run"),
121
+ ("Behavior notes", "_(anything non-obvious discovered during the run)_"),
122
+ ("Lessons learned", "_(insights about the project, Forge, or the domain)_"),
123
+ ]
124
+
125
+
126
+ def _scaffold_readme(root: Path, metrics: dict) -> None:
127
+ claims = metrics.get("claims") or []
128
+ why = ("Proof answers claim" + ("s" if len(claims) != 1 else "") +
129
+ f": {', '.join(claims)}."
130
+ if claims else "_(list Claim IDs — see proposal.json)_")
131
+ body = [f"# {root.name} — proof bundle",
132
+ "",
133
+ f"Automatically scaffolded by `forge proof bundle` "
134
+ f"(Forge {metrics.get('forge_version', 'unknown')}). "
135
+ "Fill the placeholders, keep the section names.",
136
+ ""]
137
+ for title, filler in _README_SECTIONS:
138
+ body += [f"## {title}", "", filler, ""]
139
+ body.append("---\n")
140
+ body.append("*Reported numbers are log-derived (see metrics.json).*")
141
+ (root / "README.md").write_text("\n".join(body) + "\n", encoding="utf-8")
142
+
143
+
144
+ # ------------------------------------------------------------------ demo.mp4
145
+ def _render_demo(root: Path) -> bool:
146
+ transcript = root / "demo" / "transcript.txt"
147
+ script = _TOOLS / "proof-render-demo.py"
148
+ if not transcript.exists() or not script.exists():
149
+ return False
150
+ try:
151
+ subprocess.run([sys.executable, str(script), str(root), str(transcript)],
152
+ check=True, capture_output=True, text=True, timeout=300)
153
+ return True
154
+ except Exception as e:
155
+ print(f" hint: demo.mp4 render failed: {e}")
156
+ return False
157
+
158
+
159
+ # ------------------------------------------------------------------ bundle
160
+ def _sha(path: Path) -> str:
161
+ return hashlib.sha256(path.read_bytes()).hexdigest()
162
+
163
+
164
+ def _verify_derived_byte_identical(root: Path, forge_version: str) -> bool:
165
+ """Re-derive in a temp dir NAMED after the proof (the 'proof' field of
166
+ graph.json is the directory name) and compare bytes — the §5
167
+ reproducibility rule, same inputs as check_invariants S7."""
168
+ # Pin the derive stamp to the version the shipped artifacts claim —
169
+ # otherwise a repo version bump (or a caller passing today's version)
170
+ # reports byte-drift against artifacts stamped at their own
171
+ # derivation version. Same rule as check_invariants S7.
172
+ claimed = (root / "graph.json").exists() and json.loads(
173
+ (root / "graph.json").read_text(encoding="utf-8")).get("forge_version")
174
+ if claimed:
175
+ forge_version = claimed
176
+ if not (root / "graph.json").exists() or not (root / "metrics.json").exists():
177
+ return True
178
+ with tempfile.TemporaryDirectory() as td:
179
+ tmp = Path(td) / root.name
180
+ tmp.mkdir()
181
+ for fname in ("events.log", "proposal.json"):
182
+ src = root / fname
183
+ if src.exists():
184
+ (tmp / fname).write_bytes(src.read_bytes())
185
+ proof_derive.derive_dir(str(tmp), forge_version=forge_version)
186
+ if (tmp / "graph.json").exists() and (root / "graph.json").exists():
187
+ same_g = _sha(tmp / "graph.json") == _sha(root / "graph.json")
188
+ same_m = _sha(tmp / "metrics.json") == _sha(root / "metrics.json")
189
+ return same_g and same_m
190
+ return True if not (root / "graph.json").exists() else True
191
+
192
+
193
+ def bundle_dir(proof_dir, forge_version="unknown"):
194
+ root = Path(proof_dir)
195
+ emitted: list[str] = []
196
+ verified: list[str] = []
197
+ skipped: list[str] = []
198
+
199
+ if not (root / "events.log").exists():
200
+ print(f"error: {root} has no events.log — bundle needs a Forge project "
201
+ "or proof dir with a raw event log", file=sys.stderr)
202
+ return 1
203
+
204
+ # 1. derived artifacts: write when missing, verify byte-identity when present
205
+ if not (root / "graph.json").exists() or not (root / "metrics.json").exists():
206
+ proof_derive.derive_dir(str(root), forge_version=forge_version)
207
+ emitted += ["graph.json", "metrics.json", "demo/_replay_facts.md"]
208
+ elif _verify_derived_byte_identical(root, forge_version):
209
+ verified += ["graph.json + metrics.json byte-identical after re-derive"]
210
+ else:
211
+ print(" problem: re-derived graph.json/metrics.json differ from the "
212
+ "shipped files — the log was edited or derive drifted",
213
+ file=sys.stderr)
214
+
215
+ # 2. replay.md (rendered, never clobbered)
216
+ if not (root / "replay.md").exists():
217
+ proof_replay.render_dir(str(root))
218
+ emitted.append("replay.md")
219
+ else:
220
+ skipped.append("replay.md (curated — left untouched)")
221
+
222
+ # 3. graph.png
223
+ if not (root / "graph.png").exists():
224
+ if _render_graph(root):
225
+ emitted.append("graph.png")
226
+ else:
227
+ skipped.append("graph.png (existing — left untouched)")
228
+
229
+ # 4. README.md scaffold
230
+ if not (root / "README.md").exists():
231
+ metrics = json.loads((root / "metrics.json").read_text(encoding="utf-8"))
232
+ _scaffold_readme(root, metrics)
233
+ emitted.append("README.md (scaffold — fill the placeholders)")
234
+ else:
235
+ skipped.append("README.md (curated — left untouched)")
236
+
237
+ # 5. demo.mp4 from a captured transcript (never clobbered)
238
+ if not (root / "demo.mp4").exists():
239
+ if _render_demo(root):
240
+ emitted.append("demo.mp4 (rendered from demo/transcript.txt)")
241
+ else:
242
+ skipped.append("demo.mp4 (existing — left untouched)")
243
+
244
+ for s in skipped:
245
+ print(f" kept: {s}")
246
+ if emitted:
247
+ print(f" emitted: {', '.join(emitted)}")
248
+ if verified:
249
+ print(f" verified: {', '.join(verified)}")
250
+ if not emitted and not verified:
251
+ print(f" no changes: all nine artifacts already present")
252
+
253
+ probs = proof_check.problems(root)
254
+ if probs:
255
+ print(" note: the gaps below are run-captured (record them, re-run "
256
+ "bundle):" if all("screenshots" in p or "demo.mp4" in p or "run.py" in p
257
+ for p in probs) else
258
+ " note: conformance gaps:")
259
+ return proof_check.verdict(root, probs)
@@ -0,0 +1,152 @@
1
+ """Vendored proof conformance check (port of tools/proof-check.py).
2
+
3
+ Implements the Proof Standard §6 conformance checklist exactly as the
4
+ canonical tools/ script does — same constants, same problems —
5
+ so `forge proof check` and `tools/proof-check.py` agree on any bundle.
6
+ Stdlib only; ffprobe is invoked only when demo.mp4 exists.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import re
12
+ import subprocess
13
+ from collections import Counter
14
+ from pathlib import Path
15
+
16
+ REQUIRED_FILES = ["README.md", "events.log", "graph.json", "graph.png",
17
+ "replay.md", "metrics.json", "demo.mp4"]
18
+ REQUIRED_METRICS = ["proof", "status", "language", "tasks", "events",
19
+ "verification_passes", "verification_failures", "retries",
20
+ "duration_minutes", "llm", "forge_version", "conforms_to",
21
+ "claims"]
22
+ README_SECTIONS = ["What was built", "Why this proof exists", "Final architecture",
23
+ "Commands", "Reproduce", "Artifact index", "Behavior notes",
24
+ "Lessons learned"]
25
+
26
+
27
+ def problems(root: Path):
28
+ """Full §6 checklist. Returns a list of problem strings ([] = conforming)."""
29
+ out = []
30
+
31
+ readme = root / "README.md"
32
+ if not readme.exists():
33
+ out.append("missing README.md")
34
+ else:
35
+ text = readme.read_text(encoding="utf-8")
36
+ for sec in README_SECTIONS:
37
+ if not re.search(r"^#{1,3}\s+.*" + re.escape(sec), text, re.M):
38
+ out.append(f"README missing section: {sec}")
39
+
40
+ ev_file = root / "events.log"
41
+ events = []
42
+ if not ev_file.exists():
43
+ out.append("missing events.log")
44
+ else:
45
+ for i, line in enumerate(ev_file.read_text(encoding="utf-8").splitlines(), 1):
46
+ if not line.strip():
47
+ continue
48
+ try:
49
+ ev = json.loads(line)
50
+ except json.JSONDecodeError:
51
+ out.append(f"events.log line {i} is not valid JSON")
52
+ continue
53
+ events.append(ev)
54
+ if events:
55
+ seqs = [e["seq"] for e in events if "seq" in e]
56
+ if seqs != list(range(1, len(seqs) + 1)):
57
+ out.append("events.log seq not contiguous from 1")
58
+ counts = Counter(e["op"] for e in events)
59
+ if not any(op.startswith("task_created") for op in counts):
60
+ out.append("events.log has no task_created events")
61
+
62
+ gf = root / "graph.json"
63
+ if not gf.exists():
64
+ out.append("missing graph.json")
65
+ else:
66
+ g = json.loads(gf.read_text(encoding="utf-8"))
67
+ for t in g.get("tasks", []):
68
+ for k in ("id", "title", "status", "priority"):
69
+ if k not in t:
70
+ out.append(f"graph.json task {t.get('id')} missing '{k}'")
71
+ if events:
72
+ created = {e["id"] for e in events if e["op"] == "task_created"}
73
+ gids = {t["id"] for t in g.get("tasks", [])}
74
+ if gids != created:
75
+ out.append("graph.json task ids differ from events.log")
76
+ deps = {(e["depends_on"], e["task"]) for e in events
77
+ if e["op"] == "dependency_added"}
78
+ gdeps = {(d["depends_on"], d["task"]) for d in g.get("dependencies", [])}
79
+ if gdeps != deps:
80
+ out.append("graph.json dependencies differ from events.log")
81
+
82
+ for f in ("graph.png", "replay.md"):
83
+ p = root / f
84
+ if not p.exists():
85
+ out.append(f"missing {f}")
86
+ if (root / "replay.md").exists():
87
+ rt = (root / "replay.md").read_text(encoding="utf-8")
88
+ for kw in ("Goal", "Outcome", "Timeline", "Turning points"):
89
+ if kw not in rt:
90
+ out.append(f"replay.md missing '{kw}'")
91
+
92
+ mf = root / "metrics.json"
93
+ if not mf.exists():
94
+ out.append("missing metrics.json")
95
+ else:
96
+ m = json.loads(mf.read_text(encoding="utf-8"))
97
+ for k in REQUIRED_METRICS:
98
+ if k not in m:
99
+ out.append(f"metrics.json missing field '{k}'")
100
+ if events and "tasks" in m and "events" in m:
101
+ if m["tasks"] != len({e["id"] for e in events if e["op"] == "task_created"}):
102
+ out.append("metrics.json 'tasks' != log task count")
103
+ if m["events"] != len(events):
104
+ out.append("metrics.json 'events' != log line count")
105
+ counts = Counter(e["op"] for e in events)
106
+ pairs = [("verification_passes", "verification_passed"),
107
+ ("verification_failures", "verification_failed"),
108
+ ("retries", "task_retried")]
109
+ for key, op in pairs:
110
+ if key in m and m[key] != counts.get(op, 0):
111
+ out.append(f"metrics.json '{key}' != log count ({m[key]} vs {counts.get(op, 0)})")
112
+
113
+ shots = sorted((root / "screenshots").glob("*.png")) if (root / "screenshots").exists() else []
114
+ if len(shots) < 2:
115
+ out.append(f"screenshots/ has {len(shots)} PNGs (need >= 2)")
116
+
117
+ demo = root / "demo.mp4"
118
+ if demo.exists():
119
+ try:
120
+ r = subprocess.run(["ffprobe", "-v", "error", "-select_streams", "v:0",
121
+ "-show_entries", "stream=width,height",
122
+ "-show_entries", "format=duration",
123
+ "-of", "json", str(demo)], capture_output=True, text=True)
124
+ info = json.loads(r.stdout)
125
+ dur = float(info["format"]["duration"])
126
+ w, h = info["streams"][0]["width"], info["streams"][0]["height"]
127
+ if dur > 120:
128
+ out.append(f"demo.mp4 too long: {dur:.0f}s (> 120s)")
129
+ if w > 1280 or h > 720:
130
+ out.append(f"demo.mp4 too large: {w}x{h} (> 720p)")
131
+ except Exception as e:
132
+ out.append(f"demo.mp4 unreadable: {e}")
133
+
134
+ if not (root / "run.py").exists():
135
+ out.append("no run.py entrypoint (README commands not checkable)")
136
+
137
+ idx = root.parent.parent / "proofs" / "INDEX.md"
138
+ if idx.exists() and root.name not in idx.read_text(encoding="utf-8"):
139
+ out.append(f"no INDEX.md entry for {root.name}")
140
+
141
+ return out
142
+
143
+
144
+ def verdict(proof_dir, problems_list):
145
+ root = Path(proof_dir)
146
+ if problems_list:
147
+ print(f"NON-CONFORMING: {root.name}")
148
+ for p in problems_list:
149
+ print(f" - {p}")
150
+ return 1
151
+ print(f"CONFORMING: {root.name} — all checks pass")
152
+ return 0
@@ -0,0 +1,83 @@
1
+ """CLI registration for forge-proof (the `proof` command).
2
+
3
+ Follows the forge.commands entry-point contract (see forge/plugins.py):
4
+ ``register(subparsers)`` adds the `proof` subparser with its own
5
+ argument shape and returns the handler map. The handler never receives
6
+ a Kernel (main() dispatches proof outside the project gate) — proof
7
+ tooling is stdlib-only and kernel-free by design.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ from . import check as proof_check
16
+ from . import derive as proof_derive
17
+ from . import replay as proof_replay
18
+ from .bundle import bundle_dir
19
+
20
+
21
+ def register(subparsers: argparse._SubParsersAction) -> dict:
22
+ """Entry point for ``forge.commands``: add `proof` and return its handler."""
23
+ c = subparsers.add_parser(
24
+ "proof", help="proof evidence pipeline: check / derive / replay / "
25
+ "bundle (provided by forge-proof)")
26
+ sub = c.add_subparsers(dest="proof_cmd", required=True, metavar="SUB",
27
+ title="proof subcommands")
28
+ pc = sub.add_parser("check", help="validate a proof bundle against the "
29
+ "Proof Standard §6 conformance checklist")
30
+ pc.add_argument("dir")
31
+ pd = sub.add_parser("derive", help="derive graph.json/metrics.json/replay "
32
+ "facts from events.log (§5 derivation rule)")
33
+ pd.add_argument("dir")
34
+ pd.add_argument("--forge-version", default="unknown")
35
+ pd.add_argument("--snapshot", default=None,
36
+ help="also copy graph.json to demo/snapshots/<name>.graph.json")
37
+ pr = sub.add_parser("replay", help="render replay.md from the derived facts "
38
+ "(Goal/Outcome/Timeline/Turning points, seq citations)")
39
+ pr.add_argument("dir")
40
+ pb = sub.add_parser("bundle", help="emit the full artifact bundle "
41
+ "(README/events.log/graph.json/graph.png/replay.md/"
42
+ "metrics.json/screenshots/demo.mp4) and validate it")
43
+ pb.add_argument("dir")
44
+ pb.add_argument("--forge-version", default="unknown")
45
+ return {"proof": cmd_proof}
46
+
47
+
48
+ def _req_dir(path: str) -> Path | None:
49
+ root = Path(path)
50
+ if not root.is_dir():
51
+ print(f"error: {path} is not a directory", file=sys.stderr)
52
+ return None
53
+ return root
54
+
55
+
56
+ def cmd_proof(args, k=None) -> int:
57
+ """Handler for all proof subcommands (k is always None — proof never
58
+ needs a project Kernel; main() bypasses the project gate for it)."""
59
+ root = _req_dir(args.dir)
60
+ if root is None:
61
+ return 1
62
+ if args.proof_cmd == "check":
63
+ return proof_check.verdict(root, proof_check.problems(root))
64
+ if args.proof_cmd == "derive":
65
+ try:
66
+ proof_derive.derive_dir(str(root), forge_version=args.forge_version,
67
+ snapshot=args.snapshot)
68
+ return 0
69
+ except FileNotFoundError:
70
+ print(f"error: {root / 'events.log'} not found — nothing to derive",
71
+ file=sys.stderr)
72
+ return 1
73
+ if args.proof_cmd == "replay":
74
+ try:
75
+ proof_replay.render_dir(str(root))
76
+ return 0
77
+ except FileNotFoundError as e:
78
+ print(f"error: {e}", file=sys.stderr)
79
+ return 1
80
+ if args.proof_cmd == "bundle":
81
+ return bundle_dir(str(root), forge_version=args.forge_version)
82
+ print(f"error: unknown proof subcommand {args.proof_cmd!r}", file=sys.stderr)
83
+ return 1
@@ -0,0 +1,227 @@
1
+ """Vendored proof derivation (byte-faithful port of tools/proof-derive.py).
2
+
3
+ Reproduces graph.json and metrics.json solely from events.log, per the
4
+ Forge Proof Standard derivation rule (proofs/PROOF_SPEC.md §5), plus
5
+ demo/_replay_facts.md. The port keeps the canonical tools/ script's exact
6
+ logic and JSON serialization so both produce byte-identical artifacts —
7
+ that parity is pinned by tests/test_proof_core.py. Stdlib only.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from collections import Counter
13
+ from datetime import datetime
14
+ from pathlib import Path
15
+
16
+ FIELD_DEFAULTS = {
17
+ "forge_version": "unknown",
18
+ "conforms_to": "proof-spec-0.1",
19
+ "language": "python",
20
+ }
21
+
22
+ _LANGUAGE_HINTS = [
23
+ ("rs", "Rust"),
24
+ ("cpp", "C++"), ("cc", "C++"), ("cxx", "C++"),
25
+ ("hpp", "C++"), ("hxx", "C++"),
26
+ ("py", "python"), ("pyw", "python"),
27
+ ]
28
+
29
+
30
+ def _subsystem_of(e):
31
+ for note in e.get("notes") or []:
32
+ if str(note).strip().startswith("subsystem:"):
33
+ return str(note).split(":", 1)[1].strip()
34
+ return None
35
+
36
+
37
+ def _infer_language(events):
38
+ """Primary implementation language, inferred from file references in the
39
+ log (task descriptions, evidence records). Pure function of events.log."""
40
+ import re as _re
41
+ hits = Counter()
42
+ stack = list(events)
43
+ while stack:
44
+ v = stack.pop()
45
+ if isinstance(v, dict):
46
+ stack.extend(v.values())
47
+ elif isinstance(v, list):
48
+ stack.extend(v)
49
+ elif isinstance(v, str):
50
+ for ext, lang in _LANGUAGE_HINTS:
51
+ if _re.search(r"\.%s\b" % ext, v):
52
+ hits[lang] += 1
53
+ if not hits:
54
+ return FIELD_DEFAULTS["language"]
55
+ return max(hits, key=lambda lang: (hits[lang], -("C++|Rust|python".split("|").index(lang))))
56
+
57
+
58
+ def _infer_claims(events, root):
59
+ claims = [c for e in events if e["op"] == "claims_claimed"
60
+ for c in e.get("claims", [])]
61
+ if not claims:
62
+ prop = root / "proposal.json"
63
+ if prop.exists():
64
+ claims = json.loads(prop.read_text(encoding="utf-8")).get("claims", [])
65
+ return claims
66
+
67
+
68
+ def replay(events):
69
+ """Reconstruct final task state and edge list from the event log."""
70
+ status = {}
71
+ priority = {}
72
+ title = {}
73
+ subsystem = {}
74
+ edges = []
75
+ milestones = []
76
+ meta = {
77
+ "op_counts": Counter(e["op"] for e in events),
78
+ "first_ts": min(e.get("ts") for e in events if e.get("ts")),
79
+ "last_ts": max(e.get("ts") for e in events if e.get("ts")),
80
+ "proposal_id": next((e["id"] for e in events if e["op"] == "proposal_committed"), None),
81
+ }
82
+ for e in events:
83
+ op, seq = e["op"], e["seq"]
84
+ if op == "task_created":
85
+ status[e["id"]] = "todo"
86
+ priority[e["id"]] = e.get("priority", "medium")
87
+ title[e["id"]] = e.get("title", e["id"])
88
+ subsystem[e["id"]] = _subsystem_of(e)
89
+ milestones.append((seq, "task_created", e["id"], "planned"))
90
+ elif op == "task_started":
91
+ status[e["id"]] = "in_progress"
92
+ elif op == "verification_failed":
93
+ status[e["id"]] = "needs_revision"
94
+ milestones.append((seq, "verification_failed", e["id"], e.get("reason", "")))
95
+ elif op == "verification_passed":
96
+ status[e["id"]] = "done"
97
+ milestones.append((seq, "verification_passed", e["id"], ""))
98
+ elif op == "task_reopened":
99
+ status[e["id"]] = "todo"
100
+ milestones.append((seq, "task_reopened", e["id"], ""))
101
+ elif op == "task_retried":
102
+ status[e["id"]] = "in_progress"
103
+ milestones.append((seq, "task_retried", e["id"], ""))
104
+ elif op == "dependency_added":
105
+ edges.append((e["depends_on"], e["task"]))
106
+ deps = {}
107
+ for e in events:
108
+ if e["op"] == "dependency_added":
109
+ deps.setdefault(e["task"], set()).add(e["depends_on"])
110
+ frontier = {}
111
+ peak, peak_seq, peak_ev = 0, None, ("", "")
112
+
113
+ def ready_count():
114
+ return sum(1 for t, s in frontier.items()
115
+ if s == "pending" and all(frontier.get(d) == "done"
116
+ for d in deps.get(t, ())))
117
+
118
+ for e in events:
119
+ op, seq = e["op"], e["seq"]
120
+ if op == "task_created":
121
+ frontier[e["id"]] = "pending"
122
+ elif op == "task_started":
123
+ frontier[e["id"]] = "running"
124
+ elif op == "verification_passed":
125
+ frontier[e["id"]] = "done"
126
+ elif op == "verification_failed":
127
+ frontier[e["id"]] = "pending"
128
+ elif op == "task_reopened":
129
+ frontier[e["id"]] = "pending"
130
+ elif op == "task_retried":
131
+ frontier[e["id"]] = "running"
132
+ elif op == "task_cancelled":
133
+ frontier[e["id"]] = "cancelled"
134
+ rc = ready_count()
135
+ if rc > peak:
136
+ peak, peak_seq, peak_ev = rc, seq, (op, e.get("id", ""))
137
+ meta["max_ready_queue"] = peak
138
+ meta["max_ready_queue_at"] = {"seq": peak_seq, "event": peak_ev[0], "id": peak_ev[1]}
139
+ return {
140
+ "tasks": {tid: {"id": tid, "title": title[tid], "status": status[tid],
141
+ "priority": priority[tid], "subsystem": subsystem.get(tid)}
142
+ for tid in status},
143
+ "edges": edges, "status": status, "meta": meta,
144
+ "milestones": milestones,
145
+ }
146
+
147
+
148
+ def derive_dir(example_dir, forge_version="unknown", snapshot=None):
149
+ """Derive graph.json / metrics.json / demo/_replay_facts.md into
150
+ example_dir from its events.log. Byte-parity with
151
+ tools/proof-derive.py main(). Returns a summary dict."""
152
+ root = Path(example_dir)
153
+ events = [json.loads(l) for l in
154
+ (root / "events.log").read_text(encoding="utf-8").splitlines() if l.strip()]
155
+ r = replay(events)
156
+ counts = r["meta"]["op_counts"]
157
+
158
+ tasks = []
159
+ for tid in sorted(r["tasks"], key=lambda t: min(e["seq"] for e in events
160
+ if e["op"] == "task_created" and e["id"] == t)):
161
+ t = r["tasks"][tid]
162
+ tasks.append({"id": t["id"], "title": t["title"], "status": t["status"],
163
+ "priority": t["priority"], "subsystem": t.get("subsystem")})
164
+
165
+ graph = {
166
+ "proof": root.name,
167
+ "forge_version": forge_version,
168
+ "derived_from": "events.log",
169
+ "log_tail_ts": r["meta"]["last_ts"],
170
+ "tasks": tasks,
171
+ "dependencies": [{"task": t, "depends_on": d} for d, t in r["edges"]],
172
+ }
173
+
174
+ t0 = datetime.fromisoformat(r["meta"]["first_ts"].replace("Z", "+00:00"))
175
+ t1 = datetime.fromisoformat(r["meta"]["last_ts"].replace("Z", "+00:00"))
176
+ minutes = round((t1 - t0).total_seconds() / 60)
177
+
178
+ passes = counts.get("verification_passed", 0)
179
+ failures = counts.get("verification_failed", 0)
180
+ metrics = {
181
+ "proof": root.name,
182
+ "status": "completed" if all(t["status"] == "done" for t in tasks) else "partial",
183
+ "language": _infer_language(events),
184
+ "tasks": len(tasks),
185
+ "events": len(events),
186
+ "verification_passes": passes,
187
+ "verification_failures": failures,
188
+ "retries": counts.get("task_retried", 0),
189
+ "max_ready_queue": r["meta"]["max_ready_queue"],
190
+ "max_ready_queue_at": r["meta"]["max_ready_queue_at"],
191
+ "duration_minutes": minutes,
192
+ "llm": "not recorded",
193
+ "forge_version": forge_version,
194
+ "conforms_to": FIELD_DEFAULTS["conforms_to"],
195
+ "claims": _infer_claims(events, root),
196
+ }
197
+
198
+ (root / "graph.json").write_text(json.dumps(graph, indent=2) + "\n", encoding="utf-8")
199
+ (root / "metrics.json").write_text(json.dumps(metrics, indent=2) + "\n", encoding="utf-8")
200
+
201
+ if snapshot:
202
+ snap_dir = root / "demo" / "snapshots"
203
+ snap_dir.mkdir(parents=True, exist_ok=True)
204
+ (snap_dir / f"{snapshot}.graph.json").write_text(
205
+ json.dumps(graph, indent=2) + "\n", encoding="utf-8")
206
+
207
+ by_task = {}
208
+ for m in r["milestones"]:
209
+ by_task.setdefault(m[2], []).append(m)
210
+ lines = ["# Replay facts (derived from events.log)\n",
211
+ f"- tasks: {len(tasks)} · events: {len(events)} · "
212
+ f"passes: {passes} · failures: {failures} · retries: {metrics['retries']} · "
213
+ f"duration: {minutes} min\n"]
214
+ for seq, op, tid, note in sorted(r["milestones"]):
215
+ note = f" — {note}" if note else ""
216
+ lines.append(f"- seq {seq} {op:<22} {tid}{note}")
217
+ (root / "demo").mkdir(exist_ok=True)
218
+ (root / "demo" / "_replay_facts.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
219
+
220
+ print(f"derived {root.name}: {len(tasks)} tasks, {len(events)} events, "
221
+ f"{passes} passes, {failures} failures, {minutes} min, "
222
+ f"max_ready_queue={r['meta']['max_ready_queue']}")
223
+ print(f" -> {root/'graph.json'}, {root/'metrics.json'}, "
224
+ f"{root/'demo'/'_replay_facts.md'}")
225
+ return {"tasks": len(tasks), "events": len(events), "passes": passes,
226
+ "failures": failures, "minutes": minutes, "status": metrics["status"],
227
+ "max_ready_queue": r["meta"]["max_ready_queue"]}
@@ -0,0 +1,87 @@
1
+ """replay.md renderer for `forge proof replay`.
2
+
3
+ Builds a human-readable timeline with the Proof Standard's required
4
+ structure (Goal / Outcome / Timeline / Turning points, with `seq`
5
+ citations) purely from machine-derived inputs: demo/_replay_facts.md and
6
+ metrics.json. When proposal.json carries a `reason`, it becomes the Goal;
7
+ otherwise the Goal points at the README.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from pathlib import Path
13
+
14
+ TURNING_OPS = ("verification_failed", "task_retried", "task_reopened")
15
+
16
+
17
+ def render_replay(name: str, facts: str, metrics: dict,
18
+ goal: str | None = None) -> str:
19
+ lines = facts.splitlines()
20
+ timeline = [ln for ln in lines if ln.strip().startswith("- seq ")]
21
+ turning = [ln for ln in timeline
22
+ if any(op in ln for op in TURNING_OPS)]
23
+ m = metrics
24
+ goal_txt = goal or ("(see README.md — 'What was built' and 'Why this proof exists')")
25
+
26
+ head = [
27
+ f"# {name} — replay",
28
+ "",
29
+ "Automatically derived from `events.log` (via `demo/_replay_facts.md` + "
30
+ "`metrics.json`); every milestone cites `seq` numbers for "
31
+ "cross-checking.",
32
+ "",
33
+ "## Goal",
34
+ "",
35
+ goal_txt,
36
+ "",
37
+ "## Outcome",
38
+ "",
39
+ f"- tasks: **{m.get('tasks', '?')}** · events: **{m.get('events', '?')}** · "
40
+ f"passes: **{m.get('verification_passes', '?')}** · "
41
+ f"failures: **{m.get('verification_failures', '?')}** · "
42
+ f"retries: **{m.get('retries', '?')}**",
43
+ f"- duration: **{m.get('duration_minutes', '?')} min** · "
44
+ f"status: **{m.get('status', '?')}** · "
45
+ f"max_ready_queue: **{m.get('max_ready_queue', '?')}** "
46
+ f"(peaked at seq {m.get('max_ready_queue_at', {}).get('seq', '?')})",
47
+ "- numbers match `metrics.json`, which is derived from `events.log` alone.",
48
+ "",
49
+ "## Timeline",
50
+ "",
51
+ *[f"{ln}" for ln in timeline],
52
+ ]
53
+ if turning:
54
+ head += [
55
+ "",
56
+ "## Turning points",
57
+ "",
58
+ *[f"{ln}" for ln in turning],
59
+ ]
60
+ else:
61
+ head += ["", "## Turning points", "", "None — no failures, retries, or reopens."]
62
+ head += ["", f"*rendered by forge proof replay from {len(timeline)} seq-cited milestones*"]
63
+ return "\n".join(head) + "\n"
64
+
65
+
66
+ def render_dir(proof_dir) -> None:
67
+ """Render replay.md into proof_dir (facts + metrics must exist)."""
68
+ root = Path(proof_dir)
69
+ facts_path = root / "demo" / "_replay_facts.md"
70
+ if not facts_path.exists():
71
+ raise FileNotFoundError(
72
+ f"no derived facts at {facts_path} — run 'forge proof derive {root.name}' first")
73
+ metrics = json.loads((root / "metrics.json").read_text(encoding="utf-8"))
74
+ goal = None
75
+ prop = root / "proposal.json"
76
+ if prop.exists():
77
+ try:
78
+ goal = json.loads(prop.read_text(encoding="utf-8")).get("reason")
79
+ except json.JSONDecodeError:
80
+ goal = None
81
+ md = render_replay(root.name, facts_path.read_text(encoding="utf-8"),
82
+ metrics, goal)
83
+ (root / "replay.md").write_text(md, encoding="utf-8")
84
+ print(f"wrote {root / 'replay.md'} "
85
+ f"({metrics.get('tasks', '?')} tasks, "
86
+ f"{metrics.get('events', '?')} events)")
87
+ return md
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: forge-proof
3
+ Version: 0.1.0a5
4
+ Summary: Forge reference proof tooling: forge proof check/derive/replay/bundle — a Proof-Standard evidence bundle from events.log alone. Stdlib-only core; optional matplotlib+networkx for graph.png.
5
+ Author-email: Sanjay Davis <psanjuknl@gmail.com>
6
+ License-Expression: MIT
7
+ Keywords: forge,proof,evidence,sdk,autonomous-software
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+
15
+ # forge-proof
16
+
17
+ Reference proof evidence tooling for [Forge](https://github.com/SanjayDavis/Forge):
18
+ a stdlib-only, kernel-free CLI that turns a raw `events.log` into a complete
19
+ [Proof Standard](https://github.com/SanjayDavis/Forge/blob/master/proofs/PROOF_SPEC.md)
20
+ artifact bundle — no manual steps, no LLM in the loop.
21
+
22
+ ```
23
+ forge proof check <dir> validate a bundle against the §6 checklist
24
+ forge proof derive <dir> derive graph.json/metrics.json/replay facts from events.log (§5)
25
+ forge proof replay <dir> render replay.md (Goal/Outcome/Timeline/Turning points)
26
+ forge proof bundle <dir> emit the full bundle + validate (net-new artifacts only;
27
+ curated README/replay.md/media are never clobbered,
28
+ derived artifacts are verified byte-identical)
29
+ ```
30
+
31
+ Installing this package registers the `forge proof` command through the
32
+ `forge.commands` entry-point group — same mechanism as `forge plan` from
33
+ forge-planner.
34
+
35
+ Design constraints (Proof Standard §7 / repo plan):
36
+ - **stdlib-only**: no runtime dependencies; `graph.png` rendering uses
37
+ matplotlib+networkx when present in the invoking python, otherwise a
38
+ clear hint instead of a crash.
39
+ - **kernel-free**: never imports `forge`; the event log is read as raw
40
+ JSON lines, so the proof pipeline cannot disturb project state.
41
+ - **reproducible**: derived artifacts are a pure function of `events.log`
42
+ — byte-identical across runs and byte-identical to the canonical
43
+ `tools/proof-derive.py` (pinned by tests).
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ forge_proof/__init__.py
4
+ forge_proof/bundle.py
5
+ forge_proof/check.py
6
+ forge_proof/cli.py
7
+ forge_proof/derive.py
8
+ forge_proof/replay.py
9
+ forge_proof.egg-info/PKG-INFO
10
+ forge_proof.egg-info/SOURCES.txt
11
+ forge_proof.egg-info/dependency_links.txt
12
+ forge_proof.egg-info/entry_points.txt
13
+ forge_proof.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [forge.commands]
2
+ proof = forge_proof.cli:register
@@ -0,0 +1 @@
1
+ forge_proof
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "forge-proof"
7
+ version = "0.1.0a5"
8
+ description = "Forge reference proof tooling: forge proof check/derive/replay/bundle — a Proof-Standard evidence bundle from events.log alone. Stdlib-only core; optional matplotlib+networkx for graph.png."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "Sanjay Davis", email = "psanjuknl@gmail.com" }]
13
+ keywords = ["forge", "proof", "evidence", "sdk", "autonomous-software"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Operating System :: OS Independent",
19
+ ]
20
+ # Stdlib-only by design (Proof Standard §7: reference tooling stays
21
+ # dependency-free). graph.png rendering needs matplotlib+networkx present
22
+ # in the invoking python; demo.mp4 rendering shells out to the repo's
23
+ # tools/proof-render-demo.py when a transcript exists.
24
+ dependencies = []
25
+
26
+ [project.entry-points."forge.commands"]
27
+ proof = "forge_proof.cli:register"
28
+
29
+ [tool.setuptools]
30
+ packages = ["forge_proof"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+