Skip to content

Commit 17ffb61

Browse files
authored
feat: clarify PatchProof value and quick start
1 parent cc106ac commit 17ffb61

10 files changed

Lines changed: 528 additions & 82 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,24 @@
44

55
All notable changes are recorded here. The project remains alpha before `1.0.0`, and minor releases may change experimental interfaces.
66

7+
## 0.1.1 — 2026-08-03
8+
9+
### 新增 / Added
10+
11+
- 新增 `verify-receipt` CLI,用于重新验证回执结构、阶段语义和内容哈希。
12+
Added the `verify-receipt` CLI for revalidating receipt structure, phase semantics, and content hashes.
13+
- 新增无需 Docker 和第三方依赖的一键可信本地 Demo,并以回归测试保证可重复运行。
14+
Added a one-command trusted-local demo with no Docker or third-party dependency, covered by a repeatability regression test.
15+
- 新增基于真实夹具输出的 README 终端演示图。
16+
Added a README terminal visual derived from real fixture output.
17+
18+
### 变更 / Changed
19+
20+
- 重构项目首页,明确目标用户、相对普通 CI 的差异、三分钟快速开始、适用范围、成熟度和常见问题。
21+
Reworked the project front page around target users, differentiation from typical CI, a three-minute quick start, fit, maturity, and FAQ.
22+
- 收窄产品声明:当前是面向 Coding Agent 基础设施与评测工程师的 Alpha 协议实现,而不是通用测试平台或生产多租户沙箱。
23+
Narrowed the product claim: this is an alpha protocol implementation for coding-agent infrastructure and evaluation engineers, not a general test platform or production multi-tenant sandbox.
24+
725
## 0.1.0 — 2026-08-03
826

927
### 新增 / Added

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ PYTHON ?= python3
22
RUN_DIR ?= /tmp/patchproof-demo
33
IMAGE ?= python:3.12.10-slim@sha256:fd95fa221297a88e1cf49c55ec1828edd7c5a428187e67b5d1805692d11588db
44

5-
.PHONY: test demo clean
5+
.PHONY: test demo-local demo clean
66

77
test:
88
PYTHONPATH=src $(PYTHON) -m unittest discover -s tests -v
99

10+
demo-local:
11+
PYTHONPATH=src $(PYTHON) scripts/run_demo.py
12+
1013
demo:
1114
PYTHONPATH=src $(PYTHON) -m patchproof propose \
1215
--repo fixtures/calculator \

README.md

Lines changed: 233 additions & 75 deletions
Large diffs are not rendered by default.

docs/assets/patchproof-demo.svg

Lines changed: 82 additions & 0 deletions
Loading

pyproject.toml

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,37 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "patchproof"
7-
version = "0.1.0"
8-
description = "Evidence-grade validation for AI-generated patches."
7+
version = "0.1.1"
8+
description = "Verify AI coding-agent patches with fail-before/pass-after tests, isolated execution, and auditable receipts."
99
readme = "README.md"
1010
requires-python = ">=3.11"
1111
license = { text = "MIT" }
1212
authors = [{ name = "eatdrop" }]
13-
keywords = ["coding-agent", "patch-validation", "agent-safety", "docker"]
13+
keywords = [
14+
"ai-coding-agent",
15+
"agent-evaluation",
16+
"agent-safety",
17+
"docker",
18+
"patch-validation",
19+
"software-supply-chain",
20+
]
1421
classifiers = [
1522
"Development Status :: 3 - Alpha",
1623
"License :: OSI Approved :: MIT License",
1724
"Programming Language :: Python :: 3",
1825
"Programming Language :: Python :: 3.11",
1926
"Programming Language :: Python :: 3.12",
27+
"Topic :: Software Development :: Testing",
28+
"Topic :: Software Development :: Quality Assurance",
2029
]
2130
dependencies = []
2231

32+
[project.urls]
33+
Homepage = "https://github.com/eatdrop/patchproof"
34+
Repository = "https://github.com/eatdrop/patchproof"
35+
Issues = "https://github.com/eatdrop/patchproof/issues"
36+
Changelog = "https://github.com/eatdrop/patchproof/blob/main/CHANGELOG.md"
37+
2338
[project.scripts]
2439
patchproof = "patchproof.cli:main"
2540

