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.
@@ -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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,2 @@
1
+ [console_scripts]
2
+ doneproof = doneproof.cli:main
@@ -0,0 +1,3 @@
1
+
2
+ [test]
3
+ pytest>=8
@@ -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