@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 +21 -0
- package/README.md +112 -0
- package/SKILL.md +71 -0
- package/dist/checks.d.ts +6 -0
- package/dist/checks.js +226 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +68 -0
- package/dist/report.d.ts +4 -0
- package/dist/report.js +26 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +2 -0
- package/dist/verify.d.ts +12 -0
- package/dist/verify.js +169 -0
- package/docs/spec.md +149 -0
- package/package.json +45 -0
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
|
+
[](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.
|
package/dist/checks.d.ts
ADDED
|
@@ -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
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));
|
package/dist/report.d.ts
ADDED
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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -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
package/dist/verify.d.ts
ADDED
|
@@ -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
|
+
}
|