scripts/run_demo.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#!/usr/bin/env python3
2+
"""Run PatchProof's trusted local fixture without external dependencies."""
3+
4+
from __future__ import annotations
5+
6+
import tempfile
7+
from pathlib import Path
8+
9+
from patchproof.approval import PatchApproval
10+
from patchproof.proposal import PatchProposal
11+
from patchproof.repository import ReadOnlyRepository
12+
from patchproof.runner import UnsafeLocalRunner
13+
from patchproof.validator import ValidationReceipt, store_validation, validate_patch
14+
15+
16+
ROOT = Path(__file__).resolve().parents[1]
17+
PHASE_LABELS = {
18+
"baseline_reproduction": "补丁前复现 / fail before patch",
19+
"patched_reproduction": "补丁后复现 / pass after patch",
20+
"full_regression": "完整回归 / full regression",
21+
"hidden_tests": "隐藏测试 / external hidden tests",
22+
}
23+
24+
25+
def main() -> int:
26+
repository = ReadOnlyRepository(ROOT / "fixtures" / "calculator")
27+
before = repository.snapshot()
28+
proposal = PatchProposal.create(
29+
unified_diff=(ROOT / "fixtures" / "division-by-zero.diff").read_text(
30+
encoding="utf-8"
31+
),
32+
base_snapshot=before.digest,
33+
)
34+
approval = PatchApproval.create(
35+
run_id="quickstart-demo",
36+
proposal=proposal,
37+
approved_by="local-demo",
38+
supplied_proposal_hash=proposal.proposal_hash,
39+
)
40+
receipt = validate_patch(
41+
repository=repository,
42+
proposal=proposal,
43+
approval=approval,
44+
reproduction_tests=ROOT / "fixtures" / "reproduction",
45+
hidden_tests=ROOT / "fixtures" / "hidden",
46+
runner=UnsafeLocalRunner(timeout_seconds=10),
47+
)
48+
unchanged = repository.snapshot() == before
49+
with tempfile.TemporaryDirectory(prefix="patchproof-demo-") as directory:
50+
stored = store_validation(receipt, Path(directory))
51+
loaded = ValidationReceipt.from_json(
52+
stored.receipt_path.read_text(encoding="utf-8")
53+
)
54+
integrity_verified = loaded == receipt
55+
56+
print("PatchProof 可信本地演示 / trusted-local demo")
57+
print("------------------------------------------------")
58+
for phase in receipt.phases:
59+
status = "PASS" if phase.passed else "FAIL"
60+
print(
61+
f"[{status}] {PHASE_LABELS[phase.name]} "
62+
f"({phase.tests_run} test{'s' if phase.tests_run != 1 else ''})"
63+
)
64+
print("------------------------------------------------")
65+
print(f"验证结果 / validation: {'PASSED' if receipt.success else 'FAILED'}")
66+
print(f"真实仓库未变 / repository unchanged: {'YES' if unchanged else 'NO'}")
67+
print(
68+
"回执完整性 / receipt integrity: "
69+
f"{'VERIFIED' if integrity_verified else 'INVALID'}"
70+
)
71+
print("证据等级 / proof grade: NO (trusted-local; Docker required)")
72+
return 0 if receipt.success and unchanged and integrity_verified else 1
73+
74+
75+
if __name__ == "__main__":
76+
raise SystemExit(main())

src/patchproof/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@
1616
"validate_patch",
1717
]
1818

19-
__version__ = "0.1.0"
19+
__version__ = "0.1.1"

src/patchproof/cli.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
from .proposal import PatchProposal
1212
from .repository import ReadOnlyRepository
1313
from .runner import DockerRunner, UnsafeLocalRunner
14-
from .validator import store_validation, validate_patch
14+
from .validator import ValidationReceipt, store_validation, validate_patch
1515

1616

1717
def build_parser() -> argparse.ArgumentParser:
1818
parser = argparse.ArgumentParser(
1919
prog="patchproof",
20-
description="Evidence-grade validation for AI-generated patches.",
20+
description="Evidence-grade patch validation for AI coding agents.",
2121
)
2222
subparsers = parser.add_subparsers(dest="command", required=True)
2323

@@ -49,6 +49,12 @@ def build_parser() -> argparse.ArgumentParser:
4949
action="store_true",
5050
help="Run trusted fixtures without isolation; never use for untrusted code.",
5151
)
52+
53+
verify_receipt = subparsers.add_parser(
54+
"verify-receipt",
55+
help="Recompute and verify a receipt's internal integrity.",
56+
)
57+
verify_receipt.add_argument("--receipt", required=True)
5258
return parser
5359

