@pietro-falco/verity 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 Pietro Falco
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,112 @@
1
+ # verity
2
+
3
+ [![CI](https://github.com/pietro-falco/verity/actions/workflows/ci.yml/badge.svg)](https://github.com/pietro-falco/verity/actions/workflows/ci.yml)
4
+
5
+ Turn your coding agent's "done" into a receipt you can check — deterministic, local, zero-dependency.
6
+
7
+ Status: pre-alpha
8
+
9
+ ## The problem
10
+
11
+ Coding agents produce plausible narratives about the work they performed.
12
+ "Done" is a claim, not a receipt. A rules file (CLAUDE.md, Cursor rules,
13
+ AGENTS.md) can ask an agent to be honest about what it did — it cannot
14
+ verify that it was. verity checks the claim instead of trusting the
15
+ narration: it moves trust from what the agent says it did to evidence a
16
+ human (or another process) can inspect directly.
17
+
18
+ ## How it works (30 seconds)
19
+
20
+ The agent — or you — declares a set of claims in `.verity/claims.json`.
21
+ verity runs one deterministic check per claim against literal reality: the
22
+ filesystem, a git repository's `HEAD` via `git show`, or a real command's
23
+ actual exit code. You get a ✓/✗ receipt on stdout and a JSON report on disk.
24
+ The process exits `1` if anything fails, so it gates scripts and CI as
25
+ easily as it informs a human review.
26
+
27
+ ```json
28
+ {
29
+ "version": "0.1",
30
+ "claims": [
31
+ { "id": "tests-pass", "type": "command", "run": "npm test", "expect": { "exitCode": 0 } }
32
+ ]
33
+ }
34
+ ```
35
+
36
+ ```
37
+ $ verity verify
38
+ ✓ tests-pass [command] command exits 0 — exit 0
39
+
40
+ 1 passed, 0 failed
41
+ OVERALL: PASS
42
+ ```
43
+
44
+ ## Install / Quickstart
45
+
46
+ **`npx @pietro-falco/verity verify`** — the zero-install path. Run it in any
47
+ project with a `.verity/claims.json` manifest.
48
+
49
+ **From source (for development):**
50
+
51
+ ```
52
+ git clone https://github.com/pietro-falco/verity.git
53
+ cd verity
54
+ npm install
55
+ npm run build
56
+ node dist/cli.js verify
57
+ ```
58
+
59
+ ## Claim types
60
+
61
+ | type | asserts | key fields |
62
+ | ---------------- | ------------------------------------------ | ------------------------------------ |
63
+ | `file_exists` | a path exists (optionally non-empty) | `path`, `nonEmpty` |
64
+ | `file_matches` | file content matches | `path`, `match` |
65
+ | `git_committed` | path is committed at `HEAD` | `path`, `match` (against committed content) |
66
+ | `command` | a command's exit code / stdout | `run`, `cwd`, `timeoutMs`, `expect` |
67
+
68
+ `match` is `{ kind: "substring" | "regex" | "sha256", value, flags? }`. Full
69
+ field-by-field semantics, defaults, and PASS conditions are in
70
+ [`docs/spec.md`](docs/spec.md).
71
+
72
+ ## For coding agents
73
+
74
+ [`SKILL.md`](SKILL.md) is a cross-agent skill any coding agent can load. The
75
+ loop it describes: emit `.verity/claims.json` after finishing a task → run
76
+ `verity verify` → paste the full raw receipt back to the human, unedited.
77
+
78
+ ## Scope and non-goals
79
+
80
+ verity does not judge semantic correctness (whether the code is *good*,
81
+ only whether the declared claim is *true*), does not sign anything in v0,
82
+ and does not orchestrate workflows. See
83
+ [`docs/adrs/0001-verity-architecture.md`](docs/adrs/0001-verity-architecture.md)
84
+ for the full reasoning. Where it sits relative to adjacent controls: rules
85
+ files request behavior; commit/CI hooks enforce process at commit time;
86
+ supply-chain attestation (SLSA/in-toto) covers release artifacts post-build;
87
+ verity reconciles task-level claims at review time — standalone, offline,
88
+ in the development loop itself.
89
+
90
+ ## Design choices
91
+
92
+ - **Zero runtime dependencies.** Node built-ins only — the whole tool is
93
+ auditable in minutes, with near-zero supply-chain surface.
94
+ - **Deterministic and offline.** No network calls, no LLM in the loop. Same
95
+ inputs, same verdicts, every time.
96
+ - **In-toto-inspired vocabulary.** Receipts use `subject` / `predicate` /
97
+ `evidence` / `verdict`, borrowed for interoperability with other
98
+ evidence-consuming tooling.
99
+
100
+ ## Verifying verity
101
+
102
+ This repository verifies itself: [`.verity/claims.json`](.verity/claims.json)
103
+ declares claims about verity's own README, license, ADR status, docs, and
104
+ test suite. Run it with:
105
+
106
+ ```
107
+ node dist/cli.js verify
108
+ ```
109
+
110
+ ## License
111
+
112
+ MIT — see [`LICENSE`](LICENSE).
package/SKILL.md ADDED
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: verity
3
+ description: Use after completing a coding task, to produce a verifiable receipt of what was actually done instead of a prose claim of completion.
4
+ ---
5
+
6
+ ## What this does
7
+
8
+ verity reconciles your claims about a task against literal reality — files
9
+ on disk, git history, real command exit codes. The human reviewing your work
10
+ trusts the receipt it produces, not your narration of what you did.
11
+
12
+ ## Workflow
13
+
14
+ 1. After finishing the task, write `.verity/claims.json` declaring
15
+ verifiable claims about what you did. One claim per meaningful assertion:
16
+ - Files you created or modified → `file_exists` / `file_matches`.
17
+ - Work you committed → `git_committed`.
18
+ - "Tests pass" / "build passes" → `command` claims that actually run
19
+ the test suite / build, not claims that assume the outcome.
20
+ 2. Run `npx @pietro-falco/verity verify` (or, in a development checkout of verity itself,
21
+ `node dist/cli.js verify`).
22
+ 3. Paste the full raw report output back to the human — never summarize it.
23
+ 4. If any claim FAILs, either fix the underlying work or correct the claim,
24
+ then rerun. If you change a claim to make it pass, disclose that you did
25
+ so and why — never silently edit a claim just to turn a FAIL into a PASS.
26
+
27
+ ## Example `.verity/claims.json`
28
+
29
+ ```json
30
+ {
31
+ "version": "0.1",
32
+ "claims": [
33
+ {
34
+ "id": "readme-updated",
35
+ "type": "file_matches",
36
+ "path": "README.md",
37
+ "match": { "kind": "substring", "value": "## Installation" }
38
+ },
39
+ {
40
+ "id": "fix-committed",
41
+ "type": "git_committed",
42
+ "path": "src/parser.ts",
43
+ "match": { "kind": "substring", "value": "function parseHeader" }
44
+ },
45
+ {
46
+ "id": "tests-pass",
47
+ "type": "command",
48
+ "run": "npm test",
49
+ "expect": { "exitCode": 0 }
50
+ },
51
+ {
52
+ "id": "build-passes",
53
+ "type": "command",
54
+ "run": "npm run build",
55
+ "expect": { "exitCode": 0 }
56
+ }
57
+ ]
58
+ }
59
+ ```
60
+
61
+ ## Rules
62
+
63
+ - Claims must be falsifiable and specific — "it works" is not a claim;
64
+ "`npm test` exits 0" is.
65
+ - Prefer `git_committed` over `file_exists` / `file_matches` when the task
66
+ said the work should be committed — a file existing in the working tree
67
+ is not the same claim as a file being committed.
68
+ - Keep `command` claims fast and side-effect-free; they run for real, every
69
+ verification.
70
+ - Add `.verity/reports/` to your project's `.gitignore` — receipts are
71
+ generated artifacts, not source.
@@ -0,0 +1,6 @@
1
+ import type { CheckContext, Claim, ClaimResult, CommandClaim, FileExistsClaim, FileMatchesClaim, GitCommittedClaim } from "./types.ts";
2
+ export declare function checkFileExists(claim: FileExistsClaim, ctx: CheckContext): ClaimResult;
3
+ export declare function checkFileMatches(claim: FileMatchesClaim, ctx: CheckContext): ClaimResult;
4
+ export declare function checkGitCommitted(claim: GitCommittedClaim, ctx: CheckContext): ClaimResult;
5
+ export declare function checkCommand(claim: CommandClaim, ctx: CheckContext): ClaimResult;
6
+ export declare function runClaim(claim: Claim, ctx: CheckContext): ClaimResult;
package/dist/checks.js ADDED
@@ -0,0 +1,226 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { readFileSync, statSync } from "node:fs";
4
+ import { relative, resolve, sep } from "node:path";
5
+ function matchBuffer(buf, match) {
6
+ if (match.kind === "sha256") {
7
+ const digest = createHash("sha256").update(buf).digest("hex");
8
+ const pass = digest.toLowerCase() === match.value.toLowerCase();
9
+ return {
10
+ pass,
11
+ evidence: pass
12
+ ? `sha256 digest ${digest} matches expected`
13
+ : `sha256 digest ${digest} != expected ${match.value.toLowerCase()}`,
14
+ };
15
+ }
16
+ const text = buf.toString("utf8");
17
+ if (match.kind === "substring") {
18
+ const pass = text.includes(match.value);
19
+ return {
20
+ pass,
21
+ evidence: pass
22
+ ? `substring ${JSON.stringify(match.value)} found`
23
+ : `substring ${JSON.stringify(match.value)} not found (${buf.length} bytes read)`,
24
+ };
25
+ }
26
+ // regex
27
+ const re = new RegExp(match.value, match.flags);
28
+ const pass = re.test(text);
29
+ return {
30
+ pass,
31
+ evidence: pass
32
+ ? `regex /${match.value}/${match.flags ?? ""} matched`
33
+ : `regex /${match.value}/${match.flags ?? ""} did not match (${buf.length} bytes read)`,
34
+ };
35
+ }
36
+ function describeMatch(match) {
37
+ if (match.kind === "sha256")
38
+ return `sha256 digest equals ${match.value.toLowerCase()}`;
39
+ if (match.kind === "substring")
40
+ return `content contains substring ${JSON.stringify(match.value)}`;
41
+ return `content matches regex /${match.value}/${match.flags ?? ""}`;
42
+ }
43
+ export function checkFileExists(claim, ctx) {
44
+ const abs = resolve(ctx.cwd, claim.path);
45
+ const predicate = claim.nonEmpty ? "file exists and is non-empty" : "file exists";
46
+ let stat;
47
+ try {
48
+ stat = statSync(abs);
49
+ }
50
+ catch {
51
+ return {
52
+ id: claim.id,
53
+ type: claim.type,
54
+ subject: claim.path,
55
+ predicate,
56
+ verdict: "FAIL",
57
+ evidence: `does not exist at ${abs}`,
58
+ };
59
+ }
60
+ if (claim.nonEmpty && stat.size === 0) {
61
+ return {
62
+ id: claim.id,
63
+ type: claim.type,
64
+ subject: claim.path,
65
+ predicate,
66
+ verdict: "FAIL",
67
+ evidence: "exists, 0 bytes (nonEmpty required)",
68
+ };
69
+ }
70
+ return {
71
+ id: claim.id,
72
+ type: claim.type,
73
+ subject: claim.path,
74
+ predicate,
75
+ verdict: "PASS",
76
+ evidence: `exists, ${stat.size} bytes`,
77
+ };
78
+ }
79
+ export function checkFileMatches(claim, ctx) {
80
+ const abs = resolve(ctx.cwd, claim.path);
81
+ const predicate = describeMatch(claim.match);
82
+ let buf;
83
+ try {
84
+ buf = readFileSync(abs);
85
+ }
86
+ catch (err) {
87
+ return {
88
+ id: claim.id,
89
+ type: claim.type,
90
+ subject: claim.path,
91
+ predicate,
92
+ verdict: "FAIL",
93
+ evidence: `file not found at ${abs}: ${err.message}`,
94
+ };
95
+ }
96
+ const outcome = matchBuffer(buf, claim.match);
97
+ return {
98
+ id: claim.id,
99
+ type: claim.type,
100
+ subject: claim.path,
101
+ predicate,
102
+ verdict: outcome.pass ? "PASS" : "FAIL",
103
+ evidence: outcome.evidence,
104
+ };
105
+ }
106
+ export function checkGitCommitted(claim, ctx) {
107
+ const predicate = claim.match
108
+ ? `path is committed at HEAD and ${describeMatch(claim.match)}`
109
+ : "path is committed at HEAD";
110
+ if (!ctx.repoRoot) {
111
+ return {
112
+ id: claim.id,
113
+ type: claim.type,
114
+ subject: claim.path,
115
+ predicate,
116
+ verdict: "FAIL",
117
+ evidence: "not inside a git repository",
118
+ };
119
+ }
120
+ // git show HEAD:<path> requires posix separators regardless of platform.
121
+ const repoRelativePath = relative(ctx.repoRoot, resolve(ctx.cwd, claim.path)).split(sep).join("/");
122
+ const result = spawnSync("git", ["show", `HEAD:${repoRelativePath}`], {
123
+ cwd: ctx.repoRoot,
124
+ });
125
+ if (result.error) {
126
+ return {
127
+ id: claim.id,
128
+ type: claim.type,
129
+ subject: claim.path,
130
+ predicate,
131
+ verdict: "FAIL",
132
+ evidence: `failed to run git: ${result.error.message}`,
133
+ };
134
+ }
135
+ if (result.status !== 0) {
136
+ const stderr = (result.stderr ?? Buffer.alloc(0)).toString("utf8").trim();
137
+ return {
138
+ id: claim.id,
139
+ type: claim.type,
140
+ subject: claim.path,
141
+ predicate,
142
+ verdict: "FAIL",
143
+ evidence: `git show HEAD:${repoRelativePath} exit ${result.status}${stderr ? `; stderr: ${stderr}` : ""}`,
144
+ };
145
+ }
146
+ if (!claim.match) {
147
+ return {
148
+ id: claim.id,
149
+ type: claim.type,
150
+ subject: claim.path,
151
+ predicate,
152
+ verdict: "PASS",
153
+ evidence: `git show HEAD:${repoRelativePath} exit 0`,
154
+ };
155
+ }
156
+ const outcome = matchBuffer(result.stdout ?? Buffer.alloc(0), claim.match);
157
+ return {
158
+ id: claim.id,
159
+ type: claim.type,
160
+ subject: claim.path,
161
+ predicate,
162
+ verdict: outcome.pass ? "PASS" : "FAIL",
163
+ evidence: `git show HEAD:${repoRelativePath} exit 0; ${outcome.evidence}`,
164
+ };
165
+ }
166
+ const DEFAULT_COMMAND_TIMEOUT_MS = 60_000;
167
+ export function checkCommand(claim, ctx) {
168
+ const expectedExitCode = claim.expect.exitCode ?? 0;
169
+ const timeoutMs = claim.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
170
+ const cwd = claim.cwd ? resolve(ctx.cwd, claim.cwd) : ctx.cwd;
171
+ const predicate = `command exits ${expectedExitCode}${claim.expect.stdout ? ` and stdout ${describeMatch(claim.expect.stdout)}` : ""}`;
172
+ const result = spawnSync(claim.run, {
173
+ shell: true,
174
+ cwd,
175
+ timeout: timeoutMs,
176
+ });
177
+ if (result.error && result.error.code !== "ETIMEDOUT") {
178
+ return {
179
+ id: claim.id,
180
+ type: claim.type,
181
+ subject: claim.run,
182
+ predicate,
183
+ verdict: "FAIL",
184
+ evidence: `command failed to spawn: ${result.error.message}`,
185
+ };
186
+ }
187
+ if (result.signal || result.error?.code === "ETIMEDOUT") {
188
+ return {
189
+ id: claim.id,
190
+ type: claim.type,
191
+ subject: claim.run,
192
+ predicate,
193
+ verdict: "FAIL",
194
+ evidence: `command timed out after ${timeoutMs}ms (killed with ${result.signal ?? "SIGTERM"})`,
195
+ };
196
+ }
197
+ const actualExitCode = result.status ?? -1;
198
+ const exitPass = actualExitCode === expectedExitCode;
199
+ const stdoutOutcome = claim.expect.stdout
200
+ ? matchBuffer(result.stdout ?? Buffer.alloc(0), claim.expect.stdout)
201
+ : null;
202
+ const pass = exitPass && (stdoutOutcome === null || stdoutOutcome.pass);
203
+ const parts = [exitPass ? `exit ${actualExitCode}` : `exit ${actualExitCode} (expected ${expectedExitCode})`];
204
+ if (stdoutOutcome)
205
+ parts.push(`stdout: ${stdoutOutcome.evidence}`);
206
+ return {
207
+ id: claim.id,
208
+ type: claim.type,
209
+ subject: claim.run,
210
+ predicate,
211
+ verdict: pass ? "PASS" : "FAIL",
212
+ evidence: parts.join("; "),
213
+ };
214
+ }
215
+ export function runClaim(claim, ctx) {
216
+ switch (claim.type) {
217
+ case "file_exists":
218
+ return checkFileExists(claim, ctx);
219
+ case "file_matches":
220
+ return checkFileMatches(claim, ctx);
221
+ case "git_committed":
222
+ return checkGitCommitted(claim, ctx);
223
+ case "command":
224
+ return checkCommand(claim, ctx);
225
+ }
226
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function main(argv: string[]): number;
package/dist/cli.js ADDED
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ import process from "node:process";
3
+ import { formatHuman, toReceiptJson, writeReceipt } from "./report.js";
4
+ import { VerityUsageError } from "./types.js";
5
+ import { DEFAULT_MANIFEST_PATH, getToolVersion, verify } from "./verify.js";
6
+ const USAGE = `verity — deterministic claim verifier
7
+
8
+ Usage:
9
+ verity verify [manifestPath] [--json]
10
+ verity --version | -v
11
+ verity --help | -h
12
+
13
+ Arguments:
14
+ manifestPath Path to claims manifest (default: ${DEFAULT_MANIFEST_PATH})
15
+
16
+ Options:
17
+ --json Print the full JSON report to stdout instead of the human summary`;
18
+ function runVerifyCommand(args) {
19
+ let manifestPath = DEFAULT_MANIFEST_PATH;
20
+ let json = false;
21
+ for (const arg of args) {
22
+ if (arg === "--json") {
23
+ json = true;
24
+ }
25
+ else if (arg.startsWith("-")) {
26
+ process.stderr.write(`unknown option: ${arg}\n`);
27
+ return 2;
28
+ }
29
+ else {
30
+ manifestPath = arg;
31
+ }
32
+ }
33
+ let outcome;
34
+ try {
35
+ outcome = verify(manifestPath);
36
+ }
37
+ catch (err) {
38
+ if (err instanceof VerityUsageError) {
39
+ process.stderr.write(`${err.message}\n`);
40
+ return 2;
41
+ }
42
+ throw err;
43
+ }
44
+ writeReceipt(outcome.report, process.cwd());
45
+ process.stdout.write(`${json ? toReceiptJson(outcome.report) : formatHuman(outcome.report)}\n`);
46
+ return outcome.exitCode;
47
+ }
48
+ export function main(argv) {
49
+ const [cmd, ...rest] = argv;
50
+ if (cmd === "--version" || cmd === "-v") {
51
+ process.stdout.write(`${getToolVersion()}\n`);
52
+ return 0;
53
+ }
54
+ if (cmd === "--help" || cmd === "-h") {
55
+ process.stdout.write(`${USAGE}\n`);
56
+ return 0;
57
+ }
58
+ if (cmd === undefined) {
59
+ process.stderr.write(`${USAGE}\n`);
60
+ return 2;
61
+ }
62
+ if (cmd === "verify") {
63
+ return runVerifyCommand(rest);
64
+ }
65
+ process.stderr.write(`unknown command: ${cmd}\n${USAGE}\n`);
66
+ return 2;
67
+ }
68
+ process.exitCode = main(process.argv.slice(2));
@@ -0,0 +1,4 @@
1
+ import type { VerifyReport } from "./types.ts";
2
+ export declare function formatHuman(report: VerifyReport): string;
3
+ export declare function toReceiptJson(report: VerifyReport): string;
4
+ export declare function writeReceipt(report: VerifyReport, cwd: string): string;
package/dist/report.js ADDED
@@ -0,0 +1,26 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ export function formatHuman(report) {
4
+ const lines = report.results.map((r) => {
5
+ const mark = r.verdict === "PASS" ? "✓" : "✗";
6
+ return `${mark} ${r.id} [${r.type}] ${r.predicate} — ${r.evidence}`;
7
+ });
8
+ const passed = report.results.filter((r) => r.verdict === "PASS").length;
9
+ const failed = report.results.length - passed;
10
+ const overall = failed === 0 ? "PASS" : "FAIL";
11
+ lines.push("");
12
+ lines.push(`${passed} passed, ${failed} failed`);
13
+ lines.push(`OVERALL: ${overall}`);
14
+ return lines.join("\n");
15
+ }
16
+ export function toReceiptJson(report) {
17
+ return JSON.stringify(report, null, 2);
18
+ }
19
+ export function writeReceipt(report, cwd) {
20
+ const dir = resolve(cwd, ".verity", "reports");
21
+ mkdirSync(dir, { recursive: true });
22
+ const filename = `${report.timestamp.replace(/:/g, "-")}.json`;
23
+ const path = join(dir, filename);
24
+ writeFileSync(path, toReceiptJson(report));
25
+ return path;
26
+ }
@@ -0,0 +1,65 @@
1
+ export type MatchKind = "substring" | "regex" | "sha256";
2
+ export interface MatchSpec {
3
+ kind: MatchKind;
4
+ value: string;
5
+ flags?: string;
6
+ }
7
+ interface ClaimBase {
8
+ id: string;
9
+ description?: string;
10
+ }
11
+ export interface FileExistsClaim extends ClaimBase {
12
+ type: "file_exists";
13
+ path: string;
14
+ nonEmpty?: boolean;
15
+ }
16
+ export interface FileMatchesClaim extends ClaimBase {
17
+ type: "file_matches";
18
+ path: string;
19
+ match: MatchSpec;
20
+ }
21
+ export interface GitCommittedClaim extends ClaimBase {
22
+ type: "git_committed";
23
+ path: string;
24
+ match?: MatchSpec;
25
+ }
26
+ export interface CommandExpect {
27
+ exitCode?: number;
28
+ stdout?: MatchSpec;
29
+ }
30
+ export interface CommandClaim extends ClaimBase {
31
+ type: "command";
32
+ run: string;
33
+ cwd?: string;
34
+ timeoutMs?: number;
35
+ expect: CommandExpect;
36
+ }
37
+ export type Claim = FileExistsClaim | FileMatchesClaim | GitCommittedClaim | CommandClaim;
38
+ export interface Manifest {
39
+ version: string;
40
+ claims: Claim[];
41
+ }
42
+ export type Verdict = "PASS" | "FAIL";
43
+ export interface ClaimResult {
44
+ id: string;
45
+ type: Claim["type"];
46
+ subject: string;
47
+ predicate: string;
48
+ verdict: Verdict;
49
+ evidence: string;
50
+ }
51
+ export interface VerifyReport {
52
+ version: string;
53
+ timestamp: string;
54
+ gitHeadSha: string | null;
55
+ results: ClaimResult[];
56
+ }
57
+ export interface CheckContext {
58
+ /** Directory that file_exists / file_matches paths and command cwd are resolved against. */
59
+ cwd: string;
60
+ /** Git repo root used for git_committed checks; null when not inside a git repo. */
61
+ repoRoot: string | null;
62
+ }
63
+ export declare class VerityUsageError extends Error {
64
+ }
65
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export class VerityUsageError extends Error {
2
+ }
@@ -0,0 +1,12 @@
1
+ import type { Manifest, VerifyReport } from "./types.ts";
2
+ export declare const DEFAULT_MANIFEST_PATH = ".verity/claims.json";
3
+ export declare function parseManifest(raw: unknown): Manifest;
4
+ export declare function loadManifest(manifestPath: string): Manifest;
5
+ export declare function getRepoRoot(cwd: string): string | null;
6
+ export declare function getGitHeadSha(cwd: string): string | null;
7
+ export declare function getToolVersion(): string;
8
+ export interface VerifyOutcome {
9
+ report: VerifyReport;
10
+ exitCode: 0 | 1;
11
+ }
12
+ export declare function verify(manifestPath: string, cwd?: string): VerifyOutcome;
package/dist/verify.js ADDED
@@ -0,0 +1,169 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import { basename, dirname, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { runClaim } from "./checks.js";
6
+ import { VerityUsageError } from "./types.js";
7
+ export const DEFAULT_MANIFEST_PATH = ".verity/claims.json";
8
+ const CLAIM_TYPES = new Set(["file_exists", "file_matches", "git_committed", "command"]);
9
+ const MATCH_KINDS = new Set(["substring", "regex", "sha256"]);
10
+ function assertString(value, field) {
11
+ if (typeof value !== "string" || value.length === 0) {
12
+ throw new VerityUsageError(`expected non-empty string for "${field}", got ${JSON.stringify(value)}`);
13
+ }
14
+ }
15
+ function validateMatchSpec(value, field) {
16
+ if (typeof value !== "object" || value === null) {
17
+ throw new VerityUsageError(`expected match object for "${field}"`);
18
+ }
19
+ const match = value;
20
+ if (!MATCH_KINDS.has(match.kind)) {
21
+ throw new VerityUsageError(`unknown match kind "${String(match.kind)}" in "${field}"`);
22
+ }
23
+ assertString(match.value, `${field}.value`);
24
+ if (match.flags !== undefined && typeof match.flags !== "string") {
25
+ throw new VerityUsageError(`expected string for "${field}.flags"`);
26
+ }
27
+ return { kind: match.kind, value: match.value, flags: match.flags };
28
+ }
29
+ function validateClaim(raw, index) {
30
+ if (typeof raw !== "object" || raw === null) {
31
+ throw new VerityUsageError(`claim[${index}] is not an object`);
32
+ }
33
+ const claim = raw;
34
+ assertString(claim.id, `claim[${index}].id`);
35
+ if (!CLAIM_TYPES.has(claim.type)) {
36
+ throw new VerityUsageError(`claim "${claim.id}" has unknown claim type "${String(claim.type)}"`);
37
+ }
38
+ const description = claim.description !== undefined ? String(claim.description) : undefined;
39
+ switch (claim.type) {
40
+ case "file_exists": {
41
+ assertString(claim.path, `claim "${claim.id}".path`);
42
+ if (claim.nonEmpty !== undefined && typeof claim.nonEmpty !== "boolean") {
43
+ throw new VerityUsageError(`claim "${claim.id}".nonEmpty must be boolean`);
44
+ }
45
+ return {
46
+ id: claim.id,
47
+ type: "file_exists",
48
+ description,
49
+ path: claim.path,
50
+ nonEmpty: claim.nonEmpty,
51
+ };
52
+ }
53
+ case "file_matches": {
54
+ assertString(claim.path, `claim "${claim.id}".path`);
55
+ const match = validateMatchSpec(claim.match, `claim "${claim.id}".match`);
56
+ return { id: claim.id, type: "file_matches", description, path: claim.path, match };
57
+ }
58
+ case "git_committed": {
59
+ assertString(claim.path, `claim "${claim.id}".path`);
60
+ const match = claim.match !== undefined ? validateMatchSpec(claim.match, `claim "${claim.id}".match`) : undefined;
61
+ return { id: claim.id, type: "git_committed", description, path: claim.path, match };
62
+ }
63
+ case "command": {
64
+ assertString(claim.run, `claim "${claim.id}".run`);
65
+ if (claim.cwd !== undefined)
66
+ assertString(claim.cwd, `claim "${claim.id}".cwd`);
67
+ if (claim.timeoutMs !== undefined && typeof claim.timeoutMs !== "number") {
68
+ throw new VerityUsageError(`claim "${claim.id}".timeoutMs must be a number`);
69
+ }
70
+ if (typeof claim.expect !== "object" || claim.expect === null) {
71
+ throw new VerityUsageError(`claim "${claim.id}".expect is required`);
72
+ }
73
+ const expectRaw = claim.expect;
74
+ if (expectRaw.exitCode !== undefined && typeof expectRaw.exitCode !== "number") {
75
+ throw new VerityUsageError(`claim "${claim.id}".expect.exitCode must be a number`);
76
+ }
77
+ const stdout = expectRaw.stdout !== undefined ? validateMatchSpec(expectRaw.stdout, `claim "${claim.id}".expect.stdout`) : undefined;
78
+ return {
79
+ id: claim.id,
80
+ type: "command",
81
+ description,
82
+ run: claim.run,
83
+ cwd: claim.cwd,
84
+ timeoutMs: claim.timeoutMs,
85
+ expect: { exitCode: expectRaw.exitCode, stdout },
86
+ };
87
+ }
88
+ default:
89
+ throw new VerityUsageError(`claim "${claim.id}" has unknown claim type "${String(claim.type)}"`);
90
+ }
91
+ }
92
+ export function parseManifest(raw) {
93
+ if (typeof raw !== "object" || raw === null) {
94
+ throw new VerityUsageError("manifest root must be an object");
95
+ }
96
+ const data = raw;
97
+ assertString(data.version, "version");
98
+ if (!Array.isArray(data.claims)) {
99
+ throw new VerityUsageError('manifest "claims" must be an array');
100
+ }
101
+ const seenIds = new Set();
102
+ const claims = data.claims.map((c, i) => {
103
+ const claim = validateClaim(c, i);
104
+ if (seenIds.has(claim.id)) {
105
+ throw new VerityUsageError(`duplicate claim id "${claim.id}"`);
106
+ }
107
+ seenIds.add(claim.id);
108
+ return claim;
109
+ });
110
+ return { version: data.version, claims };
111
+ }
112
+ export function loadManifest(manifestPath) {
113
+ let text;
114
+ try {
115
+ text = readFileSync(manifestPath, "utf8");
116
+ }
117
+ catch (err) {
118
+ throw new VerityUsageError(`manifest not found at ${manifestPath}: ${err.message}`);
119
+ }
120
+ let json;
121
+ try {
122
+ json = JSON.parse(text);
123
+ }
124
+ catch (err) {
125
+ throw new VerityUsageError(`invalid JSON in manifest ${manifestPath}: ${err.message}`);
126
+ }
127
+ return parseManifest(json);
128
+ }
129
+ export function getRepoRoot(cwd) {
130
+ const result = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
131
+ if (result.status !== 0)
132
+ return null;
133
+ return result.stdout.trim();
134
+ }
135
+ export function getGitHeadSha(cwd) {
136
+ const result = spawnSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8" });
137
+ if (result.status !== 0)
138
+ return null;
139
+ return result.stdout.trim();
140
+ }
141
+ export function getToolVersion() {
142
+ const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
143
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
144
+ return pkg.version;
145
+ }
146
+ /**
147
+ * The base directory that relative claim paths (and default command cwd) resolve against:
148
+ * the directory containing the manifest, or its parent when the manifest lives under a
149
+ * ".verity" directory (so the base is the repo root, not the ".verity" dir itself).
150
+ */
151
+ function resolveBaseDir(absManifestPath) {
152
+ const manifestDir = dirname(absManifestPath);
153
+ return basename(manifestDir) === ".verity" ? dirname(manifestDir) : manifestDir;
154
+ }
155
+ export function verify(manifestPath, cwd = process.cwd()) {
156
+ const absManifestPath = resolve(cwd, manifestPath);
157
+ const manifest = loadManifest(absManifestPath);
158
+ const base = resolveBaseDir(absManifestPath);
159
+ const repoRoot = getRepoRoot(base);
160
+ const results = manifest.claims.map((claim) => runClaim(claim, { cwd: base, repoRoot }));
161
+ const report = {
162
+ version: getToolVersion(),
163
+ timestamp: new Date().toISOString(),
164
+ gitHeadSha: getGitHeadSha(base),
165
+ results,
166
+ };
167
+ const exitCode = results.every((r) => r.verdict === "PASS") ? 0 : 1;
168
+ return { report, exitCode };
169
+ }
package/docs/spec.md ADDED
@@ -0,0 +1,149 @@
1
+ # Claims manifest format (v0.1)
2
+
3
+ This document specifies the `.verity/claims.json` format and the deterministic
4
+ verification behavior of `verity verify`. The format version below (`"0.1"`)
5
+ is independent from the package version.
6
+
7
+ ## Purpose
8
+
9
+ A claims manifest declares specific, checkable assertions about a piece of
10
+ work — files that should exist, content that should be present, work that
11
+ should be committed, commands that should succeed. `verity verify` checks
12
+ each claim against literal reality and reports a PASS/FAIL verdict per claim.
13
+
14
+ ## File location and base directory
15
+
16
+ By convention the manifest lives at `.verity/claims.json`, but any path may
17
+ be passed to `verity verify [manifestPath]`.
18
+
19
+ All relative claim paths (and the default `cwd` for `command` claims) resolve
20
+ against a **base directory**, computed from the manifest's own location: the
21
+ directory containing the manifest file, or — if that directory is named
22
+ `.verity` — its **parent** directory instead, so a manifest at
23
+ `.verity/claims.json` resolves paths against the repo root, not against the
24
+ `.verity/` directory itself. The process's current working directory is
25
+ never used for path resolution, only the manifest's location.
26
+
27
+ ## Manifest shape
28
+
29
+ ```json
30
+ {
31
+ "version": "0.1",
32
+ "claims": [
33
+ { "id": "string, unique within the manifest", "type": "...", "description": "optional" }
34
+ ]
35
+ }
36
+ ```
37
+
38
+ `claims` is an array of claim objects; each `id` must be unique in the manifest.
39
+
40
+ ## Claim types
41
+
42
+ ### `file_exists`
43
+
44
+ Asserts a path exists, resolved against the base directory.
45
+
46
+ - `path` (string, required)
47
+ - `nonEmpty` (boolean, optional, default `false`) — also requires size > 0.
48
+
49
+ PASS when the path exists (and, if `nonEmpty`, has size > 0). FAIL otherwise.
50
+
51
+ ### `file_matches`
52
+
53
+ Asserts a file's content matches a `match` spec, resolved against the base.
54
+
55
+ - `path` (string, required)
56
+ - `match` (MatchSpec, required)
57
+
58
+ PASS when the file exists and its content satisfies `match`. FAIL if missing
59
+ or non-matching.
60
+
61
+ ### `git_committed`
62
+
63
+ Asserts a path is committed at `HEAD`, verified via `git show HEAD:<path>`
64
+ against the git repository containing the base directory — never the
65
+ working tree.
66
+
67
+ - `path` (string, required)
68
+ - `match` (MatchSpec, optional) — matched against the **committed** content
69
+ from `git show`, not the working-tree file.
70
+
71
+ PASS when `git show HEAD:<path>` exits 0 (and, if `match` is present, the
72
+ committed content satisfies it). FAIL if there is no enclosing repository,
73
+ the path is not committed at `HEAD`, or the match fails.
74
+
75
+ ### `command`
76
+
77
+ Runs a shell command and asserts its outcome.
78
+
79
+ - `run` (string, required) — executed via the platform shell.
80
+ - `cwd` (string, optional) — defaults to the base directory; resolved
81
+ against the base directory when given.
82
+ - `timeoutMs` (number, optional, default `60000`).
83
+ - `expect.exitCode` (number, optional, default `0`).
84
+ - `expect.stdout` (MatchSpec, optional).
85
+
86
+ PASS when the command exits within the timeout with the expected exit code
87
+ and (if given) `stdout` satisfies `expect.stdout`. FAIL on a non-matching
88
+ exit code, non-matching stdout, or a timeout (process is killed, claim FAILs).
89
+
90
+ ## The match object (`MatchSpec`)
91
+
92
+ ```json
93
+ { "kind": "substring" | "regex" | "sha256", "value": "string", "flags": "optional string" }
94
+ ```
95
+
96
+ - `substring` — PASS if `value` is found anywhere in the content.
97
+ - `regex` — PASS if `new RegExp(value, flags)` matches. `flags` are standard
98
+ JavaScript regex flags (e.g. `"m"`, `"i"`).
99
+ - `sha256` — PASS if the SHA-256 digest of the content, as lowercase hex,
100
+ equals `value` compared case-insensitively.
101
+
102
+ ## Exit codes
103
+
104
+ - `0` — manifest loaded and every claim PASSed.
105
+ - `1` — manifest loaded and at least one claim FAILed.
106
+ - `2` — usage error: manifest not found, invalid JSON, a schema violation,
107
+ or an unknown claim type.
108
+
109
+ ## The receipt
110
+
111
+ Every `verity verify` run writes a JSON receipt to
112
+ `.verity/reports/<ISO-timestamp>.json` (under the process's current working
113
+ directory), regardless of `--json`. `--json` also prints the same JSON to
114
+ stdout instead of the human-readable summary.
115
+
116
+ ```json
117
+ {
118
+ "version": "package version of the verity binary that produced the report",
119
+ "timestamp": "ISO 8601 timestamp",
120
+ "gitHeadSha": "string | null (best-effort, null if not inside a git repo)",
121
+ "results": [
122
+ {
123
+ "id": "string",
124
+ "type": "file_exists | file_matches | git_committed | command",
125
+ "subject": "the path or command the claim is about",
126
+ "predicate": "human-readable description of what was checked",
127
+ "verdict": "PASS | FAIL",
128
+ "evidence": "human-readable evidence for the verdict"
129
+ }
130
+ ]
131
+ }
132
+ ```
133
+
134
+ The `subject` / `predicate` / `evidence` / `verdict` vocabulary is borrowed
135
+ from in-toto attestation terminology for interoperability — a naming
136
+ convention, not a claim of in-toto compliance.
137
+
138
+ ## Versioning
139
+
140
+ Format version `0.1` and its `0.x` successors are additive: new optional
141
+ fields and new claim types may be added without a major bump. An unknown
142
+ claim type is a usage error (exit code `2`), not a silently skipped claim.
143
+
144
+ ## Determinism guarantees
145
+
146
+ Verification is local and offline: no network calls, no LLM involvement.
147
+ Given the same manifest and the same filesystem/git/command state, `verity
148
+ verify` produces the same verdicts every time. `command` claims are exactly
149
+ as deterministic as the command being run.
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@pietro-falco/verity",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Turn your coding agent's 'done' into a receipt you can check — deterministic, local, zero-dependency.",
6
+ "bin": {
7
+ "verity": "dist/cli.js"
8
+ },
9
+ "main": "dist/verify.js",
10
+ "types": "dist/verify.d.ts",
11
+ "files": ["dist", "SKILL.md", "docs/spec.md"],
12
+ "engines": {
13
+ "node": ">=20"
14
+ },
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "test": "node --test",
18
+ "prepare": "tsc"
19
+ },
20
+ "keywords": [
21
+ "ai-agents",
22
+ "coding-agents",
23
+ "verification",
24
+ "guardrails",
25
+ "claude-code",
26
+ "evidence",
27
+ "receipt",
28
+ "cli",
29
+ "deterministic"
30
+ ],
31
+ "author": "Pietro Falco <pietrofalco.dev@gmail.com>",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/pietro-falco/verity.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/pietro-falco/verity/issues"
39
+ },
40
+ "homepage": "https://github.com/pietro-falco/verity#readme",
41
+ "devDependencies": {
42
+ "typescript": "^6.0.3",
43
+ "@types/node": "^26.1.0"
44
+ }
45
+ }