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.
- forge_proof-0.1.0a5/PKG-INFO +43 -0
- forge_proof-0.1.0a5/README.md +29 -0
- forge_proof-0.1.0a5/forge_proof/__init__.py +12 -0
- forge_proof-0.1.0a5/forge_proof/bundle.py +259 -0
- forge_proof-0.1.0a5/forge_proof/check.py +152 -0
- forge_proof-0.1.0a5/forge_proof/cli.py +83 -0
- forge_proof-0.1.0a5/forge_proof/derive.py +227 -0
- forge_proof-0.1.0a5/forge_proof/replay.py +87 -0
- forge_proof-0.1.0a5/forge_proof.egg-info/PKG-INFO +43 -0
- forge_proof-0.1.0a5/forge_proof.egg-info/SOURCES.txt +13 -0
- forge_proof-0.1.0a5/forge_proof.egg-info/dependency_links.txt +1 -0
- forge_proof-0.1.0a5/forge_proof.egg-info/entry_points.txt +2 -0
- forge_proof-0.1.0a5/forge_proof.egg-info/top_level.txt +1 -0
- forge_proof-0.1.0a5/pyproject.toml +30 -0
- forge_proof-0.1.0a5/setup.cfg +4 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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"]
|