5460

@@ -61,6 +67,8 @@ def main(argv: Sequence[str] | None = None) -> int:
6167
return _approve(args)
6268
if args.command == "validate":
6369
return _validate(args)
70+
if args.command == "verify-receipt":
71+
return _verify_receipt(args)
6472
raise ValueError("unsupported command")
6573
except (OSError, RuntimeError, ValueError) as exc:
6674
print(
@@ -174,6 +182,36 @@ def _validate(args: argparse.Namespace) -> int:
174182
return 0 if receipt.success else 2
175183

176184

185+
def _verify_receipt(args: argparse.Namespace) -> int:
186+
receipt_path = Path(args.receipt).expanduser().resolve(strict=True)
187+
receipt = ValidationReceipt.from_json(
188+
read_bounded_regular(receipt_path).decode("utf-8")
189+
)
190+
print(
191+
json.dumps(
192+
{
193+
"status": "receipt_integrity_verified",
194+
"integrity_valid": True,
195+
"validation_success": receipt.success,
196+
"proof_grade": receipt.proof_grade,
197+
"isolated": receipt.isolated,
198+
"receipt_hash": receipt.receipt_hash,
199+
"phases": [
200+
{
201+
"name": phase.name,
202+
"passed": phase.passed,
203+
"tests_run": phase.tests_run,
204+
}
205+
for phase in receipt.phases
206+
],
207+
},
208+
ensure_ascii=False,
209+
sort_keys=True,
210+
)
211+
)
212+
return 0
213+
214+
177215
def _outside_repository(repository_root: Path, value: Path) -> Path:
178216
target = absolute_no_resolve(value)
179217
comparison = target.resolve(strict=False)

tests/test_cli.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,26 @@ def test_end_to_end_unsafe_local_is_explicitly_not_proof_grade(self) -> None:
154154
self.assertFalse(result["proof_grade"])
155155
self.assertEqual(len(list(audit.glob("*.json"))), 1)
156156
self.assertEqual(len(list(audit.glob("*.md"))), 1)
157+
receipt = next(audit.glob("*.json"))
158+
verify_output = io.StringIO()
159+
with redirect_stdout(verify_output), redirect_stderr(io.StringIO()):
160+
verify_status = main(
161+
["verify-receipt", "--receipt", str(receipt)]
162+
)
163+
verified = json.loads(verify_output.getvalue())
164+
self.assertEqual(verify_status, 0)
165+
self.assertTrue(verified["integrity_valid"])
166+
self.assertTrue(verified["validation_success"])
167+
self.assertFalse(verified["proof_grade"])
168+
169+
tampered = parent / "tampered-receipt.json"
170+
tampered_payload = json.loads(receipt.read_text())
171+
tampered_payload["success"] = False
172+
tampered.write_text(json.dumps(tampered_payload), encoding="utf-8")
173+
self.assertEqual(
174+
self._main(["verify-receipt", "--receipt", str(tampered)]),
175+
1,
176+
)
157177

158178
@staticmethod
159179
def _main(arguments: list[str]) -> int:

tests/test_demo.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import subprocess
5+
import sys
6+
import unittest
7+
from pathlib import Path
8+
9+
10+
ROOT = Path(__file__).resolve().parents[1]
11+
12+
13+
class DemoTests(unittest.TestCase):
14+
def test_trusted_local_demo_is_truthful_and_repeatable(self) -> None:
15+
environment = os.environ.copy()
16+
environment["PYTHONPATH"] = str(ROOT / "src")
17+
for _ in range(2):
18+
completed = subprocess.run(
19+
[sys.executable, str(ROOT / "scripts" / "run_demo.py")],
20+
cwd=ROOT,
21+
env=environment,
22+
text=True,
23+
stdout=subprocess.PIPE,
24+
stderr=subprocess.PIPE,
25+
timeout=30,
26+
check=False,
27+
)
28+
self.assertEqual(completed.returncode, 0, completed.stderr)
29+
self.assertIn("validation: PASSED", completed.stdout)
30+
self.assertIn("repository unchanged: YES", completed.stdout)
31+
self.assertIn("receipt integrity: VERIFIED", completed.stdout)
32+
self.assertIn("proof grade: NO", completed.stdout)
33+
34+
35+
if __name__ == "__main__":
36+
unittest.main()

0 commit comments

Comments
 (0)