@relaxedg/proof-gate 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/action.yml +56 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +84 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.js +42 -0
- package/dist/parse.d.ts +5 -0
- package/dist/parse.js +35 -0
- package/dist/types.d.ts +59 -0
- package/dist/types.js +1 -0
- package/dist/verify.d.ts +6 -0
- package/dist/verify.js +213 -0
- package/docs/done-means-proven.md +103 -0
- package/examples/toy-service/evidence/db-receipt.json +4 -0
- package/examples/toy-service/evidence/no-go.md +1 -0
- package/examples/toy-service/evidence/runtime-receipt.json +5 -0
- package/examples/toy-service/evidence/screenshot.gif +1 -0
- package/examples/toy-service/evidence/unit-test.txt +1 -0
- package/examples/toy-service/lies/bare-done.yml +7 -0
- package/examples/toy-service/lies/runtime-proven-local.yml +8 -0
- package/examples/toy-service/pr-body.md +17 -0
- package/examples/toy-service/release.json +4 -0
- package/examples/toy-service/tasks/001-local.yml +8 -0
- package/examples/toy-service/tasks/002-pr-open.yml +8 -0
- package/examples/toy-service/tasks/003-merged.yml +8 -0
- package/examples/toy-service/tasks/004-deployed.yml +10 -0
- package/examples/toy-service/tasks/005-runtime-proven.yml +9 -0
- package/examples/toy-service/tasks/006-closed-no-go.yml +9 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Greg Bond
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# proof-gate
|
|
2
|
+
|
|
3
|
+
A tiny CLI and GitHub Action that makes `done` require class-aware, inspectable evidence.
|
|
4
|
+
|
|
5
|
+
Most workflows use one word for several different claims:
|
|
6
|
+
|
|
7
|
+
- code changed
|
|
8
|
+
- checks passed
|
|
9
|
+
- a PR merged
|
|
10
|
+
- a deploy marker moved
|
|
11
|
+
- a live system behaved correctly
|
|
12
|
+
- a team correctly decided not to ship
|
|
13
|
+
|
|
14
|
+
`proof-gate` forces the claim to name its proof class and then runs the matching verifier. It does not pretend that a pointer is truth. Every pass prints an honesty depth:
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
existence < shape < inspected < reran
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx @relaxedg/proof-gate@latest --help
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
For a local checkout:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install
|
|
30
|
+
npm run build
|
|
31
|
+
npm test
|
|
32
|
+
npm run check
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick demo
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx @relaxedg/proof-gate@latest check examples/toy-service/tasks
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Example output:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
PASS LOCAL-1 [local/existence]: file exists: ../evidence/unit-test.txt
|
|
45
|
+
PASS PR-1 [ci/shape]: GitHub CI/PR URL shape accepted: https://github.com/gregbond/proof-gate/actions/runs/123456789
|
|
46
|
+
PASS MERGED-1 [ci/shape]: GitHub CI/PR URL shape accepted: https://github.com/gregbond/proof-gate/commit/abcdef1
|
|
47
|
+
PASS DEPLOY-1 [deploy/inspected]: release marker sha matches abc123
|
|
48
|
+
PASS RUNTIME-1 [runtime/inspected]: runtime receipt status 200
|
|
49
|
+
PASS NO-GO-1 [no-go/inspected]: no-go evidence file inspected: ../evidence/no-go.md
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
And the two lies this exists to reject:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
FAIL LIE-1: status=done requires a proof_state from local_only, pr_open, merged, deployed, runtime_proven, closed_no_go; got missing
|
|
56
|
+
FAIL LIE-2: proof_state=runtime_proven cannot be justified by class=local; expected one of runtime, db, visual
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Proof classes
|
|
60
|
+
|
|
61
|
+
Opinionated defaults:
|
|
62
|
+
|
|
63
|
+
- `local`: local files or commands
|
|
64
|
+
- `ci`: CI, PR, check, or commit evidence
|
|
65
|
+
- `deploy`: release marker inspection, usually a SHA comparison
|
|
66
|
+
- `runtime`: live healthcheck rerun or saved runtime receipt inspection
|
|
67
|
+
- `db`: query rerun or DB receipt inspection
|
|
68
|
+
- `visual`: screenshot/image inspection
|
|
69
|
+
- `no-go`: proof that the correct outcome was not to ship
|
|
70
|
+
|
|
71
|
+
`closed_no_go` is first-class. A stopped lane can be the correct result when the proof says stop.
|
|
72
|
+
|
|
73
|
+
## Task contract
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
id: RUNTIME-1
|
|
77
|
+
status: done
|
|
78
|
+
proof_state: runtime_proven
|
|
79
|
+
proof:
|
|
80
|
+
class: runtime
|
|
81
|
+
evidence:
|
|
82
|
+
- type: receipt
|
|
83
|
+
value: ../evidence/runtime-receipt.json
|
|
84
|
+
expected_status: 200
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`status: done` requires a non-`none` `proof_state`, a coherent `proof.class`, and at least one evidence pointer that the class verifier can inspect or rerun.
|
|
88
|
+
|
|
89
|
+
## PR body check
|
|
90
|
+
|
|
91
|
+
Put a fenced `Proof:` block in the PR body:
|
|
92
|
+
|
|
93
|
+
````markdown
|
|
94
|
+
Proof:
|
|
95
|
+
```yaml
|
|
96
|
+
id: PR-42
|
|
97
|
+
status: done
|
|
98
|
+
proof_state: deployed
|
|
99
|
+
expected_sha: abc123
|
|
100
|
+
proof:
|
|
101
|
+
class: deploy
|
|
102
|
+
evidence:
|
|
103
|
+
- type: release_json
|
|
104
|
+
value: examples/toy-service/release.json
|
|
105
|
+
expected_sha: abc123
|
|
106
|
+
```
|
|
107
|
+
````
|
|
108
|
+
|
|
109
|
+
Then run:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
proof-gate check-pr pr-body.md
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## GitHub Action
|
|
116
|
+
|
|
117
|
+
```yaml
|
|
118
|
+
name: proof-gate
|
|
119
|
+
on:
|
|
120
|
+
pull_request:
|
|
121
|
+
types: [opened, edited, synchronize, reopened]
|
|
122
|
+
|
|
123
|
+
jobs:
|
|
124
|
+
proof-gate:
|
|
125
|
+
runs-on: ubuntu-latest
|
|
126
|
+
steps:
|
|
127
|
+
- uses: actions/checkout@v4
|
|
128
|
+
- uses: gregbond/proof-gate@v0.1
|
|
129
|
+
with:
|
|
130
|
+
mode: pr
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
For a task directory:
|
|
134
|
+
|
|
135
|
+
```yaml
|
|
136
|
+
- uses: actions/checkout@v4
|
|
137
|
+
- uses: gregbond/proof-gate@v0.1
|
|
138
|
+
with:
|
|
139
|
+
mode: check
|
|
140
|
+
path: examples/toy-service/tasks
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## What v0.1 can and cannot prove
|
|
144
|
+
|
|
145
|
+
`proof-gate` does not magically prove every external truth. It makes proof claims inspectable, class-aware, and verifier-backed with explicit honesty depth.
|
|
146
|
+
|
|
147
|
+
Current verifier depths:
|
|
148
|
+
|
|
149
|
+
| Class | v0.1 behavior | Typical depth |
|
|
150
|
+
| --- | --- | --- |
|
|
151
|
+
| local | file exists, or command exits 0 with `--allow-exec` | existence or reran |
|
|
152
|
+
| ci | GitHub PR, commit, check, or Actions run URL shape | shape |
|
|
153
|
+
| deploy | reads release marker and compares SHA | inspected |
|
|
154
|
+
| runtime | reads saved receipt, or performs GET with `--network` | inspected or reran |
|
|
155
|
+
| db | reads count receipt, or runs command/query with `--allow-exec` | inspected or reran |
|
|
156
|
+
| visual | verifies screenshot magic bytes | inspected |
|
|
157
|
+
| no-go | requires reason plus non-empty file or URL | inspected or shape |
|
|
158
|
+
|
|
159
|
+
The class to verifier seam is the product. Richer adapters should raise claims from `existence` or `shape` to `inspected` or `reran`.
|
|
160
|
+
|
|
161
|
+
## Contributing
|
|
162
|
+
|
|
163
|
+
v0.1 is intentionally small. The most useful contributions are new verifiers that raise honesty depth from `existence` or `shape` to `inspected` or `reran`, especially for real CI, deploy, runtime, DB, and issue-tracker workflows.
|
|
164
|
+
|
|
165
|
+
If you try `proof-gate` and it does not fit where your team declares "done," please open an issue with:
|
|
166
|
+
|
|
167
|
+
- where the done signal lives
|
|
168
|
+
- what evidence already exists
|
|
169
|
+
- what proof class you expected
|
|
170
|
+
- what verifier would make the claim inspectable or rerunnable
|
|
171
|
+
|
|
172
|
+
## Essay
|
|
173
|
+
|
|
174
|
+
Read the launch essay: [Done Means Proven](docs/done-means-proven.md).
|
package/action.yml
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: proof-gate
|
|
2
|
+
description: Make done require class-aware, inspectable evidence.
|
|
3
|
+
inputs:
|
|
4
|
+
mode:
|
|
5
|
+
description: Run mode, either pr or check.
|
|
6
|
+
required: false
|
|
7
|
+
default: pr
|
|
8
|
+
path:
|
|
9
|
+
description: File or directory for mode=check.
|
|
10
|
+
required: false
|
|
11
|
+
default: .
|
|
12
|
+
pr-body-file:
|
|
13
|
+
description: Optional PR body file for mode=pr. Defaults to the GitHub event pull request body.
|
|
14
|
+
required: false
|
|
15
|
+
default: ""
|
|
16
|
+
version:
|
|
17
|
+
description: npm version to install.
|
|
18
|
+
required: false
|
|
19
|
+
default: latest
|
|
20
|
+
network:
|
|
21
|
+
description: Allow network-backed runtime verifiers.
|
|
22
|
+
required: false
|
|
23
|
+
default: "false"
|
|
24
|
+
allow-exec:
|
|
25
|
+
description: Allow command/query verifiers.
|
|
26
|
+
required: false
|
|
27
|
+
default: "false"
|
|
28
|
+
runs:
|
|
29
|
+
using: composite
|
|
30
|
+
steps:
|
|
31
|
+
- name: Install proof-gate
|
|
32
|
+
shell: bash
|
|
33
|
+
run: npm install -g "proof-gate@${{ inputs.version }}"
|
|
34
|
+
- name: Run proof-gate
|
|
35
|
+
shell: bash
|
|
36
|
+
env:
|
|
37
|
+
INPUT_MODE: ${{ inputs.mode }}
|
|
38
|
+
INPUT_PATH: ${{ inputs.path }}
|
|
39
|
+
INPUT_PR_BODY_FILE: ${{ inputs.pr-body-file }}
|
|
40
|
+
INPUT_NETWORK: ${{ inputs.network }}
|
|
41
|
+
INPUT_ALLOW_EXEC: ${{ inputs.allow-exec }}
|
|
42
|
+
run: |
|
|
43
|
+
set -euo pipefail
|
|
44
|
+
args=()
|
|
45
|
+
if [ "${INPUT_NETWORK}" = "true" ]; then args+=(--network); fi
|
|
46
|
+
if [ "${INPUT_ALLOW_EXEC}" = "true" ]; then args+=(--allow-exec); fi
|
|
47
|
+
if [ "${INPUT_MODE}" = "check" ]; then
|
|
48
|
+
proof-gate check "${INPUT_PATH}" "${args[@]}"
|
|
49
|
+
exit $?
|
|
50
|
+
fi
|
|
51
|
+
body_file="${INPUT_PR_BODY_FILE}"
|
|
52
|
+
if [ -z "${body_file}" ]; then
|
|
53
|
+
body_file="${RUNNER_TEMP}/proof-gate-pr-body.md"
|
|
54
|
+
node -e "const fs=require('fs'); const event=JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH,'utf8')); fs.writeFileSync(process.argv[1], event.pull_request?.body || '')" "${body_file}"
|
|
55
|
+
fi
|
|
56
|
+
proof-gate check-pr "${body_file}" "${args[@]}"
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { discoverProofFiles, parsePrBodyFile, readProofFile } from "./parse.js";
|
|
4
|
+
import { checkProofBlock } from "./verify.js";
|
|
5
|
+
function usage() {
|
|
6
|
+
return `proof-gate
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
proof-gate check <file-or-directory> [--network] [--allow-exec]
|
|
10
|
+
proof-gate check-pr <pr-body-file> [--network] [--allow-exec]
|
|
11
|
+
|
|
12
|
+
Examples:
|
|
13
|
+
proof-gate check examples/toy-service/tasks
|
|
14
|
+
proof-gate check-pr examples/toy-service/pr-body.md
|
|
15
|
+
`;
|
|
16
|
+
}
|
|
17
|
+
function parseFlags(args) {
|
|
18
|
+
const positional = [];
|
|
19
|
+
const options = { network: false, allowExec: false };
|
|
20
|
+
for (const arg of args) {
|
|
21
|
+
if (arg === "--network")
|
|
22
|
+
options.network = true;
|
|
23
|
+
else if (arg === "--allow-exec")
|
|
24
|
+
options.allowExec = true;
|
|
25
|
+
else
|
|
26
|
+
positional.push(arg);
|
|
27
|
+
}
|
|
28
|
+
return { positional, options };
|
|
29
|
+
}
|
|
30
|
+
async function runCheck(target, options) {
|
|
31
|
+
const files = discoverProofFiles(target);
|
|
32
|
+
let failed = 0;
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
const block = readProofFile(file);
|
|
35
|
+
const result = await checkProofBlock(block, { filePath: file, ...options });
|
|
36
|
+
if (result.ok) {
|
|
37
|
+
console.log(`PASS ${result.id} [${result.className}/${result.depth}]: ${result.message}`);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
failed += 1;
|
|
41
|
+
console.error(`FAIL ${result.id}: ${result.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return failed === 0 ? 0 : 1;
|
|
45
|
+
}
|
|
46
|
+
async function runCheckPr(file, options) {
|
|
47
|
+
const block = parsePrBodyFile(file);
|
|
48
|
+
const result = await checkProofBlock(block, { filePath: path.resolve(file), ...options });
|
|
49
|
+
if (result.ok) {
|
|
50
|
+
console.log(`PASS ${result.id} [${result.className}/${result.depth}]: ${result.message}`);
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
console.error(`FAIL ${result.id}: ${result.message}`);
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
57
|
+
const command = argv[0];
|
|
58
|
+
const { positional, options } = parseFlags(argv.slice(1));
|
|
59
|
+
try {
|
|
60
|
+
if (!command || command === "--help" || command === "-h") {
|
|
61
|
+
console.log(usage());
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
if (command === "check") {
|
|
65
|
+
if (!positional[0])
|
|
66
|
+
throw new Error("check requires a file or directory");
|
|
67
|
+
return await runCheck(positional[0], options);
|
|
68
|
+
}
|
|
69
|
+
if (command === "check-pr") {
|
|
70
|
+
if (!positional[0])
|
|
71
|
+
throw new Error("check-pr requires a PR body file");
|
|
72
|
+
return await runCheckPr(positional[0], options);
|
|
73
|
+
}
|
|
74
|
+
console.error(usage());
|
|
75
|
+
return 2;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.error(`proof-gate: ${error instanceof Error ? error.message : String(error)}`);
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
main().then((code) => {
|
|
83
|
+
process.exitCode = code;
|
|
84
|
+
});
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const PROOF_CLASSES: readonly ["local", "ci", "deploy", "runtime", "db", "visual", "no-go"];
|
|
2
|
+
export declare const PROOF_STATES: readonly ["none", "local_only", "pr_open", "merged", "deployed", "runtime_proven", "closed_no_go"];
|
|
3
|
+
export declare const HONESTY_DEPTHS: readonly ["existence", "shape", "inspected", "reran"];
|
|
4
|
+
export type ProofClass = (typeof PROOF_CLASSES)[number];
|
|
5
|
+
export type ProofState = (typeof PROOF_STATES)[number];
|
|
6
|
+
export type HonestyDepth = (typeof HONESTY_DEPTHS)[number];
|
|
7
|
+
export declare function normalizeProofState(value: unknown): ProofState | undefined;
|
|
8
|
+
export declare function normalizeProofClass(value: unknown): ProofClass | undefined;
|
|
9
|
+
export declare function expectedClassesForState(state: ProofState): ProofClass[];
|
|
10
|
+
export declare function depthRank(depth: HonestyDepth): number;
|
|
11
|
+
export declare function maxDepth(depths: HonestyDepth[]): HonestyDepth;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const PROOF_CLASSES = ["local", "ci", "deploy", "runtime", "db", "visual", "no-go"];
|
|
2
|
+
export const PROOF_STATES = [
|
|
3
|
+
"none",
|
|
4
|
+
"local_only",
|
|
5
|
+
"pr_open",
|
|
6
|
+
"merged",
|
|
7
|
+
"deployed",
|
|
8
|
+
"runtime_proven",
|
|
9
|
+
"closed_no_go",
|
|
10
|
+
];
|
|
11
|
+
export const HONESTY_DEPTHS = ["existence", "shape", "inspected", "reran"];
|
|
12
|
+
const STATE_TO_CLASSES = {
|
|
13
|
+
local_only: ["local"],
|
|
14
|
+
pr_open: ["local", "ci"],
|
|
15
|
+
merged: ["ci"],
|
|
16
|
+
deployed: ["deploy"],
|
|
17
|
+
runtime_proven: ["runtime", "db", "visual"],
|
|
18
|
+
closed_no_go: ["no-go"],
|
|
19
|
+
};
|
|
20
|
+
export function normalizeProofState(value) {
|
|
21
|
+
if (typeof value !== "string")
|
|
22
|
+
return undefined;
|
|
23
|
+
const normalized = value.trim().toLowerCase().replaceAll("-", "_");
|
|
24
|
+
return PROOF_STATES.includes(normalized) ? normalized : undefined;
|
|
25
|
+
}
|
|
26
|
+
export function normalizeProofClass(value) {
|
|
27
|
+
if (typeof value !== "string")
|
|
28
|
+
return undefined;
|
|
29
|
+
const normalized = value.trim().toLowerCase().replaceAll("_", "-");
|
|
30
|
+
return PROOF_CLASSES.includes(normalized) ? normalized : undefined;
|
|
31
|
+
}
|
|
32
|
+
export function expectedClassesForState(state) {
|
|
33
|
+
if (state === "none")
|
|
34
|
+
return [];
|
|
35
|
+
return STATE_TO_CLASSES[state];
|
|
36
|
+
}
|
|
37
|
+
export function depthRank(depth) {
|
|
38
|
+
return HONESTY_DEPTHS.indexOf(depth);
|
|
39
|
+
}
|
|
40
|
+
export function maxDepth(depths) {
|
|
41
|
+
return depths.reduce((best, next) => (depthRank(next) > depthRank(best) ? next : best), "existence");
|
|
42
|
+
}
|
package/dist/parse.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ProofBlock } from "./types.js";
|
|
2
|
+
export declare function loadStructuredText(raw: string, source: string): ProofBlock;
|
|
3
|
+
export declare function readProofFile(filePath: string): ProofBlock;
|
|
4
|
+
export declare function discoverProofFiles(target: string): string[];
|
|
5
|
+
export declare function parsePrBodyFile(filePath: string): ProofBlock;
|
package/dist/parse.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
const PROOF_EXTENSIONS = new Set([".json", ".yaml", ".yml"]);
|
|
5
|
+
export function loadStructuredText(raw, source) {
|
|
6
|
+
const ext = path.extname(source).toLowerCase();
|
|
7
|
+
const parsed = ext === ".json" ? JSON.parse(raw) : yaml.load(raw);
|
|
8
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
9
|
+
throw new Error(`${source} did not parse to an object`);
|
|
10
|
+
}
|
|
11
|
+
return parsed;
|
|
12
|
+
}
|
|
13
|
+
export function readProofFile(filePath) {
|
|
14
|
+
return loadStructuredText(fs.readFileSync(filePath, "utf8"), filePath);
|
|
15
|
+
}
|
|
16
|
+
export function discoverProofFiles(target) {
|
|
17
|
+
const stat = fs.statSync(target);
|
|
18
|
+
if (stat.isFile())
|
|
19
|
+
return [target];
|
|
20
|
+
if (!stat.isDirectory())
|
|
21
|
+
throw new Error(`${target} is neither a file nor a directory`);
|
|
22
|
+
return fs
|
|
23
|
+
.readdirSync(target)
|
|
24
|
+
.filter((entry) => PROOF_EXTENSIONS.has(path.extname(entry).toLowerCase()))
|
|
25
|
+
.map((entry) => path.join(target, entry))
|
|
26
|
+
.sort();
|
|
27
|
+
}
|
|
28
|
+
export function parsePrBodyFile(filePath) {
|
|
29
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
30
|
+
const match = raw.match(/Proof:\s*\n```(?:yaml|yml|json)?\s*\n([\s\S]*?)\n```/i);
|
|
31
|
+
if (!match) {
|
|
32
|
+
throw new Error(`${filePath} does not contain a fenced Proof: block`);
|
|
33
|
+
}
|
|
34
|
+
return loadStructuredText(match[1] ?? "", filePath.endsWith(".json") ? filePath : `${filePath}.yaml`);
|
|
35
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { HonestyDepth, ProofClass, ProofState } from "./config.js";
|
|
2
|
+
export interface EvidencePointer {
|
|
3
|
+
type?: string;
|
|
4
|
+
value?: string;
|
|
5
|
+
path?: string;
|
|
6
|
+
url?: string;
|
|
7
|
+
command?: string;
|
|
8
|
+
query?: string;
|
|
9
|
+
expected_sha?: string;
|
|
10
|
+
expected_status?: number;
|
|
11
|
+
expected_count?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ProofDetails {
|
|
14
|
+
class?: string;
|
|
15
|
+
reason?: string;
|
|
16
|
+
evidence?: EvidencePointer[] | EvidencePointer;
|
|
17
|
+
}
|
|
18
|
+
export interface ProofBlock {
|
|
19
|
+
id?: string;
|
|
20
|
+
status?: string;
|
|
21
|
+
proof_state?: string;
|
|
22
|
+
expected_sha?: string;
|
|
23
|
+
expected_status?: number;
|
|
24
|
+
expected_count?: number;
|
|
25
|
+
reason?: string;
|
|
26
|
+
proof?: ProofDetails;
|
|
27
|
+
}
|
|
28
|
+
export interface CheckContext {
|
|
29
|
+
filePath?: string;
|
|
30
|
+
baseDir: string;
|
|
31
|
+
network: boolean;
|
|
32
|
+
allowExec: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface VerifierPass {
|
|
35
|
+
ok: true;
|
|
36
|
+
className: ProofClass;
|
|
37
|
+
depth: HonestyDepth;
|
|
38
|
+
message: string;
|
|
39
|
+
}
|
|
40
|
+
export interface VerifierFail {
|
|
41
|
+
ok: false;
|
|
42
|
+
className?: ProofClass;
|
|
43
|
+
message: string;
|
|
44
|
+
}
|
|
45
|
+
export type VerifierResult = VerifierPass | VerifierFail;
|
|
46
|
+
export interface ProofCheckPass {
|
|
47
|
+
ok: true;
|
|
48
|
+
id: string;
|
|
49
|
+
proofState: ProofState;
|
|
50
|
+
className: ProofClass;
|
|
51
|
+
depth: HonestyDepth;
|
|
52
|
+
message: string;
|
|
53
|
+
}
|
|
54
|
+
export interface ProofCheckFail {
|
|
55
|
+
ok: false;
|
|
56
|
+
id: string;
|
|
57
|
+
message: string;
|
|
58
|
+
}
|
|
59
|
+
export type ProofCheckResult = ProofCheckPass | ProofCheckFail;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/verify.d.ts
ADDED
package/dist/verify.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { expectedClassesForState, maxDepth, normalizeProofClass, normalizeProofState } from "./config.js";
|
|
5
|
+
function idFor(block) {
|
|
6
|
+
return block.id?.trim() || "unknown";
|
|
7
|
+
}
|
|
8
|
+
function evidenceList(block) {
|
|
9
|
+
const evidence = block.proof?.evidence;
|
|
10
|
+
if (!evidence)
|
|
11
|
+
return [];
|
|
12
|
+
return Array.isArray(evidence) ? evidence : [evidence];
|
|
13
|
+
}
|
|
14
|
+
function evidenceValue(evidence) {
|
|
15
|
+
return evidence.value || evidence.path || evidence.url || evidence.command || evidence.query || "";
|
|
16
|
+
}
|
|
17
|
+
function resolveEvidencePath(raw, baseDir) {
|
|
18
|
+
return path.isAbsolute(raw) ? raw : path.resolve(baseDir, raw);
|
|
19
|
+
}
|
|
20
|
+
function pass(className, depth, message) {
|
|
21
|
+
return { ok: true, className, depth, message };
|
|
22
|
+
}
|
|
23
|
+
function fail(className, message) {
|
|
24
|
+
return { ok: false, className, message };
|
|
25
|
+
}
|
|
26
|
+
function readJsonFile(filePath) {
|
|
27
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
28
|
+
}
|
|
29
|
+
function firstObject(value) {
|
|
30
|
+
if (value && typeof value === "object" && !Array.isArray(value))
|
|
31
|
+
return value;
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
function numeric(value) {
|
|
35
|
+
if (typeof value === "number")
|
|
36
|
+
return value;
|
|
37
|
+
if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value)))
|
|
38
|
+
return Number(value);
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
function verifyLocal(evidence, block, baseDir, allowExec) {
|
|
42
|
+
const value = evidenceValue(evidence);
|
|
43
|
+
if (!value)
|
|
44
|
+
return fail("local", "local evidence needs a file path or command");
|
|
45
|
+
if (evidence.type === "command" || evidence.command) {
|
|
46
|
+
if (!allowExec)
|
|
47
|
+
return fail("local", "local command evidence requires --allow-exec");
|
|
48
|
+
execSync(value, { stdio: "pipe", cwd: baseDir });
|
|
49
|
+
return pass("local", "reran", `command exited 0: ${value}`);
|
|
50
|
+
}
|
|
51
|
+
const filePath = resolveEvidencePath(value, baseDir);
|
|
52
|
+
if (!fs.existsSync(filePath))
|
|
53
|
+
return fail("local", `file does not exist: ${value}`);
|
|
54
|
+
return pass("local", "existence", `file exists: ${value}`);
|
|
55
|
+
}
|
|
56
|
+
function verifyCi(evidence) {
|
|
57
|
+
const value = evidenceValue(evidence);
|
|
58
|
+
if (!/^https:\/\/github\.com\/[^/]+\/[^/]+\/(actions\/runs\/\d+|pull\/\d+|commit\/[0-9a-f]{7,40}|checks)/i.test(value)) {
|
|
59
|
+
return fail("ci", "ci evidence must look like a GitHub PR, commit, check, or Actions run URL");
|
|
60
|
+
}
|
|
61
|
+
return pass("ci", "shape", `GitHub CI/PR URL shape accepted: ${value}`);
|
|
62
|
+
}
|
|
63
|
+
function verifyDeploy(evidence, block, baseDir) {
|
|
64
|
+
const value = evidenceValue(evidence);
|
|
65
|
+
if (!value)
|
|
66
|
+
return fail("deploy", "deploy evidence needs a release.json path");
|
|
67
|
+
const expectedSha = evidence.expected_sha || block.expected_sha;
|
|
68
|
+
if (!expectedSha)
|
|
69
|
+
return fail("deploy", "deploy evidence needs expected_sha");
|
|
70
|
+
const filePath = resolveEvidencePath(value, baseDir);
|
|
71
|
+
if (!fs.existsSync(filePath))
|
|
72
|
+
return fail("deploy", `release marker does not exist: ${value}`);
|
|
73
|
+
const marker = firstObject(readJsonFile(filePath));
|
|
74
|
+
const actualSha = String(marker?.sha || marker?.commit || marker?.release_sha || "");
|
|
75
|
+
if (!actualSha)
|
|
76
|
+
return fail("deploy", `release marker has no sha-like field: ${value}`);
|
|
77
|
+
if (actualSha !== expectedSha)
|
|
78
|
+
return fail("deploy", `release marker sha mismatch: expected ${expectedSha}, got ${actualSha}`);
|
|
79
|
+
return pass("deploy", "inspected", `release marker sha matches ${expectedSha}`);
|
|
80
|
+
}
|
|
81
|
+
async function verifyRuntime(evidence, block, baseDir, network) {
|
|
82
|
+
const value = evidenceValue(evidence);
|
|
83
|
+
if (!value)
|
|
84
|
+
return fail("runtime", "runtime evidence needs a URL or receipt file");
|
|
85
|
+
const expectedStatus = evidence.expected_status || block.expected_status || 200;
|
|
86
|
+
if (/^https?:\/\//i.test(value)) {
|
|
87
|
+
if (!network)
|
|
88
|
+
return fail("runtime", "runtime URL evidence requires --network or a saved receipt");
|
|
89
|
+
const response = await fetch(value);
|
|
90
|
+
if (response.status !== expectedStatus)
|
|
91
|
+
return fail("runtime", `runtime GET expected ${expectedStatus}, got ${response.status}`);
|
|
92
|
+
return pass("runtime", "reran", `GET ${value} returned ${response.status}`);
|
|
93
|
+
}
|
|
94
|
+
const filePath = resolveEvidencePath(value, baseDir);
|
|
95
|
+
if (!fs.existsSync(filePath))
|
|
96
|
+
return fail("runtime", `runtime receipt does not exist: ${value}`);
|
|
97
|
+
const receipt = firstObject(readJsonFile(filePath));
|
|
98
|
+
const status = numeric(receipt?.status_code ?? receipt?.status ?? receipt?.http_status);
|
|
99
|
+
if (status === undefined)
|
|
100
|
+
return fail("runtime", `runtime receipt has no status field: ${value}`);
|
|
101
|
+
if (status !== expectedStatus)
|
|
102
|
+
return fail("runtime", `runtime receipt expected ${expectedStatus}, got ${status}`);
|
|
103
|
+
return pass("runtime", "inspected", `runtime receipt status ${status}`);
|
|
104
|
+
}
|
|
105
|
+
function verifyDb(evidence, block, baseDir, allowExec) {
|
|
106
|
+
const value = evidenceValue(evidence);
|
|
107
|
+
if (!value)
|
|
108
|
+
return fail("db", "db evidence needs a receipt file or command");
|
|
109
|
+
if (evidence.type === "command" || evidence.command || evidence.query) {
|
|
110
|
+
if (!allowExec)
|
|
111
|
+
return fail("db", "db command/query evidence requires --allow-exec");
|
|
112
|
+
execSync(value, { stdio: "pipe", cwd: baseDir });
|
|
113
|
+
return pass("db", "reran", "db command/query exited 0");
|
|
114
|
+
}
|
|
115
|
+
const filePath = resolveEvidencePath(value, baseDir);
|
|
116
|
+
if (!fs.existsSync(filePath))
|
|
117
|
+
return fail("db", `db receipt does not exist: ${value}`);
|
|
118
|
+
const receipt = firstObject(readJsonFile(filePath));
|
|
119
|
+
const actualCount = numeric(receipt?.count ?? receipt?.row_count ?? receipt?.rows);
|
|
120
|
+
const expectedCount = evidence.expected_count ?? block.expected_count;
|
|
121
|
+
if (expectedCount !== undefined && actualCount !== expectedCount) {
|
|
122
|
+
return fail("db", `db receipt expected count ${expectedCount}, got ${actualCount ?? "missing"}`);
|
|
123
|
+
}
|
|
124
|
+
return pass("db", "inspected", expectedCount === undefined ? "db receipt inspected" : `db count ${actualCount}`);
|
|
125
|
+
}
|
|
126
|
+
function verifyVisual(evidence, block, baseDir) {
|
|
127
|
+
const value = evidenceValue(evidence);
|
|
128
|
+
if (!value)
|
|
129
|
+
return fail("visual", "visual evidence needs a screenshot path");
|
|
130
|
+
const filePath = resolveEvidencePath(value, baseDir);
|
|
131
|
+
if (!fs.existsSync(filePath))
|
|
132
|
+
return fail("visual", `screenshot does not exist: ${value}`);
|
|
133
|
+
const bytes = fs.readFileSync(filePath);
|
|
134
|
+
const isPng = bytes.length >= 8 && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47;
|
|
135
|
+
const isJpeg = bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff;
|
|
136
|
+
const isGif = bytes.length >= 6 && bytes.subarray(0, 3).toString("ascii") === "GIF";
|
|
137
|
+
if (!isPng && !isJpeg && !isGif)
|
|
138
|
+
return fail("visual", `screenshot is not PNG/JPEG/GIF by magic bytes: ${value}`);
|
|
139
|
+
return pass("visual", "inspected", `image magic bytes verified: ${value}`);
|
|
140
|
+
}
|
|
141
|
+
function verifyNoGo(evidence, block, baseDir) {
|
|
142
|
+
const reason = block.reason || block.proof?.reason;
|
|
143
|
+
if (!reason?.trim())
|
|
144
|
+
return fail("no-go", "closed_no_go requires a reason");
|
|
145
|
+
const value = evidenceValue(evidence);
|
|
146
|
+
if (!value)
|
|
147
|
+
return fail("no-go", "no-go evidence needs a pointer");
|
|
148
|
+
if (/^https?:\/\//i.test(value))
|
|
149
|
+
return pass("no-go", "shape", `no-go URL shape accepted: ${value}`);
|
|
150
|
+
const filePath = resolveEvidencePath(value, baseDir);
|
|
151
|
+
if (!fs.existsSync(filePath))
|
|
152
|
+
return fail("no-go", `no-go evidence file does not exist: ${value}`);
|
|
153
|
+
if (fs.statSync(filePath).size === 0)
|
|
154
|
+
return fail("no-go", `no-go evidence file is empty: ${value}`);
|
|
155
|
+
return pass("no-go", "inspected", `no-go evidence file inspected: ${value}`);
|
|
156
|
+
}
|
|
157
|
+
async function verifyEvidence(className, evidence, block, baseDir, network, allowExec) {
|
|
158
|
+
switch (className) {
|
|
159
|
+
case "local":
|
|
160
|
+
return verifyLocal(evidence, block, baseDir, allowExec);
|
|
161
|
+
case "ci":
|
|
162
|
+
return verifyCi(evidence);
|
|
163
|
+
case "deploy":
|
|
164
|
+
return verifyDeploy(evidence, block, baseDir);
|
|
165
|
+
case "runtime":
|
|
166
|
+
return verifyRuntime(evidence, block, baseDir, network);
|
|
167
|
+
case "db":
|
|
168
|
+
return verifyDb(evidence, block, baseDir, allowExec);
|
|
169
|
+
case "visual":
|
|
170
|
+
return verifyVisual(evidence, block, baseDir);
|
|
171
|
+
case "no-go":
|
|
172
|
+
return verifyNoGo(evidence, block, baseDir);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export async function checkProofBlock(block, options = {}) {
|
|
176
|
+
const id = idFor(block);
|
|
177
|
+
const status = block.status?.trim().toLowerCase();
|
|
178
|
+
const rawState = block.proof_state;
|
|
179
|
+
const proofState = normalizeProofState(rawState);
|
|
180
|
+
if (status === "done" && !proofState) {
|
|
181
|
+
return { ok: false, id, message: `status=done requires a proof_state from local_only, pr_open, merged, deployed, runtime_proven, closed_no_go; got ${rawState ? String(rawState) : "missing"}` };
|
|
182
|
+
}
|
|
183
|
+
if (status === "done" && proofState === "none") {
|
|
184
|
+
return { ok: false, id, message: "status=done requires proof_state to be non-none" };
|
|
185
|
+
}
|
|
186
|
+
if (!proofState || proofState === "none") {
|
|
187
|
+
return { ok: false, id, message: "proof_state is required" };
|
|
188
|
+
}
|
|
189
|
+
const className = normalizeProofClass(block.proof?.class);
|
|
190
|
+
if (!className)
|
|
191
|
+
return { ok: false, id, message: `proof.class is required and must be one of: local, ci, deploy, runtime, db, visual, no-go` };
|
|
192
|
+
const expected = expectedClassesForState(proofState);
|
|
193
|
+
if (!expected.includes(className)) {
|
|
194
|
+
return { ok: false, id, message: `proof_state=${proofState} cannot be justified by class=${className}; expected one of ${expected.join(", ")}` };
|
|
195
|
+
}
|
|
196
|
+
if (proofState === "closed_no_go" && !(block.reason || block.proof?.reason)?.trim()) {
|
|
197
|
+
return { ok: false, id, message: "proof_state=closed_no_go requires a reason" };
|
|
198
|
+
}
|
|
199
|
+
const evidence = evidenceList(block);
|
|
200
|
+
if (evidence.length === 0)
|
|
201
|
+
return { ok: false, id, message: "proof.evidence requires at least one pointer" };
|
|
202
|
+
const baseDir = options.filePath ? path.dirname(options.filePath) : process.cwd();
|
|
203
|
+
const results = [];
|
|
204
|
+
for (const pointer of evidence) {
|
|
205
|
+
results.push(await verifyEvidence(className, pointer, block, baseDir, Boolean(options.network), Boolean(options.allowExec)));
|
|
206
|
+
}
|
|
207
|
+
const failed = results.find((result) => !result.ok);
|
|
208
|
+
if (failed && !failed.ok)
|
|
209
|
+
return { ok: false, id, message: failed.message };
|
|
210
|
+
const passes = results.filter((result) => result.ok);
|
|
211
|
+
const depth = maxDepth(passes.map((result) => result.depth));
|
|
212
|
+
return { ok: true, id, proofState, className, depth, message: passes.map((result) => result.message).join("; ") };
|
|
213
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Done Means Proven
|
|
2
|
+
|
|
3
|
+
Software teams have overloaded one word until it stopped meaning anything.
|
|
4
|
+
|
|
5
|
+
Done can mean a patch exists. It can mean tests passed. It can mean a PR merged. It can mean the deploy job ran. It can mean the live system behaved correctly. In agent-heavy workflows, the collapse gets worse because agents are very good at producing confident summaries from partial evidence.
|
|
6
|
+
|
|
7
|
+
The failure is not only capability. It is epistemology.
|
|
8
|
+
|
|
9
|
+
If another actor cannot independently inspect or rerun the evidence, the claim is not runtime truth. It is narrative truth. Narrative truth can be useful for routing, but it should not close work.
|
|
10
|
+
|
|
11
|
+
## The missing vocabulary
|
|
12
|
+
|
|
13
|
+
A done claim should name what kind of proof backs it:
|
|
14
|
+
|
|
15
|
+
- `local`: the local worktree or developer runtime proved it
|
|
16
|
+
- `ci`: automated checks proved the branch or PR path
|
|
17
|
+
- `deploy`: the deploy marker or release path moved to the expected version
|
|
18
|
+
- `runtime`: the running system was exercised
|
|
19
|
+
- `db`: authoritative stored state was inspected
|
|
20
|
+
- `visual`: a rendered screen or screenshot proved the visual claim
|
|
21
|
+
- `no-go`: the correct outcome was to stop, roll back, or decide not to ship
|
|
22
|
+
|
|
23
|
+
The last class matters. Most definition-of-done systems have no vocabulary for a correct no. They reward motion and leave stopped work looking incomplete. But sometimes the highest-quality outcome is proving that a lane should not ship.
|
|
24
|
+
|
|
25
|
+
## The rule
|
|
26
|
+
|
|
27
|
+
`done` requires a proof state, a proof class, and evidence that can be inspected or rerun.
|
|
28
|
+
|
|
29
|
+
Not merely a sentence. Not merely a link. Not merely a JSON file that an agent could have fabricated.
|
|
30
|
+
|
|
31
|
+
The evidence has to match the claim. A local file can justify `local_only`. It cannot justify `runtime_proven`. A release marker can justify `deployed`. It cannot prove the live API behavior unless the runtime was exercised too.
|
|
32
|
+
|
|
33
|
+
This is the invariant behind `proof-gate`:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
status=done requires proof_state != none
|
|
37
|
+
proof_state=runtime_proven requires class in runtime, db, visual
|
|
38
|
+
class chooses the verifier
|
|
39
|
+
verifier prints honesty depth
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The depth label is the honesty contract:
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
existence < shape < inspected < reran
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
A gate that only checks for a pointer should say it checked existence. A gate that curls a healthcheck should say it reran runtime evidence. The difference is the whole point.
|
|
49
|
+
|
|
50
|
+
## What a gate can and cannot do
|
|
51
|
+
|
|
52
|
+
A small CI gate cannot prove every truth about production. It can make false confidence harder.
|
|
53
|
+
|
|
54
|
+
`proof-gate` v0.1 does not claim omniscience. It makes proof claims inspectable, class-aware, and verifier-backed with explicit honesty depth. Some verifiers are intentionally shallow in v0.1. CI URLs are shape-checked. Local files are existence-checked. Deploy markers, runtime receipts, DB receipts, and visual screenshots are inspected. Runtime URLs and command/query checks can be rerun when explicitly allowed.
|
|
55
|
+
|
|
56
|
+
That scope is not a caveat buried at the bottom. It is the architecture. The class to verifier mapping is first-class so deeper adapters can replace shallow checks without changing the contract.
|
|
57
|
+
|
|
58
|
+
## The output should tell the truth
|
|
59
|
+
|
|
60
|
+
A legitimate deploy proof should look like this:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
PASS DEPLOY-1 [deploy/inspected]: release marker sha matches abc123
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
A legitimate runtime receipt should look like this:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
PASS RUNTIME-1 [runtime/inspected]: runtime receipt status 200
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
And a local file pretending to be runtime proof should fail before any verifier runs:
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
FAIL LIE-2: proof_state=runtime_proven cannot be justified by class=local; expected one of runtime, db, visual
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
That is the difference between pointer discipline and proof discipline.
|
|
79
|
+
|
|
80
|
+
## Why this matters more with agents
|
|
81
|
+
|
|
82
|
+
Humans also blur proof states, but agents amplify the problem. They can summarize a desired outcome in polished language, cite files that exist, and move work across tools faster than teams can verify the underlying claim.
|
|
83
|
+
|
|
84
|
+
The answer is not to distrust agents. The answer is to give them contracts that make truth inspectable.
|
|
85
|
+
|
|
86
|
+
A good agent should be able to say:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
local-only: tests pass here
|
|
90
|
+
PR-open: reviewable branch exists
|
|
91
|
+
merged: main contains the change
|
|
92
|
+
deployed: release marker matches the SHA
|
|
93
|
+
runtime-proven: live behavior was exercised
|
|
94
|
+
closed-no-go: evidence says stop
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Those are different claims. They deserve different proof.
|
|
98
|
+
|
|
99
|
+
## The launch target
|
|
100
|
+
|
|
101
|
+
Attention is not adoption. Stars, comments, and traffic are useful, but they are not runtime proof.
|
|
102
|
+
|
|
103
|
+
The real launch proof for this project is one repo I do not control committing a `proof-gate.yml` workflow or equivalent CI integration. That is the moment the idea leaves narrative truth and becomes runtime truth.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
The proposed change was intentionally stopped because the deploy proof did not match the requested SHA.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
GIF89a toy visual proof
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
unit tests passed in the toy service
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Example PR
|
|
2
|
+
|
|
3
|
+
This PR updates the toy service.
|
|
4
|
+
|
|
5
|
+
Proof:
|
|
6
|
+
```yaml
|
|
7
|
+
id: PR-BODY-1
|
|
8
|
+
status: done
|
|
9
|
+
proof_state: deployed
|
|
10
|
+
expected_sha: abc123
|
|
11
|
+
proof:
|
|
12
|
+
class: deploy
|
|
13
|
+
evidence:
|
|
14
|
+
- type: release_json
|
|
15
|
+
value: release.json
|
|
16
|
+
expected_sha: abc123
|
|
17
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@relaxedg/proof-gate",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A tiny CI gate that makes done require class-aware, inspectable evidence.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"proof-gate": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"action.yml",
|
|
14
|
+
"docs",
|
|
15
|
+
"examples"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"check": "node dist/cli.js check examples/toy-service/tasks && node dist/cli.js check-pr examples/toy-service/pr-body.md",
|
|
21
|
+
"prepublishOnly": "npm run build && npm test && npm run check"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"agentops",
|
|
25
|
+
"ci",
|
|
26
|
+
"github-actions",
|
|
27
|
+
"proof",
|
|
28
|
+
"runtime-truth",
|
|
29
|
+
"ai-agents"
|
|
30
|
+
],
|
|
31
|
+
"author": "Greg Bond",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/gregbond/proof-gate.git"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/gregbond/proof-gate/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/gregbond/proof-gate#readme",
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"js-yaml": "^4.1.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/js-yaml": "^4.0.9",
|
|
49
|
+
"@types/node": "^22.10.2",
|
|
50
|
+
"typescript": "^5.7.2",
|
|
51
|
+
"vitest": "^2.1.8"
|
|
52
|
+
}
|
|
53
|
+
}
|