doneproof 0.1.0__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.
- doneproof-0.1.0/LICENSE +21 -0
- doneproof-0.1.0/NOTICE +9 -0
- doneproof-0.1.0/PKG-INFO +109 -0
- doneproof-0.1.0/README.md +81 -0
- doneproof-0.1.0/pyproject.toml +63 -0
- doneproof-0.1.0/setup.cfg +4 -0
- doneproof-0.1.0/src/doneproof/__init__.py +102 -0
- doneproof-0.1.0/src/doneproof/cli.py +53 -0
- doneproof-0.1.0/src/doneproof/py.typed +0 -0
- doneproof-0.1.0/src/doneproof.egg-info/PKG-INFO +109 -0
- doneproof-0.1.0/src/doneproof.egg-info/SOURCES.txt +14 -0
- doneproof-0.1.0/src/doneproof.egg-info/dependency_links.txt +1 -0
- doneproof-0.1.0/src/doneproof.egg-info/entry_points.txt +2 -0
- doneproof-0.1.0/src/doneproof.egg-info/requires.txt +3 -0
- doneproof-0.1.0/src/doneproof.egg-info/top_level.txt +1 -0
- doneproof-0.1.0/tests/test_doneproof.py +135 -0
doneproof-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LRNSdigital (souzalrns)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
doneproof-0.1.0/NOTICE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
doneproof
|
|
2
|
+
Copyright (c) 2026 LRNSdigital (souzalrns). Licensed under the MIT License (see LICENSE).
|
|
3
|
+
|
|
4
|
+
doneproof has no third-party runtime dependencies. The conditions are plain strings and the
|
|
5
|
+
engine is the Python standard library only (re, pathlib). Nothing is bundled or vendored.
|
|
6
|
+
|
|
7
|
+
Extracted from the governed runner at github.com/souzalrns/network-agents-setup
|
|
8
|
+
(runner/plan_runner/done_when.py). The runner keeps the glue that binds this engine to its
|
|
9
|
+
event log; this package is the standalone, store-agnostic core.
|
doneproof-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: doneproof
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Accept a task only when its done-ness is proven by evidence (a file exists, a human gate was resolved) — never the agent's own word. Plain data, zero dependencies, pluggable into any runner or agent.
|
|
5
|
+
Author: LRNSdigital
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/souzalrns/network-agents-setup/tree/main/oss/doneproof
|
|
8
|
+
Project-URL: Issues, https://github.com/souzalrns/network-agents-setup/issues
|
|
9
|
+
Keywords: acceptance,done-criteria,evidence,agents,task-completion,verification,claude-code,plugin,automation,gate
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing :: Acceptance
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
License-File: NOTICE
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# doneproof
|
|
30
|
+
|
|
31
|
+
**Accept a task only when its done-ness is proven by evidence — never the agent's own word.**
|
|
32
|
+
|
|
33
|
+
An agent (or a plan) declares *doneproof* conditions as plain strings. `doneproof`
|
|
34
|
+
checks each one against real evidence and reports **passed**, **failed** or
|
|
35
|
+
**unverifiable**. Zero dependencies, pure stdlib, pluggable into any runner or agent.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from doneproof import evaluate, is_satisfied
|
|
40
|
+
|
|
41
|
+
result = evaluate(Path("runs/42"), [
|
|
42
|
+
"artifacts/report.md exists",
|
|
43
|
+
"human_gate_resolved on publish",
|
|
44
|
+
], gate_resolved=lambda step: step in resolved_steps)
|
|
45
|
+
|
|
46
|
+
if not is_satisfied(result):
|
|
47
|
+
raise SystemExit(f"not done: {result['failed']}")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or from the shell:
|
|
51
|
+
|
|
52
|
+
```console
|
|
53
|
+
$ doneproof check runs/42 "artifacts/report.md exists" "human_gate_resolved on publish" --resolved publish
|
|
54
|
+
ok file_exists: artifacts/report.md
|
|
55
|
+
ok gate_resolved: publish
|
|
56
|
+
doneproof: satisfied (0 failed, 0 unverifiable)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Why it exists
|
|
60
|
+
|
|
61
|
+
Most agent frameworks accept `status: done` because the agent *said* it was done.
|
|
62
|
+
That is the failure mode `doneproof` refuses. A task is accepted only when its
|
|
63
|
+
completion is **verifiable**:
|
|
64
|
+
|
|
65
|
+
| Condition | Evidence checked |
|
|
66
|
+
|-----------|------------------|
|
|
67
|
+
| `<path> exists` | the file exists under the run's root (no `../` escape) |
|
|
68
|
+
| `human_gate_resolved on <step>` | a human approval for `<step>` was recorded |
|
|
69
|
+
|
|
70
|
+
Everything else is reported as **unverifiable** — not failed. Refusing to fail on
|
|
71
|
+
what you cannot check by evidence is as important as refusing to pass on a say-so.
|
|
72
|
+
The caller decides what to do with the unverifiable bucket (log it, require review,
|
|
73
|
+
or ignore it).
|
|
74
|
+
|
|
75
|
+
## The gate seam (store-agnostic)
|
|
76
|
+
|
|
77
|
+
`doneproof` never talks to an event store, queue or database. "Was the gate on
|
|
78
|
+
`<step>` resolved?" is answered by a predicate you pass in:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
evaluate(root, conditions, gate_resolved=lambda step: my_log.approved(step))
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Omit it and gates **fail closed** (nothing is considered resolved). This one seam
|
|
85
|
+
is why the same engine runs inside a governed runner and standalone in a test.
|
|
86
|
+
|
|
87
|
+
## API
|
|
88
|
+
|
|
89
|
+
- `evaluate(root, conditions, gate_resolved=None) -> dict` — returns
|
|
90
|
+
`{"passed": [...], "failed": [...], "unverifiable": [...]}`, JSON-serializable.
|
|
91
|
+
`passed`/`failed` items are `{"kind", "target"}` (kind = `file_exists` |
|
|
92
|
+
`gate_resolved`); `failed` items also carry `{"condition", "reason"}`.
|
|
93
|
+
- `is_satisfied(result) -> bool` — true iff nothing failed (unverifiable does not fail).
|
|
94
|
+
- `GateResolved` — the `Callable[[str], bool]` type of the gate predicate.
|
|
95
|
+
|
|
96
|
+
## How it compares
|
|
97
|
+
|
|
98
|
+
`doneproof` is an **acceptance primitive** for agent/task runs, not a test runner
|
|
99
|
+
or a BDD framework. See `POSITIONING.md` for the evidence-based comparison with
|
|
100
|
+
pytest, BDD (Gherkin) and shell CI gates, and for what it deliberately does not do.
|
|
101
|
+
|
|
102
|
+
## Install
|
|
103
|
+
|
|
104
|
+
```console
|
|
105
|
+
pip install doneproof
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
MIT licensed. Part of the [network-agents-setup](https://github.com/souzalrns/network-agents-setup)
|
|
109
|
+
ecosystem; extracted from its governed runner so any project can accept tasks by evidence.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# doneproof
|
|
2
|
+
|
|
3
|
+
**Accept a task only when its done-ness is proven by evidence — never the agent's own word.**
|
|
4
|
+
|
|
5
|
+
An agent (or a plan) declares *doneproof* conditions as plain strings. `doneproof`
|
|
6
|
+
checks each one against real evidence and reports **passed**, **failed** or
|
|
7
|
+
**unverifiable**. Zero dependencies, pure stdlib, pluggable into any runner or agent.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from doneproof import evaluate, is_satisfied
|
|
12
|
+
|
|
13
|
+
result = evaluate(Path("runs/42"), [
|
|
14
|
+
"artifacts/report.md exists",
|
|
15
|
+
"human_gate_resolved on publish",
|
|
16
|
+
], gate_resolved=lambda step: step in resolved_steps)
|
|
17
|
+
|
|
18
|
+
if not is_satisfied(result):
|
|
19
|
+
raise SystemExit(f"not done: {result['failed']}")
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or from the shell:
|
|
23
|
+
|
|
24
|
+
```console
|
|
25
|
+
$ doneproof check runs/42 "artifacts/report.md exists" "human_gate_resolved on publish" --resolved publish
|
|
26
|
+
ok file_exists: artifacts/report.md
|
|
27
|
+
ok gate_resolved: publish
|
|
28
|
+
doneproof: satisfied (0 failed, 0 unverifiable)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Why it exists
|
|
32
|
+
|
|
33
|
+
Most agent frameworks accept `status: done` because the agent *said* it was done.
|
|
34
|
+
That is the failure mode `doneproof` refuses. A task is accepted only when its
|
|
35
|
+
completion is **verifiable**:
|
|
36
|
+
|
|
37
|
+
| Condition | Evidence checked |
|
|
38
|
+
|-----------|------------------|
|
|
39
|
+
| `<path> exists` | the file exists under the run's root (no `../` escape) |
|
|
40
|
+
| `human_gate_resolved on <step>` | a human approval for `<step>` was recorded |
|
|
41
|
+
|
|
42
|
+
Everything else is reported as **unverifiable** — not failed. Refusing to fail on
|
|
43
|
+
what you cannot check by evidence is as important as refusing to pass on a say-so.
|
|
44
|
+
The caller decides what to do with the unverifiable bucket (log it, require review,
|
|
45
|
+
or ignore it).
|
|
46
|
+
|
|
47
|
+
## The gate seam (store-agnostic)
|
|
48
|
+
|
|
49
|
+
`doneproof` never talks to an event store, queue or database. "Was the gate on
|
|
50
|
+
`<step>` resolved?" is answered by a predicate you pass in:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
evaluate(root, conditions, gate_resolved=lambda step: my_log.approved(step))
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Omit it and gates **fail closed** (nothing is considered resolved). This one seam
|
|
57
|
+
is why the same engine runs inside a governed runner and standalone in a test.
|
|
58
|
+
|
|
59
|
+
## API
|
|
60
|
+
|
|
61
|
+
- `evaluate(root, conditions, gate_resolved=None) -> dict` — returns
|
|
62
|
+
`{"passed": [...], "failed": [...], "unverifiable": [...]}`, JSON-serializable.
|
|
63
|
+
`passed`/`failed` items are `{"kind", "target"}` (kind = `file_exists` |
|
|
64
|
+
`gate_resolved`); `failed` items also carry `{"condition", "reason"}`.
|
|
65
|
+
- `is_satisfied(result) -> bool` — true iff nothing failed (unverifiable does not fail).
|
|
66
|
+
- `GateResolved` — the `Callable[[str], bool]` type of the gate predicate.
|
|
67
|
+
|
|
68
|
+
## How it compares
|
|
69
|
+
|
|
70
|
+
`doneproof` is an **acceptance primitive** for agent/task runs, not a test runner
|
|
71
|
+
or a BDD framework. See `POSITIONING.md` for the evidence-based comparison with
|
|
72
|
+
pytest, BDD (Gherkin) and shell CI gates, and for what it deliberately does not do.
|
|
73
|
+
|
|
74
|
+
## Install
|
|
75
|
+
|
|
76
|
+
```console
|
|
77
|
+
pip install doneproof
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
MIT licensed. Part of the [network-agents-setup](https://github.com/souzalrns/network-agents-setup)
|
|
81
|
+
ecosystem; extracted from its governed runner so any project can accept tasks by evidence.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "doneproof"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Accept a task only when its done-ness is proven by evidence (a file exists, a human gate was resolved) — never the agent's own word. Plain data, zero dependencies, pluggable into any runner or agent."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE", "NOTICE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "LRNSdigital" }]
|
|
14
|
+
keywords = [
|
|
15
|
+
"acceptance", "done-criteria", "evidence", "agents", "task-completion",
|
|
16
|
+
"verification", "claude-code", "plugin", "automation", "gate",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 3 - Alpha",
|
|
20
|
+
"Environment :: Console",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
29
|
+
"Topic :: Software Development :: Testing :: Acceptance",
|
|
30
|
+
]
|
|
31
|
+
# Zero runtime dependencies: the conditions are plain strings, the engine is pure stdlib.
|
|
32
|
+
dependencies = []
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
test = ["pytest>=8"]
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
doneproof = "doneproof.cli:main"
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Repository = "https://github.com/souzalrns/network-agents-setup/tree/main/oss/doneproof"
|
|
42
|
+
Issues = "https://github.com/souzalrns/network-agents-setup/issues"
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
where = ["src"]
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.package-data]
|
|
48
|
+
doneproof = ["py.typed"] # PEP 561: ships its own types
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
testpaths = ["tests"]
|
|
52
|
+
addopts = "-ra"
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
line-length = 120
|
|
56
|
+
target-version = "py310"
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint]
|
|
59
|
+
select = ["E", "F", "W", "I", "B", "UP", "S", "RUF"]
|
|
60
|
+
ignore = ["RUF001", "RUF002", "RUF003"] # intentional unicode arrows/dashes in messages
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint.per-file-ignores]
|
|
63
|
+
"tests/**" = ["S101", "S108"]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""doneproof: accept a task only when its done-ness is proven by evidence.
|
|
2
|
+
|
|
3
|
+
An agent (or a plan) declares *doneproof* conditions in plain strings. This
|
|
4
|
+
package classifies each one as **passed**, **failed** or **unverifiable** by
|
|
5
|
+
checking real evidence — never the agent's own word that it finished.
|
|
6
|
+
|
|
7
|
+
Two evidence kinds are understood (the only two that verify without
|
|
8
|
+
interpreting free text):
|
|
9
|
+
|
|
10
|
+
- ``<path> exists`` — the file exists under the run's root directory. The path
|
|
11
|
+
is resolved and must stay inside the root (no ``../`` escape).
|
|
12
|
+
- ``human_gate_resolved on <step>`` — a human approval for ``<step>`` was
|
|
13
|
+
recorded. "Was it recorded?" is answered by a caller-supplied predicate
|
|
14
|
+
(``gate_resolved``), so this package never depends on any particular event
|
|
15
|
+
store, queue or database.
|
|
16
|
+
|
|
17
|
+
A condition in any other shape is **not failed** — it is reported as
|
|
18
|
+
``unverifiable`` so the caller can decide. Refusing to fail on what you cannot
|
|
19
|
+
check by evidence is as important as refusing to pass on an agent's say-so.
|
|
20
|
+
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from doneproof import evaluate, is_satisfied
|
|
23
|
+
|
|
24
|
+
result = evaluate(Path("runs/42"), ["artifacts/report.md exists"])
|
|
25
|
+
assert is_satisfied(result)
|
|
26
|
+
|
|
27
|
+
The return value is a plain JSON-serializable dict so it drops straight into a
|
|
28
|
+
log or an event. ``is_satisfied`` is true iff there are no failures;
|
|
29
|
+
unverifiable conditions do not make a run fail.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import re
|
|
35
|
+
from collections.abc import Callable
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
__version__ = "0.1.0"
|
|
40
|
+
|
|
41
|
+
__all__ = ["GateResolved", "__version__", "evaluate", "is_satisfied"]
|
|
42
|
+
|
|
43
|
+
# "Was the human gate on <step> resolved?" — the one thing this package cannot
|
|
44
|
+
# check from the filesystem. The caller supplies it (bound to its own event log,
|
|
45
|
+
# queue, DB, …). Default: nothing is resolved, so gate conditions fail closed.
|
|
46
|
+
GateResolved = Callable[[str], bool]
|
|
47
|
+
|
|
48
|
+
_EXISTS = re.compile(r"^(?P<path>\S+)\s+exists$")
|
|
49
|
+
_GATE = re.compile(r"^human_gate_resolved\s+on\s+(?P<step>\S+)$")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def evaluate(
|
|
53
|
+
root: str | Path,
|
|
54
|
+
conditions: list[str],
|
|
55
|
+
gate_resolved: GateResolved | None = None,
|
|
56
|
+
) -> dict[str, list[Any]]:
|
|
57
|
+
"""Classify each condition into ``passed``, ``failed`` or ``unverifiable``.
|
|
58
|
+
|
|
59
|
+
``root`` is the run directory that ``<path> exists`` conditions resolve
|
|
60
|
+
against; a path that escapes it fails. ``gate_resolved(step) -> bool``
|
|
61
|
+
answers ``human_gate_resolved on <step>``; when omitted, no gate is
|
|
62
|
+
considered resolved (fail-closed).
|
|
63
|
+
|
|
64
|
+
Returns ``{"passed": [...], "failed": [...], "unverifiable": [...]}``.
|
|
65
|
+
``passed`` and ``failed`` items are ``{"kind", "target"}``
|
|
66
|
+
(kind = ``file_exists`` | ``gate_resolved``); ``failed`` items also carry
|
|
67
|
+
``{"condition", "reason"}``. ``unverifiable`` keeps the original strings.
|
|
68
|
+
Passed items omit the original text so a reader scanning for the literal
|
|
69
|
+
``human_gate_resolved`` does not confuse an accepted condition with the
|
|
70
|
+
real approval event.
|
|
71
|
+
"""
|
|
72
|
+
resolved = gate_resolved if gate_resolved is not None else (lambda _step: False)
|
|
73
|
+
result: dict[str, list[Any]] = {"passed": [], "failed": [], "unverifiable": []}
|
|
74
|
+
base = Path(root).resolve()
|
|
75
|
+
for raw in conditions:
|
|
76
|
+
cond = str(raw).strip()
|
|
77
|
+
m = _EXISTS.match(cond)
|
|
78
|
+
if m:
|
|
79
|
+
item = {"kind": "file_exists", "target": m.group("path")}
|
|
80
|
+
target = (base / m.group("path")).resolve()
|
|
81
|
+
if not target.is_relative_to(base):
|
|
82
|
+
result["failed"].append({**item, "condition": cond, "reason": "path outside the run directory"})
|
|
83
|
+
elif target.is_file():
|
|
84
|
+
result["passed"].append(item)
|
|
85
|
+
else:
|
|
86
|
+
result["failed"].append({**item, "condition": cond, "reason": "file missing"})
|
|
87
|
+
continue
|
|
88
|
+
m = _GATE.match(cond)
|
|
89
|
+
if m:
|
|
90
|
+
item = {"kind": "gate_resolved", "target": m.group("step")}
|
|
91
|
+
if resolved(m.group("step")):
|
|
92
|
+
result["passed"].append(item)
|
|
93
|
+
else:
|
|
94
|
+
result["failed"].append({**item, "condition": cond, "reason": "gate not resolved"})
|
|
95
|
+
continue
|
|
96
|
+
result["unverifiable"].append(cond)
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def is_satisfied(result: dict[str, list[Any]]) -> bool:
|
|
101
|
+
"""True iff nothing failed. Unverifiable conditions do not fail a run."""
|
|
102
|
+
return not result["failed"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""doneproof CLI: check acceptance conditions by evidence, exit 0/1.
|
|
2
|
+
|
|
3
|
+
doneproof check <root> [CONDITION ...] [--resolved STEP ...] [--json]
|
|
4
|
+
|
|
5
|
+
Exit 0 when nothing failed (unverifiable conditions do not fail), 1 otherwise.
|
|
6
|
+
``--resolved STEP`` marks a human gate as resolved (repeatable); without it,
|
|
7
|
+
``human_gate_resolved on <step>`` fails closed. ``--json`` prints the full
|
|
8
|
+
structured result instead of the human summary.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
from . import __version__, evaluate, is_satisfied
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main(argv: list[str] | None = None) -> int:
|
|
21
|
+
ap = argparse.ArgumentParser(prog="doneproof", description=__doc__.splitlines()[0])
|
|
22
|
+
ap.add_argument("--version", action="version", version=f"doneproof {__version__}")
|
|
23
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
24
|
+
c = sub.add_parser("check", help="Check conditions against evidence under <root>")
|
|
25
|
+
c.add_argument("root", help="The run directory that `<path> exists` resolves against")
|
|
26
|
+
c.add_argument("conditions", nargs="*", help="doneproof conditions, e.g. 'artifacts/r.md exists'")
|
|
27
|
+
c.add_argument(
|
|
28
|
+
"--resolved", action="append", default=[], metavar="STEP", help="Mark a human gate as resolved (repeatable)"
|
|
29
|
+
)
|
|
30
|
+
c.add_argument("--json", action="store_true", help="Print the full structured result")
|
|
31
|
+
args = ap.parse_args(argv)
|
|
32
|
+
|
|
33
|
+
resolved = set(args.resolved)
|
|
34
|
+
result = evaluate(args.root, args.conditions, gate_resolved=lambda step: step in resolved)
|
|
35
|
+
|
|
36
|
+
if args.json:
|
|
37
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
38
|
+
else:
|
|
39
|
+
for item in result["passed"]:
|
|
40
|
+
print(f"ok {item['kind']}: {item['target']}")
|
|
41
|
+
for item in result["failed"]:
|
|
42
|
+
print(f"FAIL {item['kind']}: {item['target']} ({item['reason']})")
|
|
43
|
+
for cond in result["unverifiable"]:
|
|
44
|
+
print(f"? unverifiable: {cond}")
|
|
45
|
+
print(
|
|
46
|
+
f"doneproof: {'satisfied' if is_satisfied(result) else 'FAILED'} "
|
|
47
|
+
f"({len(result['failed'])} failed, {len(result['unverifiable'])} unverifiable)"
|
|
48
|
+
)
|
|
49
|
+
return 0 if is_satisfied(result) else 1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
sys.exit(main())
|
|
File without changes
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: doneproof
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Accept a task only when its done-ness is proven by evidence (a file exists, a human gate was resolved) — never the agent's own word. Plain data, zero dependencies, pluggable into any runner or agent.
|
|
5
|
+
Author: LRNSdigital
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/souzalrns/network-agents-setup/tree/main/oss/doneproof
|
|
8
|
+
Project-URL: Issues, https://github.com/souzalrns/network-agents-setup/issues
|
|
9
|
+
Keywords: acceptance,done-criteria,evidence,agents,task-completion,verification,claude-code,plugin,automation,gate
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing :: Acceptance
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
License-File: NOTICE
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# doneproof
|
|
30
|
+
|
|
31
|
+
**Accept a task only when its done-ness is proven by evidence — never the agent's own word.**
|
|
32
|
+
|
|
33
|
+
An agent (or a plan) declares *doneproof* conditions as plain strings. `doneproof`
|
|
34
|
+
checks each one against real evidence and reports **passed**, **failed** or
|
|
35
|
+
**unverifiable**. Zero dependencies, pure stdlib, pluggable into any runner or agent.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from doneproof import evaluate, is_satisfied
|
|
40
|
+
|
|
41
|
+
result = evaluate(Path("runs/42"), [
|
|
42
|
+
"artifacts/report.md exists",
|
|
43
|
+
"human_gate_resolved on publish",
|
|
44
|
+
], gate_resolved=lambda step: step in resolved_steps)
|
|
45
|
+
|
|
46
|
+
if not is_satisfied(result):
|
|
47
|
+
raise SystemExit(f"not done: {result['failed']}")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or from the shell:
|
|
51
|
+
|
|
52
|
+
```console
|
|
53
|
+
$ doneproof check runs/42 "artifacts/report.md exists" "human_gate_resolved on publish" --resolved publish
|
|
54
|
+
ok file_exists: artifacts/report.md
|
|
55
|
+
ok gate_resolved: publish
|
|
56
|
+
doneproof: satisfied (0 failed, 0 unverifiable)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Why it exists
|
|
60
|
+
|
|
61
|
+
Most agent frameworks accept `status: done` because the agent *said* it was done.
|
|
62
|
+
That is the failure mode `doneproof` refuses. A task is accepted only when its
|
|
63
|
+
completion is **verifiable**:
|
|
64
|
+
|
|
65
|
+
| Condition | Evidence checked |
|
|
66
|
+
|-----------|------------------|
|
|
67
|
+
| `<path> exists` | the file exists under the run's root (no `../` escape) |
|
|
68
|
+
| `human_gate_resolved on <step>` | a human approval for `<step>` was recorded |
|
|
69
|
+
|
|
70
|
+
Everything else is reported as **unverifiable** — not failed. Refusing to fail on
|
|
71
|
+
what you cannot check by evidence is as important as refusing to pass on a say-so.
|
|
72
|
+
The caller decides what to do with the unverifiable bucket (log it, require review,
|
|
73
|
+
or ignore it).
|
|
74
|
+
|
|
75
|
+
## The gate seam (store-agnostic)
|
|
76
|
+
|
|
77
|
+
`doneproof` never talks to an event store, queue or database. "Was the gate on
|
|
78
|
+
`<step>` resolved?" is answered by a predicate you pass in:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
evaluate(root, conditions, gate_resolved=lambda step: my_log.approved(step))
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Omit it and gates **fail closed** (nothing is considered resolved). This one seam
|
|
85
|
+
is why the same engine runs inside a governed runner and standalone in a test.
|
|
86
|
+
|
|
87
|
+
## API
|
|
88
|
+
|
|
89
|
+
- `evaluate(root, conditions, gate_resolved=None) -> dict` — returns
|
|
90
|
+
`{"passed": [...], "failed": [...], "unverifiable": [...]}`, JSON-serializable.
|
|
91
|
+
`passed`/`failed` items are `{"kind", "target"}` (kind = `file_exists` |
|
|
92
|
+
`gate_resolved`); `failed` items also carry `{"condition", "reason"}`.
|
|
93
|
+
- `is_satisfied(result) -> bool` — true iff nothing failed (unverifiable does not fail).
|
|
94
|
+
- `GateResolved` — the `Callable[[str], bool]` type of the gate predicate.
|
|
95
|
+
|
|
96
|
+
## How it compares
|
|
97
|
+
|
|
98
|
+
`doneproof` is an **acceptance primitive** for agent/task runs, not a test runner
|
|
99
|
+
or a BDD framework. See `POSITIONING.md` for the evidence-based comparison with
|
|
100
|
+
pytest, BDD (Gherkin) and shell CI gates, and for what it deliberately does not do.
|
|
101
|
+
|
|
102
|
+
## Install
|
|
103
|
+
|
|
104
|
+
```console
|
|
105
|
+
pip install doneproof
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
MIT licensed. Part of the [network-agents-setup](https://github.com/souzalrns/network-agents-setup)
|
|
109
|
+
ecosystem; extracted from its governed runner so any project can accept tasks by evidence.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
NOTICE
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
src/doneproof/__init__.py
|
|
6
|
+
src/doneproof/cli.py
|
|
7
|
+
src/doneproof/py.typed
|
|
8
|
+
src/doneproof.egg-info/PKG-INFO
|
|
9
|
+
src/doneproof.egg-info/SOURCES.txt
|
|
10
|
+
src/doneproof.egg-info/dependency_links.txt
|
|
11
|
+
src/doneproof.egg-info/entry_points.txt
|
|
12
|
+
src/doneproof.egg-info/requires.txt
|
|
13
|
+
src/doneproof.egg-info/top_level.txt
|
|
14
|
+
tests/test_doneproof.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
doneproof
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""doneproof: acceptance by evidence (the two verifiable forms + unverifiable)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from doneproof import evaluate, is_satisfied
|
|
10
|
+
from doneproof.cli import main
|
|
11
|
+
|
|
12
|
+
# ---- form 1: "<path> exists"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_exists_passes_when_file_present(tmp_path):
|
|
16
|
+
(tmp_path / "artifacts").mkdir()
|
|
17
|
+
(tmp_path / "artifacts" / "r.md").write_text("x", encoding="utf-8")
|
|
18
|
+
r = evaluate(tmp_path, ["artifacts/r.md exists"])
|
|
19
|
+
assert r == {"passed": [{"kind": "file_exists", "target": "artifacts/r.md"}], "failed": [], "unverifiable": []}
|
|
20
|
+
assert is_satisfied(r)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_exists_fails_with_reason_when_missing(tmp_path):
|
|
24
|
+
r = evaluate(tmp_path, ["artifacts/zz.md exists"])
|
|
25
|
+
assert r["passed"] == [] and r["unverifiable"] == []
|
|
26
|
+
assert r["failed"] == [
|
|
27
|
+
{
|
|
28
|
+
"kind": "file_exists",
|
|
29
|
+
"target": "artifacts/zz.md",
|
|
30
|
+
"condition": "artifacts/zz.md exists",
|
|
31
|
+
"reason": "file missing",
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
assert not is_satisfied(r)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_exists_outside_root_fails(tmp_path):
|
|
38
|
+
r = evaluate(tmp_path, ["../secret.md exists"])
|
|
39
|
+
assert r["failed"][0]["reason"] == "path outside the run directory"
|
|
40
|
+
assert not is_satisfied(r)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_directory_is_not_a_file(tmp_path):
|
|
44
|
+
(tmp_path / "artifacts").mkdir()
|
|
45
|
+
r = evaluate(tmp_path, ["artifacts exists"])
|
|
46
|
+
assert r["failed"][0]["reason"] == "file missing"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---- form 2: "human_gate_resolved on <step>"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_gate_passes_when_resolver_says_so(tmp_path):
|
|
53
|
+
r = evaluate(tmp_path, ["human_gate_resolved on hitl"], gate_resolved=lambda step: step == "hitl")
|
|
54
|
+
assert r["passed"] == [{"kind": "gate_resolved", "target": "hitl"}]
|
|
55
|
+
assert is_satisfied(r)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_gate_fails_closed_by_default(tmp_path):
|
|
59
|
+
r = evaluate(tmp_path, ["human_gate_resolved on hitl"])
|
|
60
|
+
assert r["failed"] == [
|
|
61
|
+
{
|
|
62
|
+
"kind": "gate_resolved",
|
|
63
|
+
"target": "hitl",
|
|
64
|
+
"condition": "human_gate_resolved on hitl",
|
|
65
|
+
"reason": "gate not resolved",
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_passed_items_drop_the_literal_gate_text(tmp_path):
|
|
71
|
+
"""A reader scanning for 'human_gate_resolved' must not match an accepted condition."""
|
|
72
|
+
r = evaluate(tmp_path, ["human_gate_resolved on hitl"], gate_resolved=lambda _s: True)
|
|
73
|
+
assert "human_gate_resolved" not in json.dumps(r["passed"])
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---- form 3: unverifiable — reported, never failed
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_unknown_shape_is_unverifiable_not_failed(tmp_path):
|
|
80
|
+
cond = "no ad_account_mutation without approve"
|
|
81
|
+
r = evaluate(tmp_path, [cond, "x.md exists"])
|
|
82
|
+
assert r["unverifiable"] == [cond]
|
|
83
|
+
# the free-text condition does not fail the set; only the missing file does
|
|
84
|
+
assert [f["kind"] for f in r["failed"]] == ["file_exists"]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_empty_conditions_is_satisfied(tmp_path):
|
|
88
|
+
assert evaluate(tmp_path, []) == {"passed": [], "failed": [], "unverifiable": []}
|
|
89
|
+
assert is_satisfied(evaluate(tmp_path, []))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ---- autonomy test: no event store, no external deps
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_runs_with_no_gate_resolver_and_pure_stdlib(tmp_path):
|
|
96
|
+
(tmp_path / "a").write_text("1", encoding="utf-8")
|
|
97
|
+
r = evaluate(tmp_path, ["a exists", "human_gate_resolved on g", "free text"])
|
|
98
|
+
assert r["passed"] == [{"kind": "file_exists", "target": "a"}]
|
|
99
|
+
assert r["failed"][0]["kind"] == "gate_resolved"
|
|
100
|
+
assert r["unverifiable"] == ["free text"]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---- CLI
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_cli_exit_0_when_satisfied(tmp_path, capsys):
|
|
107
|
+
(tmp_path / "a").write_text("1", encoding="utf-8")
|
|
108
|
+
assert main(["check", str(tmp_path), "a exists"]) == 0
|
|
109
|
+
assert "satisfied" in capsys.readouterr().out
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_cli_exit_1_when_failed(tmp_path, capsys):
|
|
113
|
+
assert main(["check", str(tmp_path), "missing exists"]) == 1
|
|
114
|
+
assert "FAILED" in capsys.readouterr().out
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_cli_resolved_flag(tmp_path):
|
|
118
|
+
assert main(["check", str(tmp_path), "human_gate_resolved on hitl", "--resolved", "hitl"]) == 0
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_cli_unverifiable_does_not_fail(tmp_path):
|
|
122
|
+
assert main(["check", str(tmp_path), "something free"]) == 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_cli_json_output(tmp_path, capsys):
|
|
126
|
+
main(["check", str(tmp_path), "missing exists", "--json"])
|
|
127
|
+
out = json.loads(capsys.readouterr().out)
|
|
128
|
+
assert out["failed"][0]["target"] == "missing"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_cli_version(capsys):
|
|
132
|
+
with pytest.raises(SystemExit) as e:
|
|
133
|
+
main(["--version"])
|
|
134
|
+
assert e.value.code == 0
|
|
135
|
+
assert "doneproof" in capsys.readouterr().out
|