Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@

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

## 0.1.1 — 2026-08-03

### 新增 / Added

- 新增 `verify-receipt` CLI,用于重新验证回执结构、阶段语义和内容哈希。
Added the `verify-receipt` CLI for revalidating receipt structure, phase semantics, and content hashes.
- 新增无需 Docker 和第三方依赖的一键可信本地 Demo,并以回归测试保证可重复运行。
Added a one-command trusted-local demo with no Docker or third-party dependency, covered by a repeatability regression test.
- 新增基于真实夹具输出的 README 终端演示图。
Added a README terminal visual derived from real fixture output.

### 变更 / Changed

- 重构项目首页,明确目标用户、相对普通 CI 的差异、三分钟快速开始、适用范围、成熟度和常见问题。
Reworked the project front page around target users, differentiation from typical CI, a three-minute quick start, fit, maturity, and FAQ.
- 收窄产品声明:当前是面向 Coding Agent 基础设施与评测工程师的 Alpha 协议实现,而不是通用测试平台或生产多租户沙箱。
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.

## 0.1.0 — 2026-08-03

### 新增 / Added
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ PYTHON ?= python3
RUN_DIR ?= /tmp/patchproof-demo
IMAGE ?= python:3.12.10-slim@sha256:fd95fa221297a88e1cf49c55ec1828edd7c5a428187e67b5d1805692d11588db

.PHONY: test demo clean
.PHONY: test demo-local demo clean

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

demo-local:
PYTHONPATH=src $(PYTHON) scripts/run_demo.py

demo:
PYTHONPATH=src $(PYTHON) -m patchproof propose \
--repo fixtures/calculator \
Expand Down
308 changes: 233 additions & 75 deletions README.md

Large diffs are not rendered by default.

82 changes: 82 additions & 0 deletions docs/assets/patchproof-demo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 18 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,37 @@ build-backend = "setuptools.build_meta"

[project]
name = "patchproof"
version = "0.1.0"
description = "Evidence-grade validation for AI-generated patches."
version = "0.1.1"
description = "Verify AI coding-agent patches with fail-before/pass-after tests, isolated execution, and auditable receipts."
readme = "README.md"
requires-python = ">=3.11"
license = { text = "MIT" }
authors = [{ name = "eatdrop" }]
keywords = ["coding-agent", "patch-validation", "agent-safety", "docker"]
keywords = [
"ai-coding-agent",
"agent-evaluation",
"agent-safety",
"docker",
"patch-validation",
"software-supply-chain",
]
classifiers = [
"Development Status :: 3 - Alpha",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Testing",
"Topic :: Software Development :: Quality Assurance",
]
dependencies = []

[project.urls]
Homepage = "https://github.com/eatdrop/patchproof"
Repository = "https://github.com/eatdrop/patchproof"
Issues = "https://github.com/eatdrop/patchproof/issues"
Changelog = "https://github.com/eatdrop/patchproof/blob/main/CHANGELOG.md"

[project.scripts]
patchproof = "patchproof.cli:main"

Expand Down
76 changes: 76 additions & 0 deletions scripts/run_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Run PatchProof's trusted local fixture without external dependencies."""

from __future__ import annotations

import tempfile
from pathlib import Path

from patchproof.approval import PatchApproval
from patchproof.proposal import PatchProposal
from patchproof.repository import ReadOnlyRepository
from patchproof.runner import UnsafeLocalRunner
from patchproof.validator import ValidationReceipt, store_validation, validate_patch


ROOT = Path(__file__).resolve().parents[1]
PHASE_LABELS = {
"baseline_reproduction": "补丁前复现 / fail before patch",
"patched_reproduction": "补丁后复现 / pass after patch",
"full_regression": "完整回归 / full regression",
"hidden_tests": "隐藏测试 / external hidden tests",
}


def main() -> int:
repository = ReadOnlyRepository(ROOT / "fixtures" / "calculator")
before = repository.snapshot()
proposal = PatchProposal.create(
unified_diff=(ROOT / "fixtures" / "division-by-zero.diff").read_text(
encoding="utf-8"
),
base_snapshot=before.digest,
)
approval = PatchApproval.create(
run_id="quickstart-demo",
proposal=proposal,
approved_by="local-demo",
supplied_proposal_hash=proposal.proposal_hash,
)
receipt = validate_patch(
repository=repository,
proposal=proposal,
approval=approval,
reproduction_tests=ROOT / "fixtures" / "reproduction",
hidden_tests=ROOT / "fixtures" / "hidden",
runner=UnsafeLocalRunner(timeout_seconds=10),
)
unchanged = repository.snapshot() == before
with tempfile.TemporaryDirectory(prefix="patchproof-demo-") as directory:
stored = store_validation(receipt, Path(directory))
loaded = ValidationReceipt.from_json(
stored.receipt_path.read_text(encoding="utf-8")
)
integrity_verified = loaded == receipt

