veriskit 0.2.0 → 0.3.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 +9 -0
- package/README.md +108 -7
- package/dist/index.js +555 -26
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 — 2026-07-10
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `veris scan` — import-graph map + untested areas, built from the project's own TypeScript (with a dep-free scanner fallback); writes `.veris/graph.json`.
|
|
7
|
+
- `veris plan` — prioritized recommendations (high-impact untested files, weak verification, risky changes). Analysis only — no code generation.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- `veris affected` / `veris watch` are now graph-based: the unit run is narrowed to only the test files that transitively import your changes, with a conservative full-suite fallback (config/global change, unresolved file, or untested change) so an affected test is never skipped. No new runtime dependencies.
|
|
11
|
+
|
|
3
12
|
## 0.2.0 — 2026-07-09
|
|
4
13
|
|
|
5
14
|
### Added
|
package/README.md
CHANGED
|
@@ -95,6 +95,78 @@ want partial results to pass CI can opt in explicitly with
|
|
|
95
95
|
| failed | `1` | at least one check failed |
|
|
96
96
|
| partial | `2` (`0` with `--partial-ok`) | no failures, but something was skipped |
|
|
97
97
|
|
|
98
|
+
## Project intelligence
|
|
99
|
+
|
|
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.
|
|
104
|
+
|
|
105
|
+
### `veris scan`
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
veris scan
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`scan` discovers every source and test file in the project, builds an import
|
|
112
|
+
graph between them, and reports the source files with the most dependents
|
|
113
|
+
that no test transitively reaches ("untested, by impact"). It writes the
|
|
114
|
+
graph to `.veris/graph.json` — a derived cache, rebuilt on every run and not
|
|
115
|
+
meant to be committed.
|
|
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.
|
|
136
|
+
|
|
137
|
+
### `veris plan`
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
veris plan # recommendations from the current graph
|
|
141
|
+
veris plan --base main # also factor in changes vs another ref
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`plan` reads the same graph and turns it into prioritized recommendations:
|
|
145
|
+
|
|
146
|
+
- the highest-impact untested files to test first (most dependents, no test
|
|
147
|
+
reaches them);
|
|
148
|
+
- gaps in your verification setup (e.g. no lint or type-check configured);
|
|
149
|
+
- with `--base <ref>`, which files that changed since that ref are "risky" —
|
|
150
|
+
high blast radius and either untested or actually changed.
|
|
151
|
+
|
|
152
|
+
**`plan` only recommends — it never generates or writes any code.** Test
|
|
153
|
+
generation is a separate, later goal (v0.8), not something v0.3 does.
|
|
154
|
+
|
|
155
|
+
### What's still deferred
|
|
156
|
+
|
|
157
|
+
- **No framework route/endpoint detection.** The graph understands imports
|
|
158
|
+
only; it doesn't know a file is an Express route, a Next.js page, or an
|
|
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.
|
|
169
|
+
|
|
98
170
|
## Developer loop
|
|
99
171
|
|
|
100
172
|
For fast local iteration, veris can scope checks to what you changed instead
|
|
@@ -120,12 +192,34 @@ changed file to the check categories it plausibly touches:
|
|
|
120
192
|
| docs/assets (`.md`, images, `LICENSE`) | nothing |
|
|
121
193
|
| anything else unrecognized | every available check (safe default) |
|
|
122
194
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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:
|
|
214
|
+
|
|
215
|
+
```text
|
|
216
|
+
unit narrowed to 3 of 41 test file(s) via typescript graph
|
|
217
|
+
unit ran in full — global/config change (package.json)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Today it will still sometimes run more than the minimal ideal set (a config
|
|
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.
|
|
129
223
|
|
|
130
224
|
**The verdict is honestly scoped.** An `affected` run never prints a bare
|
|
131
225
|
"Verified" — the terminal output and report say "Affected checks passed" (or
|
|
@@ -151,6 +245,11 @@ isn't supported, or on filesystems (containers, some network mounts) where
|
|
|
151
245
|
native change events are unreliable, pass `--poll` to fall back to an
|
|
152
246
|
interval-based scan that diffs file mtimes instead.
|
|
153
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
|
+
|
|
154
253
|
Each tick reprints the full check board. A capability that wasn't affected by
|
|
155
254
|
the latest change keeps showing its last real result, marked `⟳ cached` — a
|
|
156
255
|
cached **failure stays a failure** (✗); it is never hidden or silently
|
|
@@ -169,7 +268,9 @@ Every `veris verify` run writes:
|
|
|
169
268
|
|
|
170
269
|
`.veris/config.json` and `.veris/.gitignore` are meant to be committed;
|
|
171
270
|
`.veris/runs/`, `.veris/reports/`, and `.veris/cache/` are gitignored by
|
|
172
|
-
`veris init`.
|
|
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.
|
|
173
274
|
|
|
174
275
|
## Design
|
|
175
276
|
|
package/dist/index.js
CHANGED
|
@@ -231,7 +231,7 @@ var init_store = __esm({
|
|
|
231
231
|
// src/util/exec.ts
|
|
232
232
|
import { spawn } from "child_process";
|
|
233
233
|
function exec(cmd, args, opts = {}) {
|
|
234
|
-
return new Promise((
|
|
234
|
+
return new Promise((resolve2) => {
|
|
235
235
|
const start = performance.now();
|
|
236
236
|
const child = spawn(cmd, args, {
|
|
237
237
|
cwd: opts.cwd,
|
|
@@ -249,7 +249,7 @@ function exec(cmd, args, opts = {}) {
|
|
|
249
249
|
child.stderr.on("data", (d) => stderr += d.toString());
|
|
250
250
|
child.on("close", (code) => {
|
|
251
251
|
if (timer) clearTimeout(timer);
|
|
252
|
-
|
|
252
|
+
resolve2({
|
|
253
253
|
code: code ?? 1,
|
|
254
254
|
stdout,
|
|
255
255
|
stderr,
|
|
@@ -259,7 +259,7 @@ function exec(cmd, args, opts = {}) {
|
|
|
259
259
|
});
|
|
260
260
|
child.on("error", () => {
|
|
261
261
|
if (timer) clearTimeout(timer);
|
|
262
|
-
|
|
262
|
+
resolve2({
|
|
263
263
|
code: 127,
|
|
264
264
|
stdout,
|
|
265
265
|
stderr: stderr || `failed to spawn ${cmd}`,
|
|
@@ -375,13 +375,13 @@ var init_jest = __esm({
|
|
|
375
375
|
init_base();
|
|
376
376
|
jestRunner = {
|
|
377
377
|
id: "jest",
|
|
378
|
-
toCheck(project, _cap) {
|
|
378
|
+
toCheck(project, _cap, opts) {
|
|
379
379
|
return {
|
|
380
380
|
id: "unit",
|
|
381
381
|
title: "Unit tests",
|
|
382
382
|
runner: "jest",
|
|
383
383
|
cmd: localBin(project.root, "jest"),
|
|
384
|
-
args: ["--ci"]
|
|
384
|
+
args: ["--ci", ...opts?.targetFiles ?? []]
|
|
385
385
|
};
|
|
386
386
|
},
|
|
387
387
|
run(check, ctx) {
|
|
@@ -459,13 +459,14 @@ var init_vitest = __esm({
|
|
|
459
459
|
init_base();
|
|
460
460
|
vitestRunner = {
|
|
461
461
|
id: "vitest",
|
|
462
|
-
toCheck(project, _cap) {
|
|
462
|
+
toCheck(project, _cap, opts) {
|
|
463
|
+
const files = opts?.targetFiles ?? [];
|
|
463
464
|
return {
|
|
464
465
|
id: "unit",
|
|
465
466
|
title: "Unit tests",
|
|
466
467
|
runner: "vitest",
|
|
467
468
|
cmd: localBin(project.root, "vitest"),
|
|
468
|
-
args: ["run", "--reporter=json"]
|
|
469
|
+
args: ["run", "--reporter=json", ...files]
|
|
469
470
|
};
|
|
470
471
|
},
|
|
471
472
|
run(check, ctx) {
|
|
@@ -544,7 +545,7 @@ var init_verdict = __esm({
|
|
|
544
545
|
});
|
|
545
546
|
|
|
546
547
|
// src/core/orchestrator.ts
|
|
547
|
-
async function runChecks(project, ids, root) {
|
|
548
|
+
async function runChecks(project, ids, root, opts = {}) {
|
|
548
549
|
const known = new Set(project.capabilities.map((c) => c.id));
|
|
549
550
|
const unknown = ids.filter((id2) => !known.has(id2));
|
|
550
551
|
if (unknown.length > 0) {
|
|
@@ -565,7 +566,9 @@ async function runChecks(project, ids, root) {
|
|
|
565
566
|
summary
|
|
566
567
|
};
|
|
567
568
|
}
|
|
568
|
-
const check = runner.toCheck(project, cap
|
|
569
|
+
const check = runner.toCheck(project, cap, {
|
|
570
|
+
targetFiles: opts.targetFiles?.[capId]
|
|
571
|
+
});
|
|
569
572
|
return runner.run(check, ctx);
|
|
570
573
|
});
|
|
571
574
|
const results = await Promise.all(tasks);
|
|
@@ -708,7 +711,7 @@ var init_init = __esm({
|
|
|
708
711
|
"use strict";
|
|
709
712
|
init_detect();
|
|
710
713
|
init_fs_safe();
|
|
711
|
-
GITIGNORE = ["runs/", "reports/", "cache/", ""].join("\n");
|
|
714
|
+
GITIGNORE = ["runs/", "reports/", "cache/", "graph.json", ""].join("\n");
|
|
712
715
|
}
|
|
713
716
|
});
|
|
714
717
|
|
|
@@ -940,6 +943,338 @@ var init_changes = __esm({
|
|
|
940
943
|
}
|
|
941
944
|
});
|
|
942
945
|
|
|
946
|
+
// src/project-graph/discover.ts
|
|
947
|
+
import { readdirSync as readdirSync2, statSync } from "fs";
|
|
948
|
+
import { join as join7, relative as relative2, sep } from "path";
|
|
949
|
+
function toPosix(p) {
|
|
950
|
+
return sep === "/" ? p : p.split(sep).join("/");
|
|
951
|
+
}
|
|
952
|
+
function classify(rel) {
|
|
953
|
+
if (TEST_RE2.test(rel)) return "test";
|
|
954
|
+
if (CONFIG_RE2.test(rel)) return "config";
|
|
955
|
+
return "source";
|
|
956
|
+
}
|
|
957
|
+
function discoverFiles(root) {
|
|
958
|
+
const out = [];
|
|
959
|
+
const walk = (dir) => {
|
|
960
|
+
let entries;
|
|
961
|
+
try {
|
|
962
|
+
entries = readdirSync2(dir);
|
|
963
|
+
} catch {
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
for (const name of entries) {
|
|
967
|
+
const abs = join7(dir, name);
|
|
968
|
+
const rel = toPosix(relative2(root, abs));
|
|
969
|
+
if (IGNORE.test(rel)) continue;
|
|
970
|
+
let st;
|
|
971
|
+
try {
|
|
972
|
+
st = statSync(abs);
|
|
973
|
+
} catch {
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
if (st.isDirectory()) {
|
|
977
|
+
walk(abs);
|
|
978
|
+
} else if (CODE_RE.test(rel)) {
|
|
979
|
+
out.push({ file: rel, kind: classify(rel) });
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
walk(root);
|
|
984
|
+
return out.sort((a, b) => a.file.localeCompare(b.file));
|
|
985
|
+
}
|
|
986
|
+
var IGNORE, CODE_RE, TEST_RE2, CONFIG_RE2;
|
|
987
|
+
var init_discover = __esm({
|
|
988
|
+
"src/project-graph/discover.ts"() {
|
|
989
|
+
"use strict";
|
|
990
|
+
IGNORE = /(^|\/)(\.git|\.claude|\.veris|\.agentloop|\.agentflight|node_modules|dist|coverage|build|fixtures|__fixtures__)(\/|$)/;
|
|
991
|
+
CODE_RE = /\.[cm]?[jt]sx?$/;
|
|
992
|
+
TEST_RE2 = /(\.(test|spec)\.[cm]?[jt]sx?$)|(^|\/)(test|tests|__tests__)\//;
|
|
993
|
+
CONFIG_RE2 = /(^|\/)([^/]+\.config\.[cm]?[jt]sx?)$/;
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
// src/project-graph/scanner-resolver.ts
|
|
998
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
999
|
+
import { dirname, join as join8, relative as relative3, resolve } from "path";
|
|
1000
|
+
function extractSpecifiers(text) {
|
|
1001
|
+
const specs = [];
|
|
1002
|
+
SPEC_RE.lastIndex = 0;
|
|
1003
|
+
let m;
|
|
1004
|
+
m = SPEC_RE.exec(text);
|
|
1005
|
+
while (m !== null) {
|
|
1006
|
+
const s = m[1] ?? m[2] ?? m[3] ?? m[4];
|
|
1007
|
+
if (s) specs.push(s);
|
|
1008
|
+
m = SPEC_RE.exec(text);
|
|
1009
|
+
}
|
|
1010
|
+
return specs;
|
|
1011
|
+
}
|
|
1012
|
+
function isFile(p) {
|
|
1013
|
+
try {
|
|
1014
|
+
return statSync2(p).isFile();
|
|
1015
|
+
} catch {
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function resolveRelative(fromDir, spec) {
|
|
1020
|
+
const base = resolve(fromDir, spec);
|
|
1021
|
+
const candidates = [base];
|
|
1022
|
+
for (const ext of EXTS) candidates.push(base + ext);
|
|
1023
|
+
const extMatch = base.match(/\.[cm]?[jt]sx?$/);
|
|
1024
|
+
if (extMatch) {
|
|
1025
|
+
const noExt = base.slice(0, -extMatch[0].length);
|
|
1026
|
+
for (const ext of EXTS) candidates.push(noExt + ext);
|
|
1027
|
+
}
|
|
1028
|
+
for (const ext of EXTS) candidates.push(join8(base, `index${ext}`));
|
|
1029
|
+
for (const c of candidates) {
|
|
1030
|
+
if (existsSync3(c) && isFile(c)) return c;
|
|
1031
|
+
}
|
|
1032
|
+
return null;
|
|
1033
|
+
}
|
|
1034
|
+
function scannerImports(root, file) {
|
|
1035
|
+
let text;
|
|
1036
|
+
try {
|
|
1037
|
+
text = readFileSync3(join8(root, file), "utf8");
|
|
1038
|
+
} catch {
|
|
1039
|
+
return [];
|
|
1040
|
+
}
|
|
1041
|
+
const dir = dirname(join8(root, file));
|
|
1042
|
+
const out = /* @__PURE__ */ new Set();
|
|
1043
|
+
for (const spec of extractSpecifiers(text)) {
|
|
1044
|
+
if (!spec.startsWith(".")) continue;
|
|
1045
|
+
const resolved = resolveRelative(dir, spec);
|
|
1046
|
+
if (resolved) out.add(toPosix(relative3(root, resolved)));
|
|
1047
|
+
}
|
|
1048
|
+
return [...out];
|
|
1049
|
+
}
|
|
1050
|
+
var EXTS, SPEC_RE;
|
|
1051
|
+
var init_scanner_resolver = __esm({
|
|
1052
|
+
"src/project-graph/scanner-resolver.ts"() {
|
|
1053
|
+
"use strict";
|
|
1054
|
+
init_discover();
|
|
1055
|
+
EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
1056
|
+
SPEC_RE = /(?:import|export)\b[^'"]*?\bfrom\s*['"]([^'"]+)['"]|(?:^|[^.\w])import\s*['"]([^'"]+)['"]|\bimport\(\s*['"]([^'"]+)['"]\s*\)|\brequire\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
// src/project-graph/ts-resolver.ts
|
|
1061
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
1062
|
+
import { createRequire } from "module";
|
|
1063
|
+
import { join as join9, relative as relative4, sep as sep2 } from "path";
|
|
1064
|
+
function hasClassicApi(mod) {
|
|
1065
|
+
const m = mod;
|
|
1066
|
+
return !!m && typeof m.preProcessFile === "function" && typeof m.resolveModuleName === "function" && typeof m.readConfigFile === "function" && typeof m.parseJsonConfigFileContent === "function" && typeof m.sys === "object" && m.sys !== null;
|
|
1067
|
+
}
|
|
1068
|
+
function loadTypeScript(root) {
|
|
1069
|
+
try {
|
|
1070
|
+
const require2 = createRequire(join9(root, "__veris__.js"));
|
|
1071
|
+
const tsPath = require2.resolve("typescript");
|
|
1072
|
+
const mod = require2(tsPath);
|
|
1073
|
+
return hasClassicApi(mod) ? mod : null;
|
|
1074
|
+
} catch {
|
|
1075
|
+
return null;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
function loadCompilerOptions(tsmod, root) {
|
|
1079
|
+
const configPath = join9(root, "tsconfig.json");
|
|
1080
|
+
const read = tsmod.readConfigFile(configPath, tsmod.sys.readFile);
|
|
1081
|
+
if (read.error || !read.config) return {};
|
|
1082
|
+
const parsed = tsmod.parseJsonConfigFileContent(read.config, tsmod.sys, root);
|
|
1083
|
+
return parsed.options;
|
|
1084
|
+
}
|
|
1085
|
+
function tsImports(tsmod, root, options, file) {
|
|
1086
|
+
let text;
|
|
1087
|
+
try {
|
|
1088
|
+
text = readFileSync4(join9(root, file), "utf8");
|
|
1089
|
+
} catch {
|
|
1090
|
+
return [];
|
|
1091
|
+
}
|
|
1092
|
+
const abs = join9(root, file);
|
|
1093
|
+
const out = /* @__PURE__ */ new Set();
|
|
1094
|
+
try {
|
|
1095
|
+
const pre = tsmod.preProcessFile(text, true, true);
|
|
1096
|
+
for (const imp of pre.importedFiles) {
|
|
1097
|
+
const res = tsmod.resolveModuleName(
|
|
1098
|
+
imp.fileName,
|
|
1099
|
+
abs,
|
|
1100
|
+
options,
|
|
1101
|
+
tsmod.sys
|
|
1102
|
+
);
|
|
1103
|
+
const resolved = res.resolvedModule?.resolvedFileName;
|
|
1104
|
+
if (resolved && !resolved.includes("node_modules") && resolved.startsWith(root + sep2)) {
|
|
1105
|
+
out.add(toPosix(relative4(root, resolved)));
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
} catch {
|
|
1109
|
+
}
|
|
1110
|
+
return [...out];
|
|
1111
|
+
}
|
|
1112
|
+
function selectResolver(root) {
|
|
1113
|
+
const tsmod = existsSync4(join9(root, "tsconfig.json")) ? loadTypeScript(root) : null;
|
|
1114
|
+
if (tsmod) {
|
|
1115
|
+
try {
|
|
1116
|
+
const options = loadCompilerOptions(tsmod, root);
|
|
1117
|
+
return {
|
|
1118
|
+
resolver: "typescript",
|
|
1119
|
+
importsOf: (file) => tsImports(tsmod, root, options, file)
|
|
1120
|
+
};
|
|
1121
|
+
} catch {
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return {
|
|
1125
|
+
resolver: "scanner",
|
|
1126
|
+
importsOf: (file) => scannerImports(root, file)
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
var init_ts_resolver = __esm({
|
|
1130
|
+
"src/project-graph/ts-resolver.ts"() {
|
|
1131
|
+
"use strict";
|
|
1132
|
+
init_discover();
|
|
1133
|
+
init_scanner_resolver();
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
// src/project-graph/graph.ts
|
|
1138
|
+
var graph_exports = {};
|
|
1139
|
+
__export(graph_exports, {
|
|
1140
|
+
buildGraph: () => buildGraph
|
|
1141
|
+
});
|
|
1142
|
+
async function buildGraph(project) {
|
|
1143
|
+
const root = project.root;
|
|
1144
|
+
const files = discoverFiles(root);
|
|
1145
|
+
const known = new Set(files.map((f) => f.file));
|
|
1146
|
+
const { resolver, importsOf } = selectResolver(root);
|
|
1147
|
+
const nodes = {};
|
|
1148
|
+
for (const f of files) {
|
|
1149
|
+
nodes[f.file] = { file: f.file, kind: f.kind, imports: [], importedBy: [] };
|
|
1150
|
+
}
|
|
1151
|
+
for (const f of files) {
|
|
1152
|
+
const imps = importsOf(f.file).filter((i) => known.has(i) && i !== f.file);
|
|
1153
|
+
nodes[f.file].imports = imps;
|
|
1154
|
+
for (const i of imps) nodes[i]?.importedBy.push(f.file);
|
|
1155
|
+
}
|
|
1156
|
+
return {
|
|
1157
|
+
root,
|
|
1158
|
+
resolver,
|
|
1159
|
+
nodes,
|
|
1160
|
+
sourceFiles: files.filter((f) => f.kind === "source").map((f) => f.file),
|
|
1161
|
+
testFiles: files.filter((f) => f.kind === "test").map((f) => f.file)
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
var init_graph = __esm({
|
|
1165
|
+
"src/project-graph/graph.ts"() {
|
|
1166
|
+
"use strict";
|
|
1167
|
+
init_discover();
|
|
1168
|
+
init_ts_resolver();
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
// src/project-graph/analyze.ts
|
|
1173
|
+
function transitiveDependents(graph, file) {
|
|
1174
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1175
|
+
const stack = [...graph.nodes[file]?.importedBy ?? []];
|
|
1176
|
+
while (stack.length) {
|
|
1177
|
+
const f = stack.pop();
|
|
1178
|
+
if (!f || seen.has(f)) continue;
|
|
1179
|
+
seen.add(f);
|
|
1180
|
+
for (const d of graph.nodes[f]?.importedBy ?? []) stack.push(d);
|
|
1181
|
+
}
|
|
1182
|
+
return seen;
|
|
1183
|
+
}
|
|
1184
|
+
function reachableFromTests(graph) {
|
|
1185
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1186
|
+
const stack = [...graph.testFiles];
|
|
1187
|
+
while (stack.length) {
|
|
1188
|
+
const f = stack.pop();
|
|
1189
|
+
if (!f || seen.has(f)) continue;
|
|
1190
|
+
seen.add(f);
|
|
1191
|
+
for (const i of graph.nodes[f]?.imports ?? []) stack.push(i);
|
|
1192
|
+
}
|
|
1193
|
+
return seen;
|
|
1194
|
+
}
|
|
1195
|
+
function analyze(graph, changed = []) {
|
|
1196
|
+
const blastRadius = {};
|
|
1197
|
+
for (const file of Object.keys(graph.nodes)) {
|
|
1198
|
+
blastRadius[file] = transitiveDependents(graph, file).size;
|
|
1199
|
+
}
|
|
1200
|
+
const reached = reachableFromTests(graph);
|
|
1201
|
+
const byBlast = (a, b) => (blastRadius[b] ?? 0) - (blastRadius[a] ?? 0);
|
|
1202
|
+
const untested = graph.sourceFiles.filter((f) => !reached.has(f)).sort(byBlast);
|
|
1203
|
+
const changedSet = new Set(changed);
|
|
1204
|
+
const untestedSet = new Set(untested);
|
|
1205
|
+
const risky = graph.sourceFiles.filter(
|
|
1206
|
+
(f) => (blastRadius[f] ?? 0) > 0 && (untestedSet.has(f) || changedSet.has(f))
|
|
1207
|
+
).sort(byBlast);
|
|
1208
|
+
return { untested, blastRadius, risky };
|
|
1209
|
+
}
|
|
1210
|
+
var init_analyze = __esm({
|
|
1211
|
+
"src/project-graph/analyze.ts"() {
|
|
1212
|
+
"use strict";
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
// src/affected/select.ts
|
|
1217
|
+
var select_exports = {};
|
|
1218
|
+
__export(select_exports, {
|
|
1219
|
+
selectAffectedTests: () => selectAffectedTests
|
|
1220
|
+
});
|
|
1221
|
+
function selectAffectedTests(graph, changed) {
|
|
1222
|
+
if (graph.resolver !== "typescript") {
|
|
1223
|
+
return {
|
|
1224
|
+
mode: "full",
|
|
1225
|
+
testFiles: [],
|
|
1226
|
+
reason: "scanner graph can miss aliased/subpath imports \u2014 running the full suite"
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
for (const f of changed) {
|
|
1230
|
+
if (GLOBAL_RE.test(f)) {
|
|
1231
|
+
return {
|
|
1232
|
+
mode: "full",
|
|
1233
|
+
testFiles: [],
|
|
1234
|
+
reason: `global/config change (${f})`
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
if (!(f in graph.nodes)) {
|
|
1238
|
+
return {
|
|
1239
|
+
mode: "full",
|
|
1240
|
+
testFiles: [],
|
|
1241
|
+
reason: `unresolved changed file (${f})`
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
const tests = /* @__PURE__ */ new Set();
|
|
1246
|
+
for (const f of changed) {
|
|
1247
|
+
if (graph.nodes[f]?.kind === "test") tests.add(f);
|
|
1248
|
+
for (const dep of transitiveDependents(graph, f)) {
|
|
1249
|
+
if (graph.nodes[dep]?.kind === "test") tests.add(dep);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
if (tests.size === 0) {
|
|
1253
|
+
return {
|
|
1254
|
+
mode: "full",
|
|
1255
|
+
testFiles: [],
|
|
1256
|
+
reason: "no tests reach the changed files"
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
const selected = [...tests].sort();
|
|
1260
|
+
if (selected.some((t) => t.startsWith("-"))) {
|
|
1261
|
+
return {
|
|
1262
|
+
mode: "full",
|
|
1263
|
+
testFiles: [],
|
|
1264
|
+
reason: "a reaching test path starts with '-' (unsafe as a CLI argument)"
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
return { mode: "graph", testFiles: selected, reason: "" };
|
|
1268
|
+
}
|
|
1269
|
+
var GLOBAL_RE;
|
|
1270
|
+
var init_select = __esm({
|
|
1271
|
+
"src/affected/select.ts"() {
|
|
1272
|
+
"use strict";
|
|
1273
|
+
init_analyze();
|
|
1274
|
+
GLOBAL_RE = /(^|\/)(tsconfig[^/]*\.json|package\.json|biome\.jsonc?|\.eslintrc[^/]*|eslint\.config\.[^/]+|veris\.config\.[^/]+|[^/]+\.config\.[cm]?[jt]sx?|[^/]+\.setup\.[cm]?[jt]sx?)$/;
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
|
|
943
1278
|
// src/cli/commands/affected.ts
|
|
944
1279
|
var affected_exports = {};
|
|
945
1280
|
__export(affected_exports, {
|
|
@@ -956,7 +1291,25 @@ async function runAffected(root, opts = {}) {
|
|
|
956
1291
|
);
|
|
957
1292
|
return 0;
|
|
958
1293
|
}
|
|
959
|
-
|
|
1294
|
+
let targetFiles;
|
|
1295
|
+
let narrowedNote = "";
|
|
1296
|
+
const unitRunner = project.capabilities.find((c) => c.id === "unit")?.runner;
|
|
1297
|
+
const canNarrow = unitRunner === "vitest" || unitRunner === "jest";
|
|
1298
|
+
if (plan.checks.includes("unit") && canNarrow) {
|
|
1299
|
+
const { buildGraph: buildGraph2 } = await Promise.resolve().then(() => (init_graph(), graph_exports));
|
|
1300
|
+
const { selectAffectedTests: selectAffectedTests2 } = await Promise.resolve().then(() => (init_select(), select_exports));
|
|
1301
|
+
const graph = await buildGraph2(project);
|
|
1302
|
+
const sel = selectAffectedTests2(graph, files);
|
|
1303
|
+
if (sel.mode === "graph") {
|
|
1304
|
+
targetFiles = { unit: sel.testFiles };
|
|
1305
|
+
narrowedNote = `unit narrowed to ${sel.testFiles.length} of ${graph.testFiles.length} test file(s) via ${graph.resolver} graph`;
|
|
1306
|
+
} else {
|
|
1307
|
+
narrowedNote = `unit ran in full \u2014 ${sel.reason}`;
|
|
1308
|
+
}
|
|
1309
|
+
} else if (plan.checks.includes("unit") && unitRunner) {
|
|
1310
|
+
narrowedNote = `unit ran in full \u2014 the ${unitRunner} runner does not support file filtering`;
|
|
1311
|
+
}
|
|
1312
|
+
const run = await runChecks(project, plan.checks, root, { targetFiles });
|
|
960
1313
|
run.scope = { kind: "affected", changedCount: files.length };
|
|
961
1314
|
const affected = new Set(plan.checks);
|
|
962
1315
|
for (const cap of project.capabilities) {
|
|
@@ -974,6 +1327,8 @@ async function runAffected(root, opts = {}) {
|
|
|
974
1327
|
const runDir = await createRunDir(root, run.id);
|
|
975
1328
|
await writeMetadata(runDir, run);
|
|
976
1329
|
process.stdout.write(`${renderRun(run)}
|
|
1330
|
+
`);
|
|
1331
|
+
if (narrowedNote) process.stdout.write(`${narrowedNote}
|
|
977
1332
|
`);
|
|
978
1333
|
return verdictExitCode(run.verdict, opts);
|
|
979
1334
|
}
|
|
@@ -994,10 +1349,10 @@ var init_affected = __esm({
|
|
|
994
1349
|
// src/watch/watcher.ts
|
|
995
1350
|
import {
|
|
996
1351
|
watch as fsWatch,
|
|
997
|
-
readdirSync as
|
|
998
|
-
statSync
|
|
1352
|
+
readdirSync as readdirSync3,
|
|
1353
|
+
statSync as statSync3
|
|
999
1354
|
} from "fs";
|
|
1000
|
-
import { basename, join as
|
|
1355
|
+
import { basename, join as join10, relative as relative5 } from "path";
|
|
1001
1356
|
function watch(root, opts, onBatch) {
|
|
1002
1357
|
const debounceMs = opts.debounceMs ?? 150;
|
|
1003
1358
|
const rootBase = basename(root);
|
|
@@ -1010,7 +1365,7 @@ function watch(root, opts, onBatch) {
|
|
|
1010
1365
|
if (batch.length) onBatch(batch);
|
|
1011
1366
|
};
|
|
1012
1367
|
const schedule = (rel) => {
|
|
1013
|
-
if (!rel || rel === rootBase ||
|
|
1368
|
+
if (!rel || rel === rootBase || IGNORE2.test(rel)) return;
|
|
1014
1369
|
pending.add(rel);
|
|
1015
1370
|
if (timer) clearTimeout(timer);
|
|
1016
1371
|
timer = setTimeout(flush, debounceMs);
|
|
@@ -1051,17 +1406,17 @@ function scanMtimes(root) {
|
|
|
1051
1406
|
const walk = (dir) => {
|
|
1052
1407
|
let entries;
|
|
1053
1408
|
try {
|
|
1054
|
-
entries =
|
|
1409
|
+
entries = readdirSync3(dir);
|
|
1055
1410
|
} catch {
|
|
1056
1411
|
return;
|
|
1057
1412
|
}
|
|
1058
1413
|
for (const name of entries) {
|
|
1059
|
-
const abs =
|
|
1060
|
-
const rel =
|
|
1061
|
-
if (
|
|
1414
|
+
const abs = join10(dir, name);
|
|
1415
|
+
const rel = relative5(root, abs);
|
|
1416
|
+
if (IGNORE2.test(rel)) continue;
|
|
1062
1417
|
let st;
|
|
1063
1418
|
try {
|
|
1064
|
-
st =
|
|
1419
|
+
st = statSync3(abs);
|
|
1065
1420
|
} catch {
|
|
1066
1421
|
continue;
|
|
1067
1422
|
}
|
|
@@ -1072,11 +1427,11 @@ function scanMtimes(root) {
|
|
|
1072
1427
|
walk(root);
|
|
1073
1428
|
return out;
|
|
1074
1429
|
}
|
|
1075
|
-
var
|
|
1430
|
+
var IGNORE2;
|
|
1076
1431
|
var init_watcher = __esm({
|
|
1077
1432
|
"src/watch/watcher.ts"() {
|
|
1078
1433
|
"use strict";
|
|
1079
|
-
|
|
1434
|
+
IGNORE2 = /(^|\/)(\.git|\.veris|\.agentloop|\.agentflight|node_modules|dist)(\/|$)/;
|
|
1080
1435
|
}
|
|
1081
1436
|
});
|
|
1082
1437
|
|
|
@@ -1122,9 +1477,29 @@ async function runWatch(root, opts = {}) {
|
|
|
1122
1477
|
const project = await detectProject(root);
|
|
1123
1478
|
const { files } = await changedFiles(root);
|
|
1124
1479
|
const checks = initial ? availableIds(project) : affectedChecks(files, project).checks;
|
|
1480
|
+
let targetFiles;
|
|
1481
|
+
let narrowedNote = "";
|
|
1482
|
+
const unitRunner = project.capabilities.find(
|
|
1483
|
+
(c) => c.id === "unit"
|
|
1484
|
+
)?.runner;
|
|
1485
|
+
const canNarrow = unitRunner === "vitest" || unitRunner === "jest";
|
|
1486
|
+
if (!initial && checks.includes("unit") && canNarrow) {
|
|
1487
|
+
const { buildGraph: buildGraph2 } = await Promise.resolve().then(() => (init_graph(), graph_exports));
|
|
1488
|
+
const { selectAffectedTests: selectAffectedTests2 } = await Promise.resolve().then(() => (init_select(), select_exports));
|
|
1489
|
+
const graph = await buildGraph2(project);
|
|
1490
|
+
const sel = selectAffectedTests2(graph, files);
|
|
1491
|
+
if (sel.mode === "graph") {
|
|
1492
|
+
targetFiles = { unit: sel.testFiles };
|
|
1493
|
+
narrowedNote = `unit narrowed to ${sel.testFiles.length} of ${graph.testFiles.length} test file(s) via ${graph.resolver} graph`;
|
|
1494
|
+
} else {
|
|
1495
|
+
narrowedNote = `unit ran in full \u2014 ${sel.reason}`;
|
|
1496
|
+
}
|
|
1497
|
+
} else if (!initial && checks.includes("unit") && unitRunner) {
|
|
1498
|
+
narrowedNote = `unit ran in full \u2014 the ${unitRunner} runner does not support file filtering`;
|
|
1499
|
+
}
|
|
1125
1500
|
let fresh = [];
|
|
1126
1501
|
if (checks.length) {
|
|
1127
|
-
const r = await runChecks(project, checks, root);
|
|
1502
|
+
const r = await runChecks(project, checks, root, { targetFiles });
|
|
1128
1503
|
fresh = r.results;
|
|
1129
1504
|
for (const result of fresh) cache.set(result.checkId, { ...result });
|
|
1130
1505
|
}
|
|
@@ -1149,6 +1524,8 @@ async function runWatch(root, opts = {}) {
|
|
|
1149
1524
|
scope: { kind: "watch", changedCount: files.length }
|
|
1150
1525
|
};
|
|
1151
1526
|
process.stdout.write(`${renderRun(run)}
|
|
1527
|
+
`);
|
|
1528
|
+
if (narrowedNote) process.stdout.write(`${narrowedNote}
|
|
1152
1529
|
`);
|
|
1153
1530
|
} finally {
|
|
1154
1531
|
running = false;
|
|
@@ -1156,7 +1533,7 @@ async function runWatch(root, opts = {}) {
|
|
|
1156
1533
|
};
|
|
1157
1534
|
process.stdout.write("veris watch \u2014 press Ctrl-C to stop\n");
|
|
1158
1535
|
await tick(true);
|
|
1159
|
-
return await new Promise((
|
|
1536
|
+
return await new Promise((resolve2) => {
|
|
1160
1537
|
let stop;
|
|
1161
1538
|
try {
|
|
1162
1539
|
stop = watch(root, { poll: opts.poll }, () => {
|
|
@@ -1167,13 +1544,13 @@ async function runWatch(root, opts = {}) {
|
|
|
1167
1544
|
`veris: ${err instanceof Error ? err.message : String(err)}
|
|
1168
1545
|
`
|
|
1169
1546
|
);
|
|
1170
|
-
|
|
1547
|
+
resolve2(1);
|
|
1171
1548
|
return;
|
|
1172
1549
|
}
|
|
1173
1550
|
process.on("SIGINT", () => {
|
|
1174
1551
|
stop();
|
|
1175
1552
|
process.stdout.write("\nStopped.\n");
|
|
1176
|
-
|
|
1553
|
+
resolve2(0);
|
|
1177
1554
|
});
|
|
1178
1555
|
});
|
|
1179
1556
|
}
|
|
@@ -1190,6 +1567,147 @@ var init_watch = __esm({
|
|
|
1190
1567
|
}
|
|
1191
1568
|
});
|
|
1192
1569
|
|
|
1570
|
+
// src/cli/commands/scan.ts
|
|
1571
|
+
var scan_exports = {};
|
|
1572
|
+
__export(scan_exports, {
|
|
1573
|
+
renderScan: () => renderScan,
|
|
1574
|
+
runScan: () => runScan
|
|
1575
|
+
});
|
|
1576
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
1577
|
+
import { join as join11 } from "path";
|
|
1578
|
+
import pc3 from "picocolors";
|
|
1579
|
+
function renderScan(graph, analysis) {
|
|
1580
|
+
const plain = isPlain();
|
|
1581
|
+
const bold = (s) => plain ? s : pc3.bold(s);
|
|
1582
|
+
const dim = (s) => plain ? s : pc3.dim(s);
|
|
1583
|
+
const warn = (s) => plain ? s : pc3.yellow(s);
|
|
1584
|
+
const lines = [];
|
|
1585
|
+
lines.push(bold("Veris \u2014 scan"));
|
|
1586
|
+
lines.push("");
|
|
1587
|
+
lines.push(
|
|
1588
|
+
`Resolver ${graph.resolver}${graph.resolver === "scanner" ? dim(" (no TypeScript found \u2014 relative imports only)") : ""}`
|
|
1589
|
+
);
|
|
1590
|
+
lines.push(
|
|
1591
|
+
`Modules ${Object.keys(graph.nodes).length} \xB7 Source ${graph.sourceFiles.length} \xB7 Tests ${graph.testFiles.length}`
|
|
1592
|
+
);
|
|
1593
|
+
lines.push("");
|
|
1594
|
+
lines.push("Untested (top by impact)");
|
|
1595
|
+
const top = analysis.untested.slice(0, 10);
|
|
1596
|
+
if (top.length === 0)
|
|
1597
|
+
lines.push(dim(" none \u2014 every source file is reached by a test"));
|
|
1598
|
+
for (const f of top) {
|
|
1599
|
+
lines.push(
|
|
1600
|
+
` ${warn("!")} ${f} ${dim(`${analysis.blastRadius[f] ?? 0} dependents`)}`
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
return lines.join("\n");
|
|
1604
|
+
}
|
|
1605
|
+
async function runScan(root) {
|
|
1606
|
+
const project = await detectProject(root);
|
|
1607
|
+
const graph = await buildGraph(project);
|
|
1608
|
+
const analysis = analyze(graph);
|
|
1609
|
+
const dir = join11(root, ".veris");
|
|
1610
|
+
await ensureDir(dir);
|
|
1611
|
+
await writeFile3(
|
|
1612
|
+
join11(dir, "graph.json"),
|
|
1613
|
+
JSON.stringify({ resolver: graph.resolver, nodes: graph.nodes }, null, 2),
|
|
1614
|
+
"utf8"
|
|
1615
|
+
);
|
|
1616
|
+
process.stdout.write(`${renderScan(graph, analysis)}
|
|
1617
|
+
`);
|
|
1618
|
+
process.stdout.write(`
|
|
1619
|
+
Graph ${join11(".veris", "graph.json")}
|
|
1620
|
+
`);
|
|
1621
|
+
return 0;
|
|
1622
|
+
}
|
|
1623
|
+
var init_scan = __esm({
|
|
1624
|
+
"src/cli/commands/scan.ts"() {
|
|
1625
|
+
"use strict";
|
|
1626
|
+
init_detect();
|
|
1627
|
+
init_analyze();
|
|
1628
|
+
init_graph();
|
|
1629
|
+
init_fs_safe();
|
|
1630
|
+
init_tty();
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
// src/cli/commands/plan.ts
|
|
1635
|
+
var plan_exports = {};
|
|
1636
|
+
__export(plan_exports, {
|
|
1637
|
+
renderPlan: () => renderPlan,
|
|
1638
|
+
runPlan: () => runPlan
|
|
1639
|
+
});
|
|
1640
|
+
import pc4 from "picocolors";
|
|
1641
|
+
function renderPlan(project, graph, analysis, changed) {
|
|
1642
|
+
const plain = isPlain();
|
|
1643
|
+
const bold = (s) => plain ? s : pc4.bold(s);
|
|
1644
|
+
const dim = (s) => plain ? s : pc4.dim(s);
|
|
1645
|
+
const warn = (s) => plain ? s : pc4.yellow(s);
|
|
1646
|
+
const lines = [];
|
|
1647
|
+
lines.push(bold("Veris \u2014 plan"));
|
|
1648
|
+
lines.push(dim(`(graph via ${graph.resolver})`));
|
|
1649
|
+
lines.push("");
|
|
1650
|
+
lines.push("Test these first (high impact, untested)");
|
|
1651
|
+
const top = analysis.untested.slice(0, 5);
|
|
1652
|
+
if (top.length === 0)
|
|
1653
|
+
lines.push(
|
|
1654
|
+
dim(
|
|
1655
|
+
" none \u2014 untested files have no dependents or all files are covered"
|
|
1656
|
+
)
|
|
1657
|
+
);
|
|
1658
|
+
top.forEach((f, i) => {
|
|
1659
|
+
lines.push(
|
|
1660
|
+
` ${i + 1}. ${f} ${dim(`(${analysis.blastRadius[f] ?? 0} dependents, no test reaches it)`)}`
|
|
1661
|
+
);
|
|
1662
|
+
});
|
|
1663
|
+
lines.push("");
|
|
1664
|
+
lines.push("Verification setup");
|
|
1665
|
+
const weak = project.capabilities.filter(
|
|
1666
|
+
(c) => !c.available && c.id !== "browser"
|
|
1667
|
+
);
|
|
1668
|
+
if (weak.length === 0)
|
|
1669
|
+
lines.push(dim(" \u2713 types, lint, and unit are all configured"));
|
|
1670
|
+
for (const c of weak)
|
|
1671
|
+
lines.push(
|
|
1672
|
+
` ${warn("\u26A0")} ${c.id} capability unavailable \u2014 ${c.reason ?? "not configured"}`
|
|
1673
|
+
);
|
|
1674
|
+
if (changed.length) {
|
|
1675
|
+
lines.push("");
|
|
1676
|
+
lines.push(`Risky changes (${changed.length} changed file(s))`);
|
|
1677
|
+
if (analysis.risky.length === 0) lines.push(dim(" none flagged"));
|
|
1678
|
+
for (const f of analysis.risky.slice(0, 8)) {
|
|
1679
|
+
const untested = analysis.untested.includes(f) ? ", untested" : "";
|
|
1680
|
+
lines.push(
|
|
1681
|
+
` ${warn("!")} ${f} ${dim(`${analysis.blastRadius[f] ?? 0} dependents${untested}`)}`
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
lines.push("");
|
|
1686
|
+
lines.push(
|
|
1687
|
+
dim("Next: `veris affected` runs only the tests that reach your changes.")
|
|
1688
|
+
);
|
|
1689
|
+
return lines.join("\n");
|
|
1690
|
+
}
|
|
1691
|
+
async function runPlan(root, opts = {}) {
|
|
1692
|
+
const project = await detectProject(root);
|
|
1693
|
+
const graph = await buildGraph(project);
|
|
1694
|
+
const { files } = await changedFiles(root, { base: opts.base });
|
|
1695
|
+
const analysis = analyze(graph, files);
|
|
1696
|
+
process.stdout.write(`${renderPlan(project, graph, analysis, files)}
|
|
1697
|
+
`);
|
|
1698
|
+
return 0;
|
|
1699
|
+
}
|
|
1700
|
+
var init_plan = __esm({
|
|
1701
|
+
"src/cli/commands/plan.ts"() {
|
|
1702
|
+
"use strict";
|
|
1703
|
+
init_detect();
|
|
1704
|
+
init_changes();
|
|
1705
|
+
init_analyze();
|
|
1706
|
+
init_graph();
|
|
1707
|
+
init_tty();
|
|
1708
|
+
}
|
|
1709
|
+
});
|
|
1710
|
+
|
|
1193
1711
|
// src/cli/index.ts
|
|
1194
1712
|
import { realpathSync } from "fs";
|
|
1195
1713
|
import { argv } from "process";
|
|
@@ -1242,6 +1760,17 @@ function buildCli() {
|
|
|
1242
1760
|
const { runWatch: runWatch2 } = await Promise.resolve().then(() => (init_watch(), watch_exports));
|
|
1243
1761
|
process.exitCode = await runWatch2(process.cwd(), { poll: opts.poll });
|
|
1244
1762
|
});
|
|
1763
|
+
cli.command("scan", "Map the import graph and untested areas (read-only)").action(async () => {
|
|
1764
|
+
const { runScan: runScan2 } = await Promise.resolve().then(() => (init_scan(), scan_exports));
|
|
1765
|
+
process.exitCode = await runScan2(process.cwd());
|
|
1766
|
+
});
|
|
1767
|
+
cli.command(
|
|
1768
|
+
"plan",
|
|
1769
|
+
"Recommend what to test, from the import graph (read-only)"
|
|
1770
|
+
).option("--base <ref>", "Also factor in changes vs a git ref").action(async (opts) => {
|
|
1771
|
+
const { runPlan: runPlan2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
|
|
1772
|
+
process.exitCode = await runPlan2(process.cwd(), { base: opts.base });
|
|
1773
|
+
});
|
|
1245
1774
|
return { raw: cli, version: VERSION };
|
|
1246
1775
|
}
|
|
1247
1776
|
async function main(argv2) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "veriskit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The fastest way to prove your software works — a zero-config verification CLI.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "vitest run",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@biomejs/biome": "^2.5.3",
|
|
35
35
|
"@types/node": "^26.1.1",
|
|
36
36
|
"tsup": "^8.5.1",
|
|
37
|
-
"typescript": "^
|
|
37
|
+
"typescript": "^6.0.3",
|
|
38
38
|
"vitest": "^4.1.10"
|
|
39
39
|
},
|
|
40
40
|
"files": [
|