veriskit 0.3.0 → 0.4.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/CHANGELOG.md +12 -0
- package/README.md +148 -195
- package/dist/index.js +488 -69
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 — 2026-07-10
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Evidence System. Every `veris verify` and `veris affected` run writes a canonical, git-anchored `.veris/runs/<id>/evidence.json` (schema `veriskit/evidence@1`) with a sha256 integrity digest over the whole record and a sha256 of each per-check log. No new runtime dependencies.
|
|
7
|
+
- `veris evidence verify <file>` recomputes and checks a record or a bundle, and states plainly what an integrity digest does and does not prove.
|
|
8
|
+
- `veris evidence bundle` packages the latest run (record, report, and logs, each digested) into one portable proof file under `.veris/evidence/`.
|
|
9
|
+
- `veris evidence show` prints the latest record's key facts.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- The report and terminal output now show the git commit and whether the tree was clean, plus the evidence digest in the report.
|
|
13
|
+
- `evidence.json` replaces the older `metadata.json`. `veris init` now also gitignores `evidence/`.
|
|
14
|
+
|
|
3
15
|
## 0.3.0 — 2026-07-10
|
|
4
16
|
|
|
5
17
|
### Added
|
package/README.md
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/veriskit-icon.svg" alt="VerisKit" width="96" height="96">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">VerisKit</h1>
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
<p align="center"><strong>The fastest way to prove your software works.</strong></p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://www.npmjs.com/package/veriskit"><img src="https://img.shields.io/npm/v/veriskit?color=14b8a6&label=npm" alt="npm version"></a>
|
|
11
|
+
<img src="https://img.shields.io/badge/deps-cac%20%2B%20picocolors-14b8a6" alt="two runtime dependencies">
|
|
12
|
+
<img src="https://img.shields.io/badge/license-MIT-14b8a6" alt="MIT license">
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<img src="assets/veriskit-affected.gif" alt="veris affected narrowing the unit run to only the tests that reach a change" width="760">
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center"><em>Change a file. VerisKit runs only the tests that reach it (2 of 28 here), then gives an honest verdict.</em></p>
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
VerisKit runs the test and quality tools your project already has (TypeScript, Vitest, Jest, `node:test`, ESLint, Biome), then turns their results into one honest verdict with a Markdown report you can paste into a pull request. There is no config to write and no new test framework to learn.
|
|
24
|
+
|
|
25
|
+
It answers the question a wall of green checkmarks leaves open: **is this change safe enough to trust?**
|
|
9
26
|
|
|
10
27
|
## Install
|
|
11
28
|
|
|
@@ -19,8 +36,7 @@ Or add it to a project:
|
|
|
19
36
|
npm install --save-dev veriskit
|
|
20
37
|
```
|
|
21
38
|
|
|
22
|
-
|
|
23
|
-
> existing package). The installed command is still **`veris`**.
|
|
39
|
+
You install the package `veriskit`. The command it gives you is `veris`. (The bare name `veris` was already too close to another npm package.)
|
|
24
40
|
|
|
25
41
|
## Quickstart
|
|
26
42
|
|
|
@@ -30,17 +46,16 @@ veris verify # run the configured checks, print a verdict, write a report
|
|
|
30
46
|
veris report # print the latest report
|
|
31
47
|
```
|
|
32
48
|
|
|
33
|
-
`veris doctor` and `veris test`
|
|
34
|
-
capability report (what will run, what will be skipped, and why); `test` runs
|
|
35
|
-
just the detected unit test runner with the same summarized output as
|
|
36
|
-
`verify`.
|
|
49
|
+
Two more commands round out the basics. `veris doctor` prints a read-only capability report: what will run, what will be skipped, and why. `veris test` runs just the detected unit test runner with the same summarized output as `verify`.
|
|
37
50
|
|
|
38
|
-
##
|
|
51
|
+
## The verdict
|
|
52
|
+
|
|
53
|
+
`veris verify` runs your checks and prints one result. Here is a passing run:
|
|
39
54
|
|
|
40
55
|
```text
|
|
41
|
-
|
|
56
|
+
VerisKit
|
|
42
57
|
|
|
43
|
-
Project
|
|
58
|
+
Project veriskit
|
|
44
59
|
Risk —
|
|
45
60
|
|
|
46
61
|
Checks
|
|
@@ -51,56 +66,78 @@ Checks
|
|
|
51
66
|
Result
|
|
52
67
|
✓ Verified
|
|
53
68
|
|
|
69
|
+
Commit 4fa33a9 · tree clean
|
|
70
|
+
|
|
54
71
|
Report
|
|
55
|
-
.veris/reports/verify-2026-07-
|
|
72
|
+
.veris/reports/verify-2026-07-10T09-04-52-076Z-1.md
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The verdict has three states, not two:
|
|
76
|
+
|
|
77
|
+
| Verdict | Exit code | Meaning |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `verified` | `0` | every configured check ran and passed |
|
|
80
|
+
| `failed` | `1` | at least one check failed |
|
|
81
|
+
| `partial` | `2` (`0` with `--partial-ok`) | no failures, but a check was skipped or its result is unknown |
|
|
82
|
+
|
|
83
|
+
A partial verdict is not a pass. A check can end up skipped when its runner is not installed, or when nothing in your change reaches it. VerisKit lists that check as skipped and lowers the verdict rather than folding it into "verified", because a folded skip hands CI confidence it did not earn. If your team wants partial runs to pass CI, opt in with `veris verify --partial-ok`.
|
|
84
|
+
|
|
85
|
+
VerisKit orchestrates the tools you already run. It shells out to `tsc`, Vitest, Jest, `node:test`, ESLint, and Biome, reads their exit codes and output, and reports one result. It does not run browser tests, and it does not ship its own test engine. Detection stays read-only, and `init` never overwrites an existing `.veris/config.json`.
|
|
86
|
+
|
|
87
|
+
## Developer loop
|
|
88
|
+
|
|
89
|
+
While you work, scope the checks to what you touched instead of running everything.
|
|
90
|
+
|
|
91
|
+
### `veris affected`
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
veris affected # checks affected by working-tree changes vs HEAD
|
|
95
|
+
veris affected --base main # checks affected by the diff against another ref (PR/CI)
|
|
56
96
|
```
|
|
57
97
|
|
|
58
|
-
`
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
`affected` reads which files changed (your working tree plus untracked files against `HEAD`, or against `--base <ref>` for a PR/CI diff) and maps each one to the check categories it can touch:
|
|
99
|
+
|
|
100
|
+
| Changed file | Checks run |
|
|
101
|
+
|---|---|
|
|
102
|
+
| test file | unit, lint |
|
|
103
|
+
| TypeScript file | types, lint, unit |
|
|
104
|
+
| JavaScript file | lint, unit |
|
|
105
|
+
| config (`tsconfig`, biome/eslint config, `package.json`, `veris.config.*`) | every available check |
|
|
106
|
+
| docs and assets (`.md`, images, `LICENSE`) | nothing |
|
|
107
|
+
| anything else unrecognized | every available check (a safe default) |
|
|
108
|
+
|
|
109
|
+
That table picks the check categories. Inside the `unit` category, `affected` goes further: it builds the same import graph that [`veris scan` and `veris plan`](#project-intelligence) use, then runs only the test files that transitively import your changed files.
|
|
110
|
+
|
|
111
|
+
The narrowing stays conservative. It runs the full unit suite whenever it cannot prove a smaller set is safe:
|
|
112
|
+
|
|
113
|
+
- a changed file matches a config or global pattern (`tsconfig*.json`, `package.json`, a `*.config.*` or `*.setup.*` file, biome/eslint config)
|
|
114
|
+
- a changed file is not a node in the import graph (it sits under an ignored directory like `node_modules` or `.veris`, or it is not a recognized code extension)
|
|
115
|
+
- no test file reaches any changed file, so the change has no tests to run
|
|
116
|
+
- the graph came from the relative-imports scanner rather than TypeScript, which can miss aliased imports
|
|
117
|
+
|
|
118
|
+
The output says when a run was narrowed, and when it ran in full and why:
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
unit narrowed to 3 of 41 test file(s) via typescript graph
|
|
122
|
+
unit ran in full — global/config change (package.json)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
An `affected` run never prints a bare "Verified". The terminal and report say "Affected checks passed" (or "failed", or "Affected: partial"), and every available-but-unaffected capability is listed as skipped with the reason `not affected by changes`. Touch only a doc and VerisKit prints "Nothing affected" and exits `0`, which is a no-op and not a verified result: no checks ran.
|
|
126
|
+
|
|
127
|
+
### `veris watch`
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
veris watch # re-run affected checks as files change
|
|
131
|
+
veris watch --poll # use mtime polling instead of native fs.watch
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`watch` runs a full baseline over every available check once, then re-runs only the checks affected by whatever changed since the last tick. It uses Node's built-in recursive `fs.watch`, so it adds no dependency. On a platform where recursive `fs.watch` is unavailable, or a filesystem where native events are unreliable (some containers and network mounts), pass `--poll` to diff file mtimes on an interval instead.
|
|
135
|
+
|
|
136
|
+
Each tick reprints the full board and rebuilds the graph fresh, so narrowing reflects the file you just saved. A capability the latest change did not touch keeps its last real result, marked `⟳ cached`. A cached failure stays a failure (`✗`); VerisKit never hides it just because it did not rerun this tick. Press Ctrl-C to stop, and the watcher (or poll loop) closes cleanly with exit `0`.
|
|
97
137
|
|
|
98
138
|
## Project intelligence
|
|
99
139
|
|
|
100
|
-
`veris scan` and `veris plan` map your codebase's import graph and turn it
|
|
101
|
-
into recommendations — what to test, where verification is weak, and (with
|
|
102
|
-
`--base`) which of your changes are risky. Both are **read-only analysis**;
|
|
103
|
-
neither generates or writes any code.
|
|
140
|
+
`veris scan` and `veris plan` map your codebase's import graph and turn it into recommendations: what to test, where verification is thin, and which of your changes carry risk. Both are read-only analysis. Neither writes or generates any code.
|
|
104
141
|
|
|
105
142
|
### `veris scan`
|
|
106
143
|
|
|
@@ -108,31 +145,12 @@ neither generates or writes any code.
|
|
|
108
145
|
veris scan
|
|
109
146
|
```
|
|
110
147
|
|
|
111
|
-
`scan`
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
**`scan` always states which resolver built the graph**, because the two
|
|
118
|
-
have very different accuracy:
|
|
119
|
-
|
|
120
|
-
- **`typescript`** — used when the project has a `tsconfig.json` and its own
|
|
121
|
-
`typescript` package exposes the classic compiler API
|
|
122
|
-
(`preProcessFile`/`resolveModuleName`/`readConfigFile`/…). Veris loads
|
|
123
|
-
*your project's own* TypeScript at run time — **no new dependency is added
|
|
124
|
-
for this** — and resolves imports the way `tsc` would: `tsconfig` path
|
|
125
|
-
aliases, extension-mapped specifiers, index resolution, and so on.
|
|
126
|
-
- **`scanner`** — the fallback when there's no TypeScript, no
|
|
127
|
-
`tsconfig.json`, or the installed `typescript` package is a 7.x
|
|
128
|
-
native/Go-ported build that doesn't expose the classic compiler API veris
|
|
129
|
-
needs. The scanner is a dependency-free, **relative-imports-only** reader:
|
|
130
|
-
it follows `./foo` and `../bar/baz` but does not understand `tsconfig`
|
|
131
|
-
path aliases or computed (non-string-literal) dynamic imports. On a
|
|
132
|
-
project that relies on aliases, this can miss real edges and undercount a
|
|
133
|
-
file's blast radius.
|
|
134
|
-
`scan` says plainly when this fallback is active — it is never presented
|
|
135
|
-
as equivalent to the TypeScript-accurate graph.
|
|
148
|
+
`scan` finds every source and test file, builds the import graph between them, and lists the source files with the most dependents that no test reaches. It writes the graph to `.veris/graph.json`, a derived cache rebuilt on every run.
|
|
149
|
+
|
|
150
|
+
`scan` always names the resolver that built the graph, because the two differ in accuracy:
|
|
151
|
+
|
|
152
|
+
- **`typescript`** runs when your project has a `tsconfig.json` and a `typescript` package that exposes the classic compiler API. VerisKit loads your project's own TypeScript at run time, so it adds no dependency, and it resolves imports the way `tsc` does: `tsconfig` path aliases, extension-mapped specifiers, index resolution.
|
|
153
|
+
- **`scanner`** is the fallback for a project with no TypeScript, no `tsconfig.json`, or a TypeScript 7.x native build that drops the classic compiler API. The scanner reads relative imports only. It follows `./foo` and `../bar/baz` but does not resolve `tsconfig` path aliases or computed dynamic imports, so on an alias-heavy project it can miss edges. `scan` labels the resolver it used, and VerisKit never treats a scanner graph as equal to the TypeScript one. This is also why graph-based narrowing in `affected` and `watch` runs the full suite in scanner mode.
|
|
136
154
|
|
|
137
155
|
### `veris plan`
|
|
138
156
|
|
|
@@ -141,141 +159,76 @@ veris plan # recommendations from the current graph
|
|
|
141
159
|
veris plan --base main # also factor in changes vs another ref
|
|
142
160
|
```
|
|
143
161
|
|
|
144
|
-
`plan` reads the
|
|
162
|
+
`plan` reads the graph and prioritizes:
|
|
145
163
|
|
|
146
|
-
- the highest-impact untested files to
|
|
147
|
-
|
|
148
|
-
-
|
|
149
|
-
- with `--base <ref>`, which files that changed since that ref are "risky" —
|
|
150
|
-
high blast radius and either untested or actually changed.
|
|
164
|
+
- the highest-impact untested files to cover first (the most dependents, reached by no test)
|
|
165
|
+
- gaps in your setup, such as a missing linter or type-check
|
|
166
|
+
- with `--base <ref>`, the changed files that carry risk: high blast radius, and either untested or freshly changed
|
|
151
167
|
|
|
152
|
-
|
|
153
|
-
generation is a separate, later goal (v0.8), not something v0.3 does.
|
|
168
|
+
`plan` recommends. It never writes or generates test code. Generation is a later goal, not something VerisKit does today.
|
|
154
169
|
|
|
155
|
-
|
|
170
|
+
## Evidence
|
|
156
171
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
API handler, so it can't flag "this endpoint has no test" the way it flags
|
|
160
|
-
"this module has no test." Planned as a v0.3.x follow-up, not v0.3.0.
|
|
161
|
-
- **No test generation.** `plan` recommends what to test; it does not write
|
|
162
|
-
test files. That's v0.8.
|
|
163
|
-
- **Single tsconfig, single root.** Monorepos with multiple `tsconfig.json`
|
|
164
|
-
files aren't modeled yet — resolution runs against the root project only.
|
|
165
|
-
- **Plain JS / TS 7.x-native projects degrade to the scanner.** There's no
|
|
166
|
-
new dependency added to compensate — the classic TypeScript compiler API
|
|
167
|
-
is what makes the accurate resolver possible, and projects without it get
|
|
168
|
-
the honestly-labeled, relative-imports-only fallback described above.
|
|
172
|
+
Every `veris verify` and `veris affected` run leaves a canonical, git-anchored
|
|
173
|
+
evidence record you can check later or hand to someone else as proof.
|
|
169
174
|
|
|
170
|
-
|
|
175
|
+
- `.veris/runs/<run-id>/evidence.json` is the machine-readable record: schema
|
|
176
|
+
`veriskit/evidence@1`, the verdict, per-check results, the environment, the
|
|
177
|
+
git commit and whether the tree was clean, and a sha256 integrity digest over
|
|
178
|
+
the whole record.
|
|
179
|
+
- `.veris/reports/verify-<run-id>.md` is the human-readable report, now showing
|
|
180
|
+
the commit and the digest. Paste it into a PR.
|
|
181
|
+
- Raw per-check logs live under `.veris/runs/<run-id>/`, and the record carries
|
|
182
|
+
a sha256 of each one.
|
|
171
183
|
|
|
172
|
-
|
|
173
|
-
of always running the full set.
|
|
174
|
-
|
|
175
|
-
### `veris affected`
|
|
184
|
+
Check a record:
|
|
176
185
|
|
|
177
186
|
```bash
|
|
178
|
-
veris
|
|
179
|
-
veris affected --base main # checks affected by the diff against another ref (PR/CI)
|
|
187
|
+
veris evidence verify .veris/runs/<run-id>/evidence.json
|
|
180
188
|
```
|
|
181
189
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
| Changed file | Checks run |
|
|
187
|
-
|----------------------|-------------------------------|
|
|
188
|
-
| test file | unit, lint |
|
|
189
|
-
| TypeScript file | types, lint, unit |
|
|
190
|
-
| JavaScript file | lint, unit |
|
|
191
|
-
| config (tsconfig, biome/eslint config, `package.json`, `veris.config.*`) | every available check |
|
|
192
|
-
| docs/assets (`.md`, images, `LICENSE`) | nothing |
|
|
193
|
-
| anything else unrecognized | every available check (safe default) |
|
|
194
|
-
|
|
195
|
-
The table above decides *which check categories* run — that part is still a
|
|
196
|
-
coarse, file-extension-based mapping. **What changed in v0.3 is what happens
|
|
197
|
-
inside the `unit` category.** Instead of running every unit test in the
|
|
198
|
-
project, `affected` builds the same import graph that
|
|
199
|
-
[`veris scan`/`veris plan`](#project-intelligence) use and narrows the unit
|
|
200
|
-
run to only the test files that **transitively import** your changed files.
|
|
201
|
-
|
|
202
|
-
That narrowing is deliberately conservative — it **falls back to running the
|
|
203
|
-
full test suite** rather than ever risk hiding an affected test:
|
|
204
|
-
|
|
205
|
-
- any changed file matches a config/global pattern (`tsconfig*.json`,
|
|
206
|
-
`package.json`, a `*.config.*` or `*.setup.*` file, biome/eslint config, …);
|
|
207
|
-
- a changed file isn't a node in the import graph at all (e.g. it's under an
|
|
208
|
-
ignored directory like `node_modules` or `.veris`, or it's not a
|
|
209
|
-
recognized code extension);
|
|
210
|
-
- no test file transitively reaches any of the changed files (an untested
|
|
211
|
-
change).
|
|
212
|
-
|
|
213
|
-
The output says when a run was narrowed and why it wasn't:
|
|
190
|
+
This recomputes the digest and reports whether the record was edited or
|
|
191
|
+
corrupted since it was written. An integrity digest is not forgery-proof on its
|
|
192
|
+
own. To prove authorship, publish the digest separately (a CI log or PR) or sign
|
|
193
|
+
it. Cryptographic signing is planned for a later release.
|
|
214
194
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
195
|
+
Package a run as a single portable file (the record, its report, and its logs,
|
|
196
|
+
each with a digest, plus a bundle digest over everything):
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
veris evidence bundle # writes .veris/evidence/<run-id>.bundle.json
|
|
200
|
+
veris evidence verify <bundle> # checks the record, every log, and the report
|
|
218
201
|
```
|
|
219
202
|
|
|
220
|
-
|
|
221
|
-
change reruns everything, an unresolved file falls back to full), but it
|
|
222
|
-
never silently skips work it can't prove is safe to skip.
|
|
203
|
+
`veris evidence show` prints the latest record's key facts.
|
|
223
204
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
"Affected checks failed" / "Affected: partial") instead, and any
|
|
227
|
-
available-but-unaffected capability is listed as `skipped — not affected by
|
|
228
|
-
changes` rather than being folded into the pass. If nothing is affected (for
|
|
229
|
-
example you only touched a doc file), veris prints "Nothing affected" and
|
|
230
|
-
exits `0` — but that is explicitly **not** a verified result: no checks ran
|
|
231
|
-
at all.
|
|
205
|
+
Commit `.veris/config.json` and `.veris/.gitignore`. `veris init` keeps `runs/`,
|
|
206
|
+
`reports/`, `cache/`, `graph.json`, and `evidence/` out of your history.
|
|
232
207
|
|
|
233
|
-
|
|
208
|
+
## What VerisKit does not do yet
|
|
234
209
|
|
|
235
|
-
|
|
236
|
-
veris watch # re-run affected checks as files change
|
|
237
|
-
veris watch --poll # use mtime polling instead of native fs.watch
|
|
238
|
-
```
|
|
210
|
+
VerisKit says what it cannot do as plainly as what it can:
|
|
239
211
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
native change events are unreliable, pass `--poll` to fall back to an
|
|
246
|
-
interval-based scan that diffs file mtimes instead.
|
|
247
|
-
|
|
248
|
-
Every tick after the baseline uses the same graph-based `unit` narrowing (and
|
|
249
|
-
conservative full-suite fallback) described above for `affected` — the graph
|
|
250
|
-
is rebuilt fresh each tick, so it always reflects the file you just saved,
|
|
251
|
-
not a stale snapshot.
|
|
252
|
-
|
|
253
|
-
Each tick reprints the full check board. A capability that wasn't affected by
|
|
254
|
-
the latest change keeps showing its last real result, marked `⟳ cached` — a
|
|
255
|
-
cached **failure stays a failure** (✗); it is never hidden or silently
|
|
256
|
-
dropped just because it didn't rerun this tick. Press Ctrl-C to stop; veris
|
|
257
|
-
closes the watcher (or poll loop) cleanly and exits `0`.
|
|
212
|
+
- **No framework route or endpoint detection.** The graph understands imports, not that a file is an Express route or a Next.js page, so it flags an untested module but not an untested endpoint. Planned next.
|
|
213
|
+
- **No test generation.** `plan` tells you what to test. Writing the tests is a later release.
|
|
214
|
+
- **One project root.** A monorepo with several `tsconfig.json` files is not modeled yet. Resolution runs against the root project.
|
|
215
|
+
- **Scanner fallback on plain-JS or TS 7.x-native projects.** The accurate resolver needs the classic TypeScript compiler API. Without it you get the labeled, relative-imports-only graph described above, and no dependency is added to paper over the gap.
|
|
216
|
+
- **No cryptographic signing.** Evidence carries an integrity digest, not a signature. Keyless signing is planned.
|
|
258
217
|
|
|
259
|
-
##
|
|
218
|
+
## Part of Baseframe Labs
|
|
260
219
|
|
|
261
|
-
|
|
220
|
+
VerisKit is one of four developer tools from [Baseframe Labs](https://www.baseframelabs.com), each answering a different question about your work:
|
|
262
221
|
|
|
263
|
-
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
- Raw per-check logs and run metadata under `.veris/runs/<run-id>/`.
|
|
222
|
+
- **[ProjScan](https://www.baseframelabs.com/apps/projscan)** asks: is the repository healthy?
|
|
223
|
+
- **[AgentLoopKit](https://www.baseframelabs.com/apps/agentloopkit)** asks: what should the agent do next?
|
|
224
|
+
- **[AgentFlight](https://www.baseframelabs.com/apps/agentflight)** asks: what did the agent actually do?
|
|
225
|
+
- **VerisKit** asks: can we trust the result?
|
|
268
226
|
|
|
269
|
-
|
|
270
|
-
`.veris/runs/`, `.veris/reports/`, and `.veris/cache/` are gitignored by
|
|
271
|
-
`veris init`. `.veris/graph.json` (written by [`veris scan`](#project-intelligence))
|
|
272
|
-
is a separate derived cache, rebuilt on every scan — treat it the same way
|
|
273
|
-
and don't commit it.
|
|
227
|
+
Each works on its own. VerisKit needs none of the others to verify a change.
|
|
274
228
|
|
|
275
229
|
## Design
|
|
276
230
|
|
|
277
|
-
|
|
278
|
-
spec: [`docs/superpowers/specs/2026-07-08-veris-v0.1-design.md`](docs/superpowers/specs/2026-07-08-veris-v0.1-design.md).
|
|
231
|
+
The design specs, locked decisions, and roadmap live in [`docs/superpowers/specs`](docs/superpowers/specs).
|
|
279
232
|
|
|
280
233
|
## License
|
|
281
234
|
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,20 @@ var __export = (target, all) => {
|
|
|
8
8
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
// src/version.ts
|
|
12
|
+
import { readFileSync } from "fs";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
var pkgUrl, VERSION;
|
|
15
|
+
var init_version = __esm({
|
|
16
|
+
"src/version.ts"() {
|
|
17
|
+
"use strict";
|
|
18
|
+
pkgUrl = new URL("../package.json", import.meta.url);
|
|
19
|
+
VERSION = JSON.parse(
|
|
20
|
+
readFileSync(fileURLToPath(pkgUrl), "utf8")
|
|
21
|
+
).version;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
11
25
|
// src/util/fs-safe.ts
|
|
12
26
|
import { existsSync } from "fs";
|
|
13
27
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
@@ -57,6 +71,7 @@ async function detectProject(root) {
|
|
|
57
71
|
];
|
|
58
72
|
return {
|
|
59
73
|
root,
|
|
74
|
+
name: pkg.name ?? void 0,
|
|
60
75
|
packageManager: detectPackageManager(root),
|
|
61
76
|
frameworks,
|
|
62
77
|
languages,
|
|
@@ -153,7 +168,7 @@ function renderDoctor(project, env) {
|
|
|
153
168
|
const ok = (s) => plain ? s : pc.green(s);
|
|
154
169
|
const dim = (s) => plain ? s : pc.dim(s);
|
|
155
170
|
const lines = [];
|
|
156
|
-
lines.push("
|
|
171
|
+
lines.push("VerisKit doctor");
|
|
157
172
|
lines.push("");
|
|
158
173
|
lines.push(`Package manager ${project.packageManager}`);
|
|
159
174
|
lines.push(`Node ${env.node}`);
|
|
@@ -189,8 +204,80 @@ var init_doctor = __esm({
|
|
|
189
204
|
}
|
|
190
205
|
});
|
|
191
206
|
|
|
207
|
+
// src/evidence/record.ts
|
|
208
|
+
import { createHash } from "crypto";
|
|
209
|
+
import { basename } from "path";
|
|
210
|
+
function canonicalize(value) {
|
|
211
|
+
return JSON.stringify(sortValue(value));
|
|
212
|
+
}
|
|
213
|
+
function sortValue(value) {
|
|
214
|
+
if (Array.isArray(value)) return value.map(sortValue);
|
|
215
|
+
if (value && typeof value === "object") {
|
|
216
|
+
const src = value;
|
|
217
|
+
const out = {};
|
|
218
|
+
for (const key of Object.keys(src).sort()) {
|
|
219
|
+
if (src[key] !== void 0) out[key] = sortValue(src[key]);
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
function sha256(text) {
|
|
226
|
+
return `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`;
|
|
227
|
+
}
|
|
228
|
+
function computeDigest(record) {
|
|
229
|
+
const { digest: _omit, ...rest } = record;
|
|
230
|
+
return sha256(canonicalize(rest));
|
|
231
|
+
}
|
|
232
|
+
function buildRecord(run, git, logDigests, toolVersion) {
|
|
233
|
+
const runnerOf = new Map(
|
|
234
|
+
run.project.capabilities.map((c) => [c.id, c.runner])
|
|
235
|
+
);
|
|
236
|
+
const checks = run.results.map((r) => {
|
|
237
|
+
const check = {
|
|
238
|
+
id: r.checkId,
|
|
239
|
+
status: r.status,
|
|
240
|
+
durationMs: r.durationMs,
|
|
241
|
+
summary: r.summary
|
|
242
|
+
};
|
|
243
|
+
const runner = runnerOf.get(r.checkId);
|
|
244
|
+
if (runner) check.runner = runner;
|
|
245
|
+
if (r.counts) check.counts = r.counts;
|
|
246
|
+
const digest = logDigests[r.checkId];
|
|
247
|
+
if (digest) check.logDigest = digest;
|
|
248
|
+
return check;
|
|
249
|
+
});
|
|
250
|
+
const scope = run.scope ? { kind: run.scope.kind, changedCount: run.scope.changedCount } : { kind: "full", changedCount: 0 };
|
|
251
|
+
const base = {
|
|
252
|
+
schema: EVIDENCE_SCHEMA,
|
|
253
|
+
id: run.id,
|
|
254
|
+
startedAt: run.startedAt,
|
|
255
|
+
tool: { name: "veriskit", version: toolVersion },
|
|
256
|
+
git,
|
|
257
|
+
env: run.env,
|
|
258
|
+
project: {
|
|
259
|
+
name: run.project.name ?? basename(run.project.root),
|
|
260
|
+
packageManager: run.project.packageManager,
|
|
261
|
+
frameworks: run.project.frameworks,
|
|
262
|
+
languages: run.project.languages
|
|
263
|
+
},
|
|
264
|
+
scope,
|
|
265
|
+
checks,
|
|
266
|
+
verdict: run.verdict
|
|
267
|
+
};
|
|
268
|
+
return { ...base, digest: computeDigest(base) };
|
|
269
|
+
}
|
|
270
|
+
var EVIDENCE_SCHEMA;
|
|
271
|
+
var init_record = __esm({
|
|
272
|
+
"src/evidence/record.ts"() {
|
|
273
|
+
"use strict";
|
|
274
|
+
EVIDENCE_SCHEMA = "veriskit/evidence@1";
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
192
278
|
// src/evidence/store.ts
|
|
193
|
-
import {
|
|
279
|
+
import { readdirSync } from "fs";
|
|
280
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
194
281
|
import { join as join2 } from "path";
|
|
195
282
|
function newRunId() {
|
|
196
283
|
counter += 1;
|
|
@@ -207,11 +294,6 @@ async function writeLog(runDir, checkId, content) {
|
|
|
207
294
|
await writeFile2(ref, content, "utf8");
|
|
208
295
|
return ref;
|
|
209
296
|
}
|
|
210
|
-
async function writeMetadata(runDir, run) {
|
|
211
|
-
const ref = join2(runDir, "metadata.json");
|
|
212
|
-
await writeFile2(ref, JSON.stringify(run, null, 2), "utf8");
|
|
213
|
-
return ref;
|
|
214
|
-
}
|
|
215
297
|
async function writeReport(root, id, markdown) {
|
|
216
298
|
const dir = join2(root, ".veris", "reports");
|
|
217
299
|
await ensureDir(dir);
|
|
@@ -219,11 +301,54 @@ async function writeReport(root, id, markdown) {
|
|
|
219
301
|
await writeFile2(ref, markdown, "utf8");
|
|
220
302
|
return ref;
|
|
221
303
|
}
|
|
304
|
+
async function writeEvidence(runDir, record) {
|
|
305
|
+
const ref = join2(runDir, "evidence.json");
|
|
306
|
+
await writeFile2(ref, `${JSON.stringify(record, null, 2)}
|
|
307
|
+
`, "utf8");
|
|
308
|
+
return ref;
|
|
309
|
+
}
|
|
310
|
+
async function digestLogs(run) {
|
|
311
|
+
const out = {};
|
|
312
|
+
for (const r of run.results) {
|
|
313
|
+
if (!r.logRef) continue;
|
|
314
|
+
try {
|
|
315
|
+
out[r.checkId] = sha256(await readFile2(r.logRef, "utf8"));
|
|
316
|
+
} catch {
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
async function readRunLogs(runDir, checkIds) {
|
|
322
|
+
const out = {};
|
|
323
|
+
for (const id of checkIds) {
|
|
324
|
+
try {
|
|
325
|
+
out[id] = await readFile2(join2(runDir, `${id}.log`), "utf8");
|
|
326
|
+
} catch {
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return out;
|
|
330
|
+
}
|
|
331
|
+
function latestRunDir(root) {
|
|
332
|
+
const runs = join2(root, ".veris", "runs");
|
|
333
|
+
try {
|
|
334
|
+
const dirs = readdirSync(runs, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name !== "watch").map((d) => d.name).sort();
|
|
335
|
+
const latest = dirs.at(-1);
|
|
336
|
+
return latest ? join2(runs, latest) : null;
|
|
337
|
+
} catch {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async function ensureEvidenceDir(root) {
|
|
342
|
+
const dir = join2(root, ".veris", "evidence");
|
|
343
|
+
await ensureDir(dir);
|
|
344
|
+
return dir;
|
|
345
|
+
}
|
|
222
346
|
var counter;
|
|
223
347
|
var init_store = __esm({
|
|
224
348
|
"src/evidence/store.ts"() {
|
|
225
349
|
"use strict";
|
|
226
350
|
init_fs_safe();
|
|
351
|
+
init_record();
|
|
227
352
|
counter = 0;
|
|
228
353
|
}
|
|
229
354
|
});
|
|
@@ -611,18 +736,18 @@ function glyph(status, plain) {
|
|
|
611
736
|
function secs(ms) {
|
|
612
737
|
return ms === 0 ? "" : `${(ms / 1e3).toFixed(1)}s`;
|
|
613
738
|
}
|
|
614
|
-
function renderRun(run) {
|
|
739
|
+
function renderRun(run, record) {
|
|
615
740
|
const plain = isPlain();
|
|
616
741
|
const bold = (s) => plain ? s : pc2.bold(s);
|
|
617
742
|
const dim = (s) => plain ? s : pc2.dim(s);
|
|
618
743
|
const lines = [];
|
|
619
744
|
const scoped = run.scope?.kind;
|
|
620
745
|
if (scoped) {
|
|
621
|
-
lines.push(bold(`
|
|
746
|
+
lines.push(bold(`VerisKit \u2014 ${scoped}`));
|
|
622
747
|
lines.push("");
|
|
623
748
|
lines.push(`Scope ${run.scope?.changedCount ?? 0} changed file(s)`);
|
|
624
749
|
} else {
|
|
625
|
-
lines.push(bold("
|
|
750
|
+
lines.push(bold("VerisKit"));
|
|
626
751
|
lines.push("");
|
|
627
752
|
lines.push(
|
|
628
753
|
`Project ${run.project.root.split("/").pop() ?? run.project.root}`
|
|
@@ -646,6 +771,13 @@ function renderRun(run) {
|
|
|
646
771
|
const label = run.verdict.state === "verified" ? scoped ? "\u2713 Affected checks passed" : "\u2713 Verified" : run.verdict.state === "failed" ? scoped ? "\u2717 Affected checks failed" : "\u2717 Failed" : scoped ? "\u25B2 Affected: partial" : "\u25B2 Partial";
|
|
647
772
|
const color = run.verdict.state === "verified" ? pc2.green : run.verdict.state === "failed" ? pc2.red : pc2.yellow;
|
|
648
773
|
lines.push(` ${plain ? label : color(label)}`);
|
|
774
|
+
if (record?.git) {
|
|
775
|
+
const g = record.git;
|
|
776
|
+
lines.push("");
|
|
777
|
+
lines.push(
|
|
778
|
+
`Commit ${g.commit.slice(0, 7)} ${g.dirty ? "\xB7 tree dirty" : "\xB7 tree clean"}`
|
|
779
|
+
);
|
|
780
|
+
}
|
|
649
781
|
if (run.reportRef) {
|
|
650
782
|
lines.push("");
|
|
651
783
|
lines.push("Report");
|
|
@@ -701,7 +833,7 @@ async function runInit(root) {
|
|
|
701
833
|
await writeIfAbsent(join4(dir, ".gitignore"), GITIGNORE);
|
|
702
834
|
process.stdout.write(
|
|
703
835
|
wroteConfig ? `Veris initialized. Detected checks: ${defaultChecks.join(", ") || "none"}.
|
|
704
|
-
` : "
|
|
836
|
+
` : "VerisKit already initialized (.veris/config.json exists). Nothing overwritten.\n"
|
|
705
837
|
);
|
|
706
838
|
return 0;
|
|
707
839
|
}
|
|
@@ -711,7 +843,14 @@ var init_init = __esm({
|
|
|
711
843
|
"use strict";
|
|
712
844
|
init_detect();
|
|
713
845
|
init_fs_safe();
|
|
714
|
-
GITIGNORE = [
|
|
846
|
+
GITIGNORE = [
|
|
847
|
+
"runs/",
|
|
848
|
+
"reports/",
|
|
849
|
+
"cache/",
|
|
850
|
+
"graph.json",
|
|
851
|
+
"evidence/",
|
|
852
|
+
""
|
|
853
|
+
].join("\n");
|
|
715
854
|
}
|
|
716
855
|
});
|
|
717
856
|
|
|
@@ -727,15 +866,60 @@ var init_load = __esm({
|
|
|
727
866
|
}
|
|
728
867
|
});
|
|
729
868
|
|
|
869
|
+
// src/git/changes.ts
|
|
870
|
+
function collect(set, out) {
|
|
871
|
+
for (const line of out.split("\n")) {
|
|
872
|
+
const f = line.trim();
|
|
873
|
+
if (f) set.add(f);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
async function changedFiles(root, opts = {}) {
|
|
877
|
+
const base = opts.base ?? null;
|
|
878
|
+
const files = /* @__PURE__ */ new Set();
|
|
879
|
+
const diffArgs = base ? ["diff", "--name-only", base] : ["diff", "--name-only", "HEAD"];
|
|
880
|
+
const diff = await exec("git", diffArgs, { cwd: root });
|
|
881
|
+
if (diff.code === 0) collect(files, diff.stdout);
|
|
882
|
+
const untracked = await exec(
|
|
883
|
+
"git",
|
|
884
|
+
["ls-files", "--others", "--exclude-standard"],
|
|
885
|
+
{ cwd: root }
|
|
886
|
+
);
|
|
887
|
+
if (untracked.code === 0) collect(files, untracked.stdout);
|
|
888
|
+
return { files: [...files].sort(), base };
|
|
889
|
+
}
|
|
890
|
+
async function gitAnchor(root) {
|
|
891
|
+
const head = await exec("git", ["rev-parse", "HEAD"], { cwd: root });
|
|
892
|
+
if (head.code !== 0) return null;
|
|
893
|
+
const commit = head.stdout.trim();
|
|
894
|
+
const branchRes = await exec("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
895
|
+
cwd: root
|
|
896
|
+
});
|
|
897
|
+
const branch = branchRes.code === 0 ? branchRes.stdout.trim() : "HEAD";
|
|
898
|
+
const status = await exec("git", ["status", "--porcelain"], { cwd: root });
|
|
899
|
+
const lines = status.code === 0 ? status.stdout.split("\n").map((l) => l.trim()).filter(Boolean) : [];
|
|
900
|
+
return {
|
|
901
|
+
commit,
|
|
902
|
+
branch,
|
|
903
|
+
dirty: lines.length > 0,
|
|
904
|
+
changedFiles: lines.length
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
var init_changes = __esm({
|
|
908
|
+
"src/git/changes.ts"() {
|
|
909
|
+
"use strict";
|
|
910
|
+
init_exec();
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
|
|
730
914
|
// src/reporters/markdown.ts
|
|
731
915
|
import { relative } from "path";
|
|
732
916
|
function cell(value) {
|
|
733
917
|
return value.replace(/\|/g, "\\|");
|
|
734
918
|
}
|
|
735
|
-
function renderMarkdown(run) {
|
|
919
|
+
function renderMarkdown(run, record) {
|
|
736
920
|
const root = run.project.root;
|
|
737
921
|
const lines = [];
|
|
738
|
-
lines.push("#
|
|
922
|
+
lines.push("# VerisKit Verification Report");
|
|
739
923
|
if (run.scope?.kind) {
|
|
740
924
|
lines.push("");
|
|
741
925
|
lines.push(
|
|
@@ -748,6 +932,13 @@ function renderMarkdown(run) {
|
|
|
748
932
|
lines.push(
|
|
749
933
|
`**Node:** ${run.env.node} \xB7 **OS:** ${run.env.os} \xB7 **PM:** ${run.env.pm} \xB7 **CI:** ${run.env.ci}`
|
|
750
934
|
);
|
|
935
|
+
if (record?.git) {
|
|
936
|
+
const g = record.git;
|
|
937
|
+
const tree = g.dirty ? `tree dirty, ${g.changedFiles} uncommitted file(s)` : "tree clean";
|
|
938
|
+
lines.push(`**Commit:** ${g.commit.slice(0, 7)} (${g.branch}) \xB7 ${tree}`);
|
|
939
|
+
} else if (record) {
|
|
940
|
+
lines.push("**Commit:** no git anchor available");
|
|
941
|
+
}
|
|
751
942
|
lines.push("");
|
|
752
943
|
lines.push("## Checks");
|
|
753
944
|
lines.push("");
|
|
@@ -779,9 +970,16 @@ function renderMarkdown(run) {
|
|
|
779
970
|
lines.push("```");
|
|
780
971
|
}
|
|
781
972
|
}
|
|
973
|
+
if (record) {
|
|
974
|
+
lines.push("");
|
|
975
|
+
lines.push(`**Evidence digest:** \`${record.digest}\``);
|
|
976
|
+
lines.push(
|
|
977
|
+
"_Integrity digest over the canonical record. Detects edits and corruption; it is not forgery-proof on its own. Publish the digest separately or sign it to prove authorship._"
|
|
978
|
+
);
|
|
979
|
+
}
|
|
782
980
|
lines.push("");
|
|
783
981
|
lines.push(
|
|
784
|
-
"_Generated by
|
|
982
|
+
"_Generated by VerisKit. A `Partial` verdict means one or more capabilities did not run \u2014 it is not a pass._"
|
|
785
983
|
);
|
|
786
984
|
lines.push("");
|
|
787
985
|
return lines.join("\n");
|
|
@@ -808,11 +1006,18 @@ async function runVerify(root, opts = {}) {
|
|
|
808
1006
|
const config = await loadConfig(root);
|
|
809
1007
|
const checks = config?.checks?.length ? config.checks : DEFAULT_CHECKS;
|
|
810
1008
|
const run = await runChecks(project, checks, root);
|
|
811
|
-
const
|
|
1009
|
+
const git = await gitAnchor(root);
|
|
1010
|
+
const logDigests = await digestLogs(run);
|
|
1011
|
+
const record = buildRecord(run, git, logDigests, VERSION);
|
|
1012
|
+
const reportRef = await writeReport(
|
|
1013
|
+
root,
|
|
1014
|
+
run.id,
|
|
1015
|
+
renderMarkdown(run, record)
|
|
1016
|
+
);
|
|
812
1017
|
run.reportRef = reportRef;
|
|
813
1018
|
const runDir = await createRunDir(root, run.id);
|
|
814
|
-
await
|
|
815
|
-
process.stdout.write(`${renderRun(run)}
|
|
1019
|
+
await writeEvidence(runDir, record);
|
|
1020
|
+
process.stdout.write(`${renderRun(run, record)}
|
|
816
1021
|
`);
|
|
817
1022
|
return verdictExitCode(run.verdict, opts);
|
|
818
1023
|
}
|
|
@@ -824,9 +1029,12 @@ var init_verify = __esm({
|
|
|
824
1029
|
init_load();
|
|
825
1030
|
init_orchestrator();
|
|
826
1031
|
init_verdict();
|
|
1032
|
+
init_record();
|
|
827
1033
|
init_store();
|
|
1034
|
+
init_changes();
|
|
828
1035
|
init_markdown();
|
|
829
1036
|
init_terminal();
|
|
1037
|
+
init_version();
|
|
830
1038
|
DEFAULT_CHECKS = ["types", "lint", "unit"];
|
|
831
1039
|
}
|
|
832
1040
|
});
|
|
@@ -836,13 +1044,13 @@ var report_exports = {};
|
|
|
836
1044
|
__export(report_exports, {
|
|
837
1045
|
runReport: () => runReport
|
|
838
1046
|
});
|
|
839
|
-
import { readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
1047
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
|
|
840
1048
|
import { join as join6 } from "path";
|
|
841
1049
|
async function runReport(root) {
|
|
842
1050
|
const dir = join6(root, ".veris", "reports");
|
|
843
1051
|
let files;
|
|
844
1052
|
try {
|
|
845
|
-
files =
|
|
1053
|
+
files = readdirSync2(dir).filter((f) => f.endsWith(".md")).sort();
|
|
846
1054
|
} catch {
|
|
847
1055
|
process.stdout.write("No reports yet. Run `veris verify` first.\n");
|
|
848
1056
|
return 0;
|
|
@@ -915,36 +1123,8 @@ var init_gate = __esm({
|
|
|
915
1123
|
}
|
|
916
1124
|
});
|
|
917
1125
|
|
|
918
|
-
// src/git/changes.ts
|
|
919
|
-
function collect(set, out) {
|
|
920
|
-
for (const line of out.split("\n")) {
|
|
921
|
-
const f = line.trim();
|
|
922
|
-
if (f) set.add(f);
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
async function changedFiles(root, opts = {}) {
|
|
926
|
-
const base = opts.base ?? null;
|
|
927
|
-
const files = /* @__PURE__ */ new Set();
|
|
928
|
-
const diffArgs = base ? ["diff", "--name-only", base] : ["diff", "--name-only", "HEAD"];
|
|
929
|
-
const diff = await exec("git", diffArgs, { cwd: root });
|
|
930
|
-
if (diff.code === 0) collect(files, diff.stdout);
|
|
931
|
-
const untracked = await exec(
|
|
932
|
-
"git",
|
|
933
|
-
["ls-files", "--others", "--exclude-standard"],
|
|
934
|
-
{ cwd: root }
|
|
935
|
-
);
|
|
936
|
-
if (untracked.code === 0) collect(files, untracked.stdout);
|
|
937
|
-
return { files: [...files].sort(), base };
|
|
938
|
-
}
|
|
939
|
-
var init_changes = __esm({
|
|
940
|
-
"src/git/changes.ts"() {
|
|
941
|
-
"use strict";
|
|
942
|
-
init_exec();
|
|
943
|
-
}
|
|
944
|
-
});
|
|
945
|
-
|
|
946
1126
|
// src/project-graph/discover.ts
|
|
947
|
-
import { readdirSync as
|
|
1127
|
+
import { readdirSync as readdirSync3, statSync } from "fs";
|
|
948
1128
|
import { join as join7, relative as relative2, sep } from "path";
|
|
949
1129
|
function toPosix(p) {
|
|
950
1130
|
return sep === "/" ? p : p.split(sep).join("/");
|
|
@@ -959,7 +1139,7 @@ function discoverFiles(root) {
|
|
|
959
1139
|
const walk = (dir) => {
|
|
960
1140
|
let entries;
|
|
961
1141
|
try {
|
|
962
|
-
entries =
|
|
1142
|
+
entries = readdirSync3(dir);
|
|
963
1143
|
} catch {
|
|
964
1144
|
return;
|
|
965
1145
|
}
|
|
@@ -1322,11 +1502,18 @@ async function runAffected(root, opts = {}) {
|
|
|
1322
1502
|
});
|
|
1323
1503
|
}
|
|
1324
1504
|
}
|
|
1325
|
-
const
|
|
1505
|
+
const git = await gitAnchor(root);
|
|
1506
|
+
const logDigests = await digestLogs(run);
|
|
1507
|
+
const record = buildRecord(run, git, logDigests, VERSION);
|
|
1508
|
+
const reportRef = await writeReport(
|
|
1509
|
+
root,
|
|
1510
|
+
run.id,
|
|
1511
|
+
renderMarkdown(run, record)
|
|
1512
|
+
);
|
|
1326
1513
|
run.reportRef = reportRef;
|
|
1327
1514
|
const runDir = await createRunDir(root, run.id);
|
|
1328
|
-
await
|
|
1329
|
-
process.stdout.write(`${renderRun(run)}
|
|
1515
|
+
await writeEvidence(runDir, record);
|
|
1516
|
+
process.stdout.write(`${renderRun(run, record)}
|
|
1330
1517
|
`);
|
|
1331
1518
|
if (narrowedNote) process.stdout.write(`${narrowedNote}
|
|
1332
1519
|
`);
|
|
@@ -1339,23 +1526,25 @@ var init_affected = __esm({
|
|
|
1339
1526
|
init_detect();
|
|
1340
1527
|
init_orchestrator();
|
|
1341
1528
|
init_verdict();
|
|
1529
|
+
init_record();
|
|
1342
1530
|
init_store();
|
|
1343
1531
|
init_changes();
|
|
1344
1532
|
init_markdown();
|
|
1345
1533
|
init_terminal();
|
|
1534
|
+
init_version();
|
|
1346
1535
|
}
|
|
1347
1536
|
});
|
|
1348
1537
|
|
|
1349
1538
|
// src/watch/watcher.ts
|
|
1350
1539
|
import {
|
|
1351
1540
|
watch as fsWatch,
|
|
1352
|
-
readdirSync as
|
|
1541
|
+
readdirSync as readdirSync4,
|
|
1353
1542
|
statSync as statSync3
|
|
1354
1543
|
} from "fs";
|
|
1355
|
-
import { basename, join as join10, relative as relative5 } from "path";
|
|
1544
|
+
import { basename as basename2, join as join10, relative as relative5 } from "path";
|
|
1356
1545
|
function watch(root, opts, onBatch) {
|
|
1357
1546
|
const debounceMs = opts.debounceMs ?? 150;
|
|
1358
|
-
const rootBase =
|
|
1547
|
+
const rootBase = basename2(root);
|
|
1359
1548
|
let pending = /* @__PURE__ */ new Set();
|
|
1360
1549
|
let timer = null;
|
|
1361
1550
|
const flush = () => {
|
|
@@ -1406,7 +1595,7 @@ function scanMtimes(root) {
|
|
|
1406
1595
|
const walk = (dir) => {
|
|
1407
1596
|
let entries;
|
|
1408
1597
|
try {
|
|
1409
|
-
entries =
|
|
1598
|
+
entries = readdirSync4(dir);
|
|
1410
1599
|
} catch {
|
|
1411
1600
|
return;
|
|
1412
1601
|
}
|
|
@@ -1582,7 +1771,7 @@ function renderScan(graph, analysis) {
|
|
|
1582
1771
|
const dim = (s) => plain ? s : pc3.dim(s);
|
|
1583
1772
|
const warn = (s) => plain ? s : pc3.yellow(s);
|
|
1584
1773
|
const lines = [];
|
|
1585
|
-
lines.push(bold("
|
|
1774
|
+
lines.push(bold("VerisKit \u2014 scan"));
|
|
1586
1775
|
lines.push("");
|
|
1587
1776
|
lines.push(
|
|
1588
1777
|
`Resolver ${graph.resolver}${graph.resolver === "scanner" ? dim(" (no TypeScript found \u2014 relative imports only)") : ""}`
|
|
@@ -1644,7 +1833,7 @@ function renderPlan(project, graph, analysis, changed) {
|
|
|
1644
1833
|
const dim = (s) => plain ? s : pc4.dim(s);
|
|
1645
1834
|
const warn = (s) => plain ? s : pc4.yellow(s);
|
|
1646
1835
|
const lines = [];
|
|
1647
|
-
lines.push(bold("
|
|
1836
|
+
lines.push(bold("VerisKit \u2014 plan"));
|
|
1648
1837
|
lines.push(dim(`(graph via ${graph.resolver})`));
|
|
1649
1838
|
lines.push("");
|
|
1650
1839
|
lines.push("Test these first (high impact, untested)");
|
|
@@ -1708,21 +1897,223 @@ var init_plan = __esm({
|
|
|
1708
1897
|
}
|
|
1709
1898
|
});
|
|
1710
1899
|
|
|
1900
|
+
// src/evidence/verify-evidence.ts
|
|
1901
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1902
|
+
function verifyRecord(record) {
|
|
1903
|
+
const recomputed = computeDigest(
|
|
1904
|
+
record
|
|
1905
|
+
);
|
|
1906
|
+
const ok = recomputed === record.digest;
|
|
1907
|
+
const checks = [
|
|
1908
|
+
{
|
|
1909
|
+
name: "record digest",
|
|
1910
|
+
ok,
|
|
1911
|
+
detail: ok ? "matches the canonical record" : `recomputed ${recomputed}, record claims ${record.digest}`
|
|
1912
|
+
}
|
|
1913
|
+
];
|
|
1914
|
+
return { ok, kind: "record", record, checks };
|
|
1915
|
+
}
|
|
1916
|
+
async function verifyEvidenceFile(path) {
|
|
1917
|
+
const parsed = JSON.parse(await readFile3(path, "utf8"));
|
|
1918
|
+
if (parsed?.schema === "veriskit/bundle@1") {
|
|
1919
|
+
const { verifyBundle: verifyBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
|
|
1920
|
+
return verifyBundle2(parsed);
|
|
1921
|
+
}
|
|
1922
|
+
return verifyRecord(parsed);
|
|
1923
|
+
}
|
|
1924
|
+
var init_verify_evidence = __esm({
|
|
1925
|
+
"src/evidence/verify-evidence.ts"() {
|
|
1926
|
+
"use strict";
|
|
1927
|
+
init_record();
|
|
1928
|
+
}
|
|
1929
|
+
});
|
|
1930
|
+
|
|
1931
|
+
// src/evidence/bundle.ts
|
|
1932
|
+
var bundle_exports = {};
|
|
1933
|
+
__export(bundle_exports, {
|
|
1934
|
+
BUNDLE_SCHEMA: () => BUNDLE_SCHEMA,
|
|
1935
|
+
buildBundle: () => buildBundle,
|
|
1936
|
+
verifyBundle: () => verifyBundle
|
|
1937
|
+
});
|
|
1938
|
+
function buildBundle(record, report, logs) {
|
|
1939
|
+
const manifest = {
|
|
1940
|
+
record: record.digest,
|
|
1941
|
+
report: sha256(report),
|
|
1942
|
+
logs: Object.fromEntries(
|
|
1943
|
+
Object.entries(logs).map(([k, v]) => [k, sha256(v)])
|
|
1944
|
+
)
|
|
1945
|
+
};
|
|
1946
|
+
const base = { schema: BUNDLE_SCHEMA, record, report, logs, manifest };
|
|
1947
|
+
return { ...base, bundleDigest: sha256(canonicalize(base)) };
|
|
1948
|
+
}
|
|
1949
|
+
function verifyBundle(bundle) {
|
|
1950
|
+
const checks = [];
|
|
1951
|
+
const recordResult = verifyRecord(bundle.record);
|
|
1952
|
+
checks.push(...recordResult.checks);
|
|
1953
|
+
const reportDigest = sha256(bundle.report);
|
|
1954
|
+
checks.push({
|
|
1955
|
+
name: "report",
|
|
1956
|
+
ok: reportDigest === bundle.manifest.report,
|
|
1957
|
+
detail: reportDigest === bundle.manifest.report ? "matches manifest" : "mismatch"
|
|
1958
|
+
});
|
|
1959
|
+
for (const [id, body] of Object.entries(bundle.logs)) {
|
|
1960
|
+
const d = sha256(body);
|
|
1961
|
+
checks.push({
|
|
1962
|
+
name: `log:${id}`,
|
|
1963
|
+
ok: d === bundle.manifest.logs[id],
|
|
1964
|
+
detail: d === bundle.manifest.logs[id] ? "matches manifest" : "mismatch"
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
const { bundleDigest: _omit, ...rest } = bundle;
|
|
1968
|
+
const recomputed = sha256(canonicalize(rest));
|
|
1969
|
+
checks.push({
|
|
1970
|
+
name: "bundle digest",
|
|
1971
|
+
ok: recomputed === bundle.bundleDigest,
|
|
1972
|
+
detail: recomputed === bundle.bundleDigest ? "matches" : "mismatch"
|
|
1973
|
+
});
|
|
1974
|
+
return {
|
|
1975
|
+
ok: checks.every((c) => c.ok),
|
|
1976
|
+
kind: "bundle",
|
|
1977
|
+
record: bundle.record,
|
|
1978
|
+
checks
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
var BUNDLE_SCHEMA;
|
|
1982
|
+
var init_bundle = __esm({
|
|
1983
|
+
"src/evidence/bundle.ts"() {
|
|
1984
|
+
"use strict";
|
|
1985
|
+
init_record();
|
|
1986
|
+
init_verify_evidence();
|
|
1987
|
+
BUNDLE_SCHEMA = "veriskit/bundle@1";
|
|
1988
|
+
}
|
|
1989
|
+
});
|
|
1990
|
+
|
|
1991
|
+
// src/cli/commands/evidence.ts
|
|
1992
|
+
var evidence_exports = {};
|
|
1993
|
+
__export(evidence_exports, {
|
|
1994
|
+
runEvidenceBundle: () => runEvidenceBundle,
|
|
1995
|
+
runEvidenceShow: () => runEvidenceShow,
|
|
1996
|
+
runEvidenceVerify: () => runEvidenceVerify
|
|
1997
|
+
});
|
|
1998
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
1999
|
+
import { writeFile as writeFile4 } from "fs/promises";
|
|
2000
|
+
import { basename as basename3, join as join12 } from "path";
|
|
2001
|
+
import pc5 from "picocolors";
|
|
2002
|
+
async function runEvidenceVerify(path) {
|
|
2003
|
+
let result;
|
|
2004
|
+
try {
|
|
2005
|
+
result = await verifyEvidenceFile(path);
|
|
2006
|
+
} catch (err) {
|
|
2007
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2008
|
+
process.stderr.write(`veris: cannot read evidence at ${path}: ${msg}
|
|
2009
|
+
`);
|
|
2010
|
+
return 1;
|
|
2011
|
+
}
|
|
2012
|
+
const plain = isPlain();
|
|
2013
|
+
const mark = (ok) => plain ? ok ? "ok" : "FAIL" : ok ? pc5.green("\u2713") : pc5.red("\u2717");
|
|
2014
|
+
for (const check of result.checks) {
|
|
2015
|
+
process.stdout.write(
|
|
2016
|
+
` ${mark(check.ok)} ${check.name}: ${check.detail}
|
|
2017
|
+
`
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
const g = result.record.git;
|
|
2021
|
+
const anchor = g ? `commit ${g.commit.slice(0, 7)} \xB7 ${g.dirty ? "tree dirty" : "tree clean"}` : "no git anchor";
|
|
2022
|
+
process.stdout.write(
|
|
2023
|
+
`
|
|
2024
|
+
${result.ok ? "digest OK" : "TAMPERED"} \xB7 verdict ${result.record.verdict.state} \xB7 ${anchor}
|
|
2025
|
+
`
|
|
2026
|
+
);
|
|
2027
|
+
process.stdout.write(`
|
|
2028
|
+
${HONESTY}
|
|
2029
|
+
`);
|
|
2030
|
+
return result.ok ? 0 : 1;
|
|
2031
|
+
}
|
|
2032
|
+
async function runEvidenceBundle(root, opts = {}) {
|
|
2033
|
+
const runDir = latestRunDir(root);
|
|
2034
|
+
if (!runDir) {
|
|
2035
|
+
process.stdout.write("No evidence yet. Run `veris verify` first.\n");
|
|
2036
|
+
return 1;
|
|
2037
|
+
}
|
|
2038
|
+
let record;
|
|
2039
|
+
try {
|
|
2040
|
+
record = JSON.parse(
|
|
2041
|
+
readFileSync5(join12(runDir, "evidence.json"), "utf8")
|
|
2042
|
+
);
|
|
2043
|
+
} catch {
|
|
2044
|
+
process.stderr.write(`veris: no evidence.json in ${runDir}
|
|
2045
|
+
`);
|
|
2046
|
+
return 1;
|
|
2047
|
+
}
|
|
2048
|
+
let report = "";
|
|
2049
|
+
try {
|
|
2050
|
+
report = readFileSync5(
|
|
2051
|
+
join12(root, ".veris", "reports", `verify-${record.id}.md`),
|
|
2052
|
+
"utf8"
|
|
2053
|
+
);
|
|
2054
|
+
} catch {
|
|
2055
|
+
}
|
|
2056
|
+
const logIds = record.checks.filter((c) => c.logDigest).map((c) => c.id);
|
|
2057
|
+
const logs = await readRunLogs(runDir, logIds);
|
|
2058
|
+
const bundle = buildBundle(record, report, logs);
|
|
2059
|
+
const outDir = await ensureEvidenceDir(root);
|
|
2060
|
+
const out = opts.out ?? join12(outDir, `${record.id}.bundle.json`);
|
|
2061
|
+
await writeFile4(out, `${JSON.stringify(bundle, null, 2)}
|
|
2062
|
+
`, "utf8");
|
|
2063
|
+
process.stdout.write(`Wrote portable evidence bundle: ${out}
|
|
2064
|
+
`);
|
|
2065
|
+
return 0;
|
|
2066
|
+
}
|
|
2067
|
+
async function runEvidenceShow(root, path) {
|
|
2068
|
+
const target = path ?? (() => {
|
|
2069
|
+
const dir = latestRunDir(root);
|
|
2070
|
+
return dir ? join12(dir, "evidence.json") : null;
|
|
2071
|
+
})();
|
|
2072
|
+
if (!target) {
|
|
2073
|
+
process.stdout.write("No evidence yet. Run `veris verify` first.\n");
|
|
2074
|
+
return 0;
|
|
2075
|
+
}
|
|
2076
|
+
let record;
|
|
2077
|
+
try {
|
|
2078
|
+
record = JSON.parse(readFileSync5(target, "utf8"));
|
|
2079
|
+
} catch {
|
|
2080
|
+
process.stderr.write(`veris: cannot read evidence at ${target}
|
|
2081
|
+
`);
|
|
2082
|
+
return 1;
|
|
2083
|
+
}
|
|
2084
|
+
const g = record.git;
|
|
2085
|
+
process.stdout.write(
|
|
2086
|
+
[
|
|
2087
|
+
`Evidence ${basename3(target)}`,
|
|
2088
|
+
`Verdict ${record.verdict.state}`,
|
|
2089
|
+
`Project ${record.project.name}`,
|
|
2090
|
+
`Scope ${record.scope.kind} (${record.scope.changedCount} changed)`,
|
|
2091
|
+
`Commit ${g ? `${g.commit.slice(0, 7)} (${g.branch}) ${g.dirty ? "\xB7 tree dirty" : "\xB7 tree clean"}` : "no git anchor"}`,
|
|
2092
|
+
`Checks ${record.checks.map((c) => `${c.id}:${c.status}`).join(", ")}`,
|
|
2093
|
+
`Digest ${record.digest}`,
|
|
2094
|
+
""
|
|
2095
|
+
].join("\n")
|
|
2096
|
+
);
|
|
2097
|
+
return 0;
|
|
2098
|
+
}
|
|
2099
|
+
var HONESTY;
|
|
2100
|
+
var init_evidence = __esm({
|
|
2101
|
+
"src/cli/commands/evidence.ts"() {
|
|
2102
|
+
"use strict";
|
|
2103
|
+
init_bundle();
|
|
2104
|
+
init_store();
|
|
2105
|
+
init_verify_evidence();
|
|
2106
|
+
init_tty();
|
|
2107
|
+
HONESTY = "An integrity digest confirms the record was not edited or corrupted since it was written.\nIt is not forgery-proof on its own: publish the digest separately (CI log, PR) or sign it (planned) to prove authorship.";
|
|
2108
|
+
}
|
|
2109
|
+
});
|
|
2110
|
+
|
|
1711
2111
|
// src/cli/index.ts
|
|
2112
|
+
init_version();
|
|
1712
2113
|
import { realpathSync } from "fs";
|
|
1713
2114
|
import { argv } from "process";
|
|
1714
2115
|
import { pathToFileURL } from "url";
|
|
1715
2116
|
import cac from "cac";
|
|
1716
|
-
|
|
1717
|
-
// src/version.ts
|
|
1718
|
-
import { readFileSync } from "fs";
|
|
1719
|
-
import { fileURLToPath } from "url";
|
|
1720
|
-
var pkgUrl = new URL("../package.json", import.meta.url);
|
|
1721
|
-
var VERSION = JSON.parse(
|
|
1722
|
-
readFileSync(fileURLToPath(pkgUrl), "utf8")
|
|
1723
|
-
).version;
|
|
1724
|
-
|
|
1725
|
-
// src/cli/index.ts
|
|
1726
2117
|
function buildCli() {
|
|
1727
2118
|
const cli = cac("veris");
|
|
1728
2119
|
cli.version(VERSION);
|
|
@@ -1771,6 +2162,34 @@ function buildCli() {
|
|
|
1771
2162
|
const { runPlan: runPlan2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
|
|
1772
2163
|
process.exitCode = await runPlan2(process.cwd(), { base: opts.base });
|
|
1773
2164
|
});
|
|
2165
|
+
cli.command(
|
|
2166
|
+
"evidence <action> [file]",
|
|
2167
|
+
"Evidence tools: verify <file> | bundle | show [file]"
|
|
2168
|
+
).option("--out <file>", "For bundle: write to a specific path").action(
|
|
2169
|
+
async (action, file, opts) => {
|
|
2170
|
+
const mod = await Promise.resolve().then(() => (init_evidence(), evidence_exports));
|
|
2171
|
+
if (action === "verify") {
|
|
2172
|
+
if (!file) {
|
|
2173
|
+
process.stderr.write("veris: evidence verify needs a <file>\n");
|
|
2174
|
+
process.exitCode = 1;
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
process.exitCode = await mod.runEvidenceVerify(file);
|
|
2178
|
+
} else if (action === "bundle") {
|
|
2179
|
+
process.exitCode = await mod.runEvidenceBundle(process.cwd(), {
|
|
2180
|
+
out: opts.out
|
|
2181
|
+
});
|
|
2182
|
+
} else if (action === "show") {
|
|
2183
|
+
process.exitCode = await mod.runEvidenceShow(process.cwd(), file);
|
|
2184
|
+
} else {
|
|
2185
|
+
process.stderr.write(
|
|
2186
|
+
`veris: unknown evidence action '${action}' (use verify | bundle | show)
|
|
2187
|
+
`
|
|
2188
|
+
);
|
|
2189
|
+
process.exitCode = 1;
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
);
|
|
1774
2193
|
return { raw: cli, version: VERSION };
|
|
1775
2194
|
}
|
|
1776
2195
|
async function main(argv2) {
|