print("PatchProof 可信本地演示 / trusted-local demo")
print("------------------------------------------------")
for phase in receipt.phases:
status = "PASS" if phase.passed else "FAIL"
print(
f"[{status}] {PHASE_LABELS[phase.name]} "
f"({phase.tests_run} test{'s' if phase.tests_run != 1 else ''})"
)
print("------------------------------------------------")
print(f"验证结果 / validation: {'PASSED' if receipt.success else 'FAILED'}")
print(f"真实仓库未变 / repository unchanged: {'YES' if unchanged else 'NO'}")
print(
"回执完整性 / receipt integrity: "
f"{'VERIFIED' if integrity_verified else 'INVALID'}"
)
print("证据等级 / proof grade: NO (trusted-local; Docker required)")
return 0 if receipt.success and unchanged and integrity_verified else 1


if __name__ == "__main__":
raise SystemExit(main())
2 changes: 1 addition & 1 deletion src/patchproof/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@
"validate_patch",
]

__version__ = "0.1.0"
__version__ = "0.1.1"
42 changes: 40 additions & 2 deletions src/patchproof/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
from .proposal import PatchProposal
from .repository import ReadOnlyRepository
from .runner import DockerRunner, UnsafeLocalRunner
from .validator import store_validation, validate_patch
from .validator import ValidationReceipt, store_validation, validate_patch


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

Expand Down Expand Up @@ -49,6 +49,12 @@ def build_parser() -> argparse.ArgumentParser:
action="store_true",
help="Run trusted fixtures without isolation; never use for untrusted code.",
)

verify_receipt = subparsers.add_parser(
"verify-receipt",
help="Recompute and verify a receipt's internal integrity.",
)
verify_receipt.add_argument("--receipt", required=True)
return parser


Expand All @@ -61,6 +67,8 @@ def main(argv: Sequence[str] | None = None) -> int:
return _approve(args)
if args.command == "validate":
return _validate(args)
if args.command == "verify-receipt":
return _verify_receipt(args)
raise ValueError("unsupported command")
except (OSError, RuntimeError, ValueError) as exc:
print(
Expand Down Expand Up @@ -174,6 +182,36 @@ def _validate(args: argparse.Namespace) -> int:
return 0 if receipt.success else 2


def _verify_receipt(args: argparse.Namespace) -> int:
receipt_path = Path(args.receipt).expanduser().resolve(strict=True)
receipt = ValidationReceipt.from_json(
read_bounded_regular(receipt_path).decode("utf-8")
)
print(
json.dumps(
{
"status": "receipt_integrity_verified",
"integrity_valid": True,
"validation_success": receipt.success,
"proof_grade": receipt.proof_grade,
"isolated": receipt.isolated,
"receipt_hash": receipt.receipt_hash,
"phases": [
{
"name": phase.name,
"passed": phase.passed,
"tests_run": phase.tests_run,
}
for phase in receipt.phases
],
},
ensure_ascii=False,
sort_keys=True,
)
)
return 0


def _outside_repository(repository_root: Path, value: Path) -> Path:
target = absolute_no_resolve(value)
comparison = target.resolve(strict=False)
Expand Down
20 changes: 20 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,26 @@ def test_end_to_end_unsafe_local_is_explicitly_not_proof_grade(self) -> None:
self.assertFalse(result["proof_grade"])
self.assertEqual(len(list(audit.glob("*.json"))), 1)
self.assertEqual(len(list(audit.glob("*.md"))), 1)
receipt = next(audit.glob("*.json"))
verify_output = io.StringIO()
with redirect_stdout(verify_output), redirect_stderr(io.StringIO()):
verify_status = main(
["verify-receipt", "--receipt", str(receipt)]
)
verified = json.loads(verify_output.getvalue())
self.assertEqual(verify_status, 0)
self.assertTrue(verified["integrity_valid"])
self.assertTrue(verified["validation_success"])
self.assertFalse(verified["proof_grade"])

tampered = parent / "tampered-receipt.json"
tampered_payload = json.loads(receipt.read_text())
tampered_payload["success"] = False
tampered.write_text(json.dumps(tampered_payload), encoding="utf-8")
self.assertEqual(
self._main(["verify-receipt", "--receipt", str(tampered)]),
1,
)

@staticmethod
def _main(arguments: list[str]) -> int:
Expand Down
36 changes: 36 additions & 0 deletions tests/test_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from __future__ import annotations

import os
import subprocess
import sys
import unittest
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]


class DemoTests(unittest.TestCase):
def test_trusted_local_demo_is_truthful_and_repeatable(self) -> None:
environment = os.environ.copy()
environment["PYTHONPATH"] = str(ROOT / "src")
for _ in range(2):
completed = subprocess.run(
[sys.executable, str(ROOT / "scripts" / "run_demo.py")],
cwd=ROOT,
env=environment,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=30,
check=False,
)
self.assertEqual(completed.returncode, 0, completed.stderr)
self.assertIn("validation: PASSED", completed.stdout)
self.assertIn("repository unchanged: YES", completed.stdout)
self.assertIn("receipt integrity: VERIFIED", completed.stdout)
self.assertIn("proof grade: NO", completed.stdout)


if __name__ == "__main__":
unittest.main()
Loading