@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 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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function main(argv?: string[]): Promise<number>;
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
+ });
@@ -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
+ }
@@ -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
+ }
@@ -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 {};
@@ -0,0 +1,6 @@
1
+ import type { ProofBlock, ProofCheckResult } from "./types.js";
2
+ export declare function checkProofBlock(block: ProofBlock, options?: {
3
+ filePath?: string;
4
+ network?: boolean;
5
+ allowExec?: boolean;
6
+ }): Promise<ProofCheckResult>;
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,4 @@
1
+ {
2
+ "count": 3,
3
+ "query": "select count(*) from toy_events"
4
+ }
@@ -0,0 +1 @@
1
+ The proposed change was intentionally stopped because the deploy proof did not match the requested SHA.
@@ -0,0 +1,5 @@
1
+ {
2
+ "status": 200,
3
+ "body": "ok",
4
+ "checked_at": "2026-07-07T00:00:00Z"
5
+ }
@@ -0,0 +1 @@
1
+ GIF89a toy visual proof
@@ -0,0 +1 @@
1
+ unit tests passed in the toy service
@@ -0,0 +1,7 @@
1
+ id: LIE-1
2
+ status: done
3
+ proof:
4
+ class: local
5
+ evidence:
6
+ - type: file
7
+ value: ../evidence/unit-test.txt
@@ -0,0 +1,8 @@
1
+ id: LIE-2
2
+ status: done
3
+ proof_state: runtime_proven
4
+ proof:
5
+ class: local
6
+ evidence:
7
+ - type: file
8
+ value: ../evidence/unit-test.txt
@@ -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
+ ```
@@ -0,0 +1,4 @@
1
+ {
2
+ "sha": "abc123",
3
+ "service": "toy-service"
4
+ }
@@ -0,0 +1,8 @@
1
+ id: LOCAL-1
2
+ status: done
3
+ proof_state: local_only
4
+ proof:
5
+ class: local
6
+ evidence:
7
+ - type: file
8
+ value: ../evidence/unit-test.txt
@@ -0,0 +1,8 @@
1
+ id: PR-1
2
+ status: done
3
+ proof_state: pr_open
4
+ proof:
5
+ class: ci
6
+ evidence:
7
+ - type: url
8
+ value: https://github.com/gregbond/proof-gate/actions/runs/123456789
@@ -0,0 +1,8 @@
1
+ id: MERGED-1
2
+ status: done
3
+ proof_state: merged
4
+ proof:
5
+ class: ci
6
+ evidence:
7
+ - type: url
8
+ value: https://github.com/gregbond/proof-gate/commit/abcdef1
@@ -0,0 +1,10 @@
1
+ id: DEPLOY-1
2
+ status: done
3
+ proof_state: deployed
4
+ expected_sha: abc123
5
+ proof:
6
+ class: deploy
7
+ evidence:
8
+ - type: release_json
9
+ value: ../release.json
10
+ expected_sha: abc123
@@ -0,0 +1,9 @@
1
+ id: RUNTIME-1
2
+ status: done
3
+ proof_state: runtime_proven
4
+ proof:
5
+ class: runtime
6
+ evidence:
7
+ - type: receipt
8
+ value: ../evidence/runtime-receipt.json
9
+ expected_status: 200
@@ -0,0 +1,9 @@
1
+ id: NO-GO-1
2
+ status: done
3
+ proof_state: closed_no_go
4
+ reason: The correct decision was not to ship this change.
5
+ proof:
6
+ class: no-go
7
+ evidence:
8
+ - type: file
9
+ value: ../evidence/no-go.md
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
+ }