verifika 0.0.0-stage → 0.0.2
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 +41 -0
- package/LICENSE +21 -0
- package/README.md +185 -2
- package/dist/ci.js +34 -0
- package/dist/cli.js +25 -0
- package/dist/config.js +294 -0
- package/dist/errors.js +19 -0
- package/dist/format.js +10 -0
- package/dist/git.js +150 -0
- package/dist/glob.js +11 -0
- package/dist/init.js +75 -0
- package/dist/logs.js +133 -0
- package/dist/main.js +294 -0
- package/dist/planner.js +169 -0
- package/dist/preview.js +147 -0
- package/dist/process.js +362 -0
- package/dist/reporters/agent.js +31 -0
- package/dist/reporters/human.js +115 -0
- package/dist/reporters/json.js +7 -0
- package/dist/reporters/plan.js +49 -0
- package/dist/reporters/quiet.js +15 -0
- package/dist/reporters/reporter.js +43 -0
- package/dist/runner.js +206 -0
- package/dist/runs.js +192 -0
- package/dist/scheduler.js +87 -0
- package/dist/types.js +1 -0
- package/package.json +70 -4
- package/schemas/verifika.schema.json +139 -0
- package/skills/verifika/SKILL.md +92 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses [Semantic Versioning](https://semver.org/). The CLI flags, exit codes, configuration format (`version: 1`) and JSON output (`schemaVersion: 1`) are the public API.
|
|
4
|
+
|
|
5
|
+
## [0.0.2] - 2026-10-12
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- `vfk` is now the advertised command: `--help`, run hints (`Run: vfk logs <check>`), error hints, the README, the docs and the agent skill use it. `verifika` keeps working and is identical.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- README: a copy-paste prompt that has a coding agent install Verifika, write and validate a config, and install the skill.
|
|
14
|
+
|
|
15
|
+
## [0.0.1] - 2026-10-12
|
|
16
|
+
|
|
17
|
+
First public release.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `vfk`: a shorthand executable for `verifika`, installed by the same package.
|
|
22
|
+
- `verifika` / `verifika run`: runs configured checks concurrently with dependency ordering, per-check timeouts, optional fail-fast, process-tree cleanup and signal handling.
|
|
23
|
+
- Safety defaults: checks run sequentially unless `defaults.concurrency` or `-j` opts into parallelism; CI always runs the full suite unless `--changed`/`--changed-since` is passed explicitly (environment variables cannot switch CI mode off); a check passes only if its process tree was confirmed cleaned up; concurrent Verifika invocations in one directory keep their run records and logs apart.
|
|
24
|
+
- Hybrid selection: selective runs from git changes locally (`changes.base`, `--changed`, `--changed-since <ref>` with merge-base semantics), full runs in CI and with `--all`, explicit `--check`. Global invalidation (`invalidateAll`), `paths`/`exclude` globs, `always` checks and `dependsOn`.
|
|
25
|
+
- Conservative fallbacks: full runs when git cannot determine changes implicitly; exit 2 when an explicit comparison is impossible.
|
|
26
|
+
- Output modes: `human` (live progress), `agent` (one line on success, bounded failure preview), `json` (versioned result, `schemaVersion: 1`) and `quiet`.
|
|
27
|
+
- `verifika plan` to show selection decisions and reasons without running checks.
|
|
28
|
+
- `verifika logs` with `--failed`, `--tail` and `--run`; logs stored in a self-ignoring `.verifika/` directory with retention (`defaults.keepRuns`).
|
|
29
|
+
- `verifika init` to create a starter config from `package.json` scripts.
|
|
30
|
+
- JSON schema for `verifika.config.json` with editor autocomplete.
|
|
31
|
+
- Agent skill (`skills/verifika/SKILL.md`) for Claude Code, Codex, OpenCode and other agents.
|
|
32
|
+
- Published to npm as `verifika`; the package tarball is also attached to the GitHub release.
|
|
33
|
+
|
|
34
|
+
### Known limitations
|
|
35
|
+
|
|
36
|
+
- No per-check lock: checks that write shared outputs need `dependsOn` or `concurrency: 1` (see the configuration reference).
|
|
37
|
+
- A process that deliberately detaches from the check (on Linux and macOS: leaves the process group, for example via `setsid`; on Windows: breaks away from the job with an exited intermediate parent) is not tracked unless it keeps the check's output open, in which case the check is reported as `error`.
|
|
38
|
+
- On Windows, confirming cleanup queries the process table with PowerShell after each check, which costs roughly 0.5–3 s per check.
|
|
39
|
+
|
|
40
|
+
[0.0.2]: https://github.com/alexportelli12/verifika/releases/tag/v0.0.2
|
|
41
|
+
[0.0.1]: https://github.com/alexportelli12/verifika/releases/tag/v0.0.1
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Portelli
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,186 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Verifika
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/alexportelli12/verifika/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/alexportelli12/verifika/releases/latest)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
**Run only the checks your change needs. Get one line back when they pass.**
|
|
8
|
+
|
|
9
|
+
Every repository has a verification gate: format, lint, typecheck, tests, build. Using it well is harder than it looks:
|
|
10
|
+
|
|
11
|
+
- **Running everything is slow.** A one-line fix waits minutes for checks it cannot affect, so people (and agents) skip the gate or batch changes until the end.
|
|
12
|
+
- **Picking checks by hand is unreliable.** It is easy to forget the one that breaks, and "it passed" stops meaning much.
|
|
13
|
+
- **Failures are buried in noise.** A test runner can print tens of thousands of lines to report one failed assertion. A coding agent pays for every line in tokens and context, and often never reaches the line that matters.
|
|
14
|
+
|
|
15
|
+
Verifika fixes this with one config file and one command. You list your checks in `verifika.config.json`, with the files each one covers. Then `vfk` runs only the checks your changes affect, CI runs all of them, and the output says exactly what failed:
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
$ vfk
|
|
19
|
+
PASS 3/3 (4.1s) selective: 2 files changed since HEAD (a1b2c3d); 2 skipped
|
|
20
|
+
|
|
21
|
+
$ vfk --all
|
|
22
|
+
FAIL 4/5 (12s) full run (--all)
|
|
23
|
+
typecheck: exit 2
|
|
24
|
+
src/cart.ts(14,7): error TS2322: Type 'string' is not assignable to type 'number'.
|
|
25
|
+
Run: vfk logs typecheck
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Why Verifika
|
|
29
|
+
|
|
30
|
+
- **Small output, especially for agents.** On a real Angular project, one failing test produced 94,900 bytes of output with the project's previous verify script (~23,700 tokens). Verifika reported the same failure in 369 bytes (~90 tokens): the test name, the assertion and the totals. Full logs stay on disk until you ask for them.
|
|
31
|
+
- **Less waiting.** Locally, only affected checks run: in a 25-check monorepo, a typical edit ran 1–13 of them. Checks that share no build outputs can also run in parallel; on that Angular project, four at a time cut the full gate from 95.5 s to 61.7 s. [Measurements](docs/benchmarks.md).
|
|
32
|
+
- **Deterministic and conservative.** Selection comes from your config, file globs, dependencies and git; no heuristics, no LLM. When the scope of a change is unclear, Verifika runs everything or fails loudly. It never quietly skips a check, and in CI it runs the full suite unless you explicitly ask for a selective run.
|
|
33
|
+
- **Trustworthy results.** Timeouts, fail-fast, correct exit codes and signal handling. A check only passes once its whole process tree has exited. Tested on Linux, macOS and Windows.
|
|
34
|
+
- **Built for humans and coding agents alike.** Live progress on a terminal, one line in agent sessions, JSON for scripts. It ships an [agent skill](skills/verifika/SKILL.md) for Claude Code, Codex, OpenCode and others.
|
|
35
|
+
- **Small.** One JSON file, one runtime dependency, no daemon, no cache to invalidate.
|
|
36
|
+
|
|
37
|
+
_Verifika_ is Maltese for "verification". The command is `vfk`, short for `verifika`; both are installed and identical.
|
|
38
|
+
|
|
39
|
+
## Get started
|
|
40
|
+
|
|
41
|
+
Requires Node.js 22.13 or newer.
|
|
42
|
+
|
|
43
|
+
### With a coding agent
|
|
44
|
+
|
|
45
|
+
Paste this prompt into Claude Code, Codex, OpenCode or another coding agent, from your repository root. It installs Verifika, writes and validates a config for your project, and reports what it found.
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
Set up Verifika (https://github.com/alexportelli12/verifika) in this repository.
|
|
49
|
+
|
|
50
|
+
1. Check that Node.js is 22.13 or newer, and install the project's dependencies if
|
|
51
|
+
they are missing. Install Verifika as a dev dependency with this repository's
|
|
52
|
+
package manager (npm install --save-dev verifika, or the pnpm/yarn equivalent).
|
|
53
|
+
It provides the `vfk` command.
|
|
54
|
+
2. Run `npx vfk init` to create verifika.config.json, then refine it by reading
|
|
55
|
+
package.json, the CI workflows and the tool configs:
|
|
56
|
+
- One check per quality gate the project actually has (such as format, lint,
|
|
57
|
+
typecheck, unit tests, build). Do not invent checks for tools it does not use,
|
|
58
|
+
or add a check that another one already fully covers. Every command must be
|
|
59
|
+
non-interactive and exit on its own: no watch mode, no prompts, no dev servers.
|
|
60
|
+
- Give each check `paths` globs for the files that can affect it, so local runs
|
|
61
|
+
select only what a change needs. If unsure, leave `paths` out: the check then
|
|
62
|
+
runs on every change.
|
|
63
|
+
- Add `dependsOn` where one check needs another's output (for example e2e after build).
|
|
64
|
+
- Keep concurrency at 1 unless the checks clearly share no build outputs.
|
|
65
|
+
- If package.json has no "verify" script, add "verify": "vfk --all".
|
|
66
|
+
Reference: https://github.com/alexportelli12/verifika/blob/main/docs/configuration.md
|
|
67
|
+
3. Validate the config:
|
|
68
|
+
- Run `npx vfk --all`. If a check fails, decide whether the config is wrong (fix it)
|
|
69
|
+
or the project already fails that check (report it; do not fix it).
|
|
70
|
+
- Run `npx vfk plan`. Until this setup is committed, the changed package.json and
|
|
71
|
+
lockfile select every check; that is expected. Explain which checks a typical
|
|
72
|
+
source edit and a documentation-only edit would select once it is committed.
|
|
73
|
+
4. Install the agent skill: copy node_modules/verifika/skills/verifika into this
|
|
74
|
+
agent's skills directory (see
|
|
75
|
+
https://github.com/alexportelli12/verifika/blob/main/docs/agents.md).
|
|
76
|
+
|
|
77
|
+
Do not change source code or existing scripts to make checks pass, and do not commit.
|
|
78
|
+
Finish with: the checks you configured and why, the --all result, the expected
|
|
79
|
+
selection for those two edits, and any existing failures you found.
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Review the config it writes before committing it: the commands in it run on every verification.
|
|
83
|
+
|
|
84
|
+
### By hand
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
npm install --save-dev verifika
|
|
88
|
+
npx vfk init # creates verifika.config.json from your package.json scripts
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`init` only maps conventional script names (`lint`, `test`, `build`, ...) and leaves every check unfiltered, so open the file and give each check `paths`:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"$schema": "./node_modules/verifika/schemas/verifika.schema.json",
|
|
96
|
+
"version": 1,
|
|
97
|
+
"changes": {
|
|
98
|
+
"invalidateAll": ["package.json", "package-lock.json", "tsconfig*.json"]
|
|
99
|
+
},
|
|
100
|
+
"checks": {
|
|
101
|
+
"format": { "command": "npm run format:check" },
|
|
102
|
+
"lint": { "command": "npm run lint", "paths": ["src/**/*.{ts,html}"] },
|
|
103
|
+
"typecheck": { "command": "npm run typecheck", "paths": ["src/**/*.ts"] },
|
|
104
|
+
"test": { "command": "npm test", "paths": ["src/**", "test/**"] },
|
|
105
|
+
"build": { "command": "npm run build", "paths": ["src/**"], "dependsOn": ["typecheck"] }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
If your repository checks the formatting of every file (for example `prettier --check .`), format the new `verifika.config.json` too. Then:
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
npx vfk plan # which checks would run for your changes, and why
|
|
114
|
+
npx vfk # run them
|
|
115
|
+
npx vfk --all # run everything (do this before you push)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Add a script so everyone runs the same full gate:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{ "scripts": { "verify": "vfk --all" } }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Run logs go to `.verifika/`, which ignores itself in git, so you do not need to edit `.gitignore`. To install globally instead, run `npm install --global verifika`.
|
|
125
|
+
|
|
126
|
+
## How checks are selected
|
|
127
|
+
|
|
128
|
+
| Situation | What runs |
|
|
129
|
+
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
130
|
+
| `vfk` locally | Checks whose `paths` match files changed since `changes.base` (default `HEAD`: your uncommitted and untracked work) |
|
|
131
|
+
| `vfk` in CI (`CI` or a known CI variable) | Everything; only `--changed`/`--changed-since` on the command line can narrow it |
|
|
132
|
+
| `vfk --all` | Everything |
|
|
133
|
+
| `vfk --check lint` | `lint` and its dependencies |
|
|
134
|
+
| `vfk --changed-since origin/main` | Checks matching files changed since `merge-base(origin/main, HEAD)`, plus uncommitted and untracked files |
|
|
135
|
+
| A changed file matches `invalidateAll` | Everything |
|
|
136
|
+
| Git unavailable, no commits, unknown base | Everything, with the reason printed (or an error, when you asked for `--changed`/`--changed-since` explicitly) |
|
|
137
|
+
|
|
138
|
+
A check without `paths` runs whenever any file changed; `"always": true` runs it even when nothing changed. Selecting a check also selects its `dependsOn` checks. `vfk plan` shows every decision.
|
|
139
|
+
|
|
140
|
+
## Output modes
|
|
141
|
+
|
|
142
|
+
The default is `human` on an interactive terminal and `agent` otherwise. Override with `--mode` or `VERIFIKA_MODE`.
|
|
143
|
+
|
|
144
|
+
| Mode | Success | Failure |
|
|
145
|
+
| ------- | ----------------------------- | --------------------------------------------------- |
|
|
146
|
+
| `human` | Live progress, summary | Failing checks, previews, logs hint |
|
|
147
|
+
| `agent` | One line: `PASS 4/4 (4.3s) …` | One line per problem, bounded preview, logs command |
|
|
148
|
+
| `json` | Versioned result document | Same document, `"status": "failed"` |
|
|
149
|
+
| `quiet` | Nothing | One line on stderr |
|
|
150
|
+
|
|
151
|
+
Exit codes: `0` passed (or nothing to verify), `1` verification failed, `2` usage or configuration error, `128+n` interrupted by a signal (`130` SIGINT, `143` SIGTERM).
|
|
152
|
+
|
|
153
|
+
## Logs
|
|
154
|
+
|
|
155
|
+
Every run stores each check's combined stdout/stderr under `.verifika/runs/<run-id>/`. The directory ignores itself in git, and only the last 10 runs are kept (`defaults.keepRuns`).
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
vfk logs # latest run: statuses and log paths
|
|
159
|
+
vfk logs typecheck # full log of one check
|
|
160
|
+
vfk logs --failed # logs of every failed check
|
|
161
|
+
vfk logs test --tail 50 # last 50 lines
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Logs contain whatever your commands print, which can include secrets. They never leave your machine; keep `.verifika/` out of anything you publish.
|
|
165
|
+
|
|
166
|
+
## Agent integration
|
|
167
|
+
|
|
168
|
+
Verifika ships an [agent skill](skills/verifika/SKILL.md) that teaches coding agents when to run selective versus full verification, how to read results, and how not to claim success too early. See [docs/agents.md](docs/agents.md) to install it for Claude Code, Codex or OpenCode.
|
|
169
|
+
|
|
170
|
+
## Documentation
|
|
171
|
+
|
|
172
|
+
- [Configuration reference](docs/configuration.md)
|
|
173
|
+
- [CLI reference](docs/cli.md)
|
|
174
|
+
- [Agent integration](docs/agents.md)
|
|
175
|
+
- [Architecture](docs/architecture.md)
|
|
176
|
+
- [Validation and benchmarks](docs/benchmarks.md)
|
|
177
|
+
- [Example configs](examples/)
|
|
178
|
+
- [Contributing](CONTRIBUTING.md) · [Changelog](CHANGELOG.md) · [Security](SECURITY.md)
|
|
179
|
+
|
|
180
|
+
## Trust model
|
|
181
|
+
|
|
182
|
+
`verifika.config.json` is trusted code: its commands run through your shell with your permissions, exactly like `package.json` scripts. Verifika never puts file names, git refs or other external input into those commands. Review config changes as carefully as script changes.
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
[MIT](LICENSE)
|
package/dist/ci.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Variables set by common CI providers. */
|
|
2
|
+
export const CI_VARIABLES = [
|
|
3
|
+
"GITHUB_ACTIONS",
|
|
4
|
+
"GITLAB_CI",
|
|
5
|
+
"BUILDKITE",
|
|
6
|
+
"CIRCLECI",
|
|
7
|
+
"TRAVIS",
|
|
8
|
+
"JENKINS_URL",
|
|
9
|
+
"TF_BUILD",
|
|
10
|
+
"TEAMCITY_VERSION",
|
|
11
|
+
"BITBUCKET_BUILD_NUMBER",
|
|
12
|
+
"CODEBUILD_BUILD_ID",
|
|
13
|
+
"DRONE",
|
|
14
|
+
"APPVEYOR",
|
|
15
|
+
"SEMAPHORE",
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Decides whether this is a CI run, where verification defaults to the full
|
|
19
|
+
* suite. Environment variables can only turn CI mode on: a truthy `CI` or any
|
|
20
|
+
* provider variable means CI, and nothing (including `CI=false`, which some
|
|
21
|
+
* build setups export for unrelated reasons) can switch a detected provider
|
|
22
|
+
* off. Selective runs in CI require an explicit `--changed` or `--changed-since`.
|
|
23
|
+
*/
|
|
24
|
+
export function detectCi(env) {
|
|
25
|
+
if (isEnabled(env.CI))
|
|
26
|
+
return { ci: true, source: "CI" };
|
|
27
|
+
const provider = CI_VARIABLES.find((name) => isEnabled(env[name]));
|
|
28
|
+
return provider ? { ci: true, source: provider } : { ci: false, source: "none" };
|
|
29
|
+
}
|
|
30
|
+
function isEnabled(value) {
|
|
31
|
+
if (value === undefined)
|
|
32
|
+
return false;
|
|
33
|
+
return !["", "0", "false", "no", "off"].includes(value.trim().toLowerCase());
|
|
34
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { ExitCode } from "./errors.js";
|
|
3
|
+
import { main } from "./main.js";
|
|
4
|
+
const write = (stream) => (text) => {
|
|
5
|
+
stream.write(text);
|
|
6
|
+
};
|
|
7
|
+
// A closed pipe (e.g. `verifika logs lint | head`) is not an error worth a stack trace.
|
|
8
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
9
|
+
stream.on("error", (error) => {
|
|
10
|
+
if (error.code !== "EPIPE")
|
|
11
|
+
throw error;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
main(process.argv.slice(2), {
|
|
15
|
+
cwd: process.cwd(),
|
|
16
|
+
env: process.env,
|
|
17
|
+
isTTY: Boolean(process.stdout.isTTY),
|
|
18
|
+
out: write(process.stdout),
|
|
19
|
+
err: write(process.stderr),
|
|
20
|
+
}).then((code) => {
|
|
21
|
+
process.exitCode = code;
|
|
22
|
+
}, (error) => {
|
|
23
|
+
process.stderr.write(`verifika: internal error: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
|
|
24
|
+
process.exitCode = ExitCode.Internal;
|
|
25
|
+
});
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { VerifikaError } from "./errors.js";
|
|
4
|
+
export const CONFIG_FILE = "verifika.config.json";
|
|
5
|
+
export const CHECK_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 600_000;
|
|
7
|
+
/** Largest delay setTimeout accepts (about 24.8 days); larger values would fire immediately. */
|
|
8
|
+
export const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
9
|
+
/** Device names Windows reserves in every directory; they cannot be log file names. */
|
|
10
|
+
const RESERVED_NAMES = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
|
|
11
|
+
const DEFAULT_KEEP_RUNS = 10;
|
|
12
|
+
/**
|
|
13
|
+
* Checks run one at a time unless the config (or `-j`) opts into parallelism:
|
|
14
|
+
* only the project knows which of its scripts share build outputs, and running
|
|
15
|
+
* those concurrently produces real but misleading failures.
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_CONCURRENCY = 1;
|
|
18
|
+
/** Walks up from `cwd` to find verifika.config.json, or uses the explicit path. */
|
|
19
|
+
export function findConfigFile(cwd, explicit) {
|
|
20
|
+
if (explicit) {
|
|
21
|
+
const file = path.resolve(cwd, explicit);
|
|
22
|
+
if (!isFile(file))
|
|
23
|
+
throw new VerifikaError(`config file not found: ${explicit}`);
|
|
24
|
+
return file;
|
|
25
|
+
}
|
|
26
|
+
let directory = path.resolve(cwd);
|
|
27
|
+
for (;;) {
|
|
28
|
+
const candidate = path.join(directory, CONFIG_FILE);
|
|
29
|
+
if (isFile(candidate))
|
|
30
|
+
return candidate;
|
|
31
|
+
const parent = path.dirname(directory);
|
|
32
|
+
if (parent === directory) {
|
|
33
|
+
throw new VerifikaError(`no ${CONFIG_FILE} found in ${cwd} or any parent directory`, "Create one with: vfk init");
|
|
34
|
+
}
|
|
35
|
+
directory = parent;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function isFile(file) {
|
|
39
|
+
try {
|
|
40
|
+
return statSync(file).isFile();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function loadConfig(file) {
|
|
47
|
+
let text;
|
|
48
|
+
try {
|
|
49
|
+
text = readFileSync(file, "utf8");
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
throw new VerifikaError(`cannot read ${file}: ${error.message}`);
|
|
53
|
+
}
|
|
54
|
+
let raw;
|
|
55
|
+
try {
|
|
56
|
+
raw = JSON.parse(text.replace(/^\uFEFF/, ""));
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
throw new VerifikaError(`${path.basename(file)} is not valid JSON: ${error.message}`);
|
|
60
|
+
}
|
|
61
|
+
return parseConfig(raw, file);
|
|
62
|
+
}
|
|
63
|
+
/** Validates raw JSON and applies defaults. Reports every problem at once. */
|
|
64
|
+
export function parseConfig(raw, file) {
|
|
65
|
+
const errors = [];
|
|
66
|
+
const v = new Validator(errors);
|
|
67
|
+
if (!v.object(raw, "config"))
|
|
68
|
+
throw configError(file, errors);
|
|
69
|
+
v.keys(raw, "config", ["$schema", "version", "defaults", "changes", "checks"]);
|
|
70
|
+
v.string(raw.$schema, "$schema");
|
|
71
|
+
if (raw.version !== 1) {
|
|
72
|
+
errors.push(raw.version === undefined
|
|
73
|
+
? "version: required; set it to 1"
|
|
74
|
+
: `version: unsupported value ${JSON.stringify(raw.version)}; this release supports 1`);
|
|
75
|
+
}
|
|
76
|
+
const defaults = raw.defaults ?? {};
|
|
77
|
+
let concurrency = DEFAULT_CONCURRENCY;
|
|
78
|
+
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
79
|
+
let failFast = false;
|
|
80
|
+
let keepRuns = DEFAULT_KEEP_RUNS;
|
|
81
|
+
if (v.object(defaults, "defaults")) {
|
|
82
|
+
v.keys(defaults, "defaults", ["concurrency", "timeoutMs", "failFast", "keepRuns"]);
|
|
83
|
+
concurrency = v.integer(defaults.concurrency, "defaults.concurrency", 1, 64) ?? concurrency;
|
|
84
|
+
timeoutMs = v.integer(defaults.timeoutMs, "defaults.timeoutMs", 1, MAX_TIMEOUT_MS) ?? timeoutMs;
|
|
85
|
+
failFast = v.boolean(defaults.failFast, "defaults.failFast") ?? failFast;
|
|
86
|
+
keepRuns = v.integer(defaults.keepRuns, "defaults.keepRuns", 1, 1000) ?? keepRuns;
|
|
87
|
+
}
|
|
88
|
+
const changes = raw.changes ?? {};
|
|
89
|
+
let base = "HEAD";
|
|
90
|
+
let invalidateAll = [];
|
|
91
|
+
if (v.object(changes, "changes")) {
|
|
92
|
+
v.keys(changes, "changes", ["base", "invalidateAll"]);
|
|
93
|
+
base = v.string(changes.base, "changes.base") ?? base;
|
|
94
|
+
invalidateAll = v.globs(changes.invalidateAll, "changes.invalidateAll", true) ?? [];
|
|
95
|
+
}
|
|
96
|
+
// A config edit can change what every check means, so it always invalidates everything.
|
|
97
|
+
if (!invalidateAll.includes(CONFIG_FILE))
|
|
98
|
+
invalidateAll = [...invalidateAll, CONFIG_FILE];
|
|
99
|
+
const checks = [];
|
|
100
|
+
if (raw.checks === undefined) {
|
|
101
|
+
errors.push("checks: required; declare at least one check");
|
|
102
|
+
}
|
|
103
|
+
else if (v.object(raw.checks, "checks")) {
|
|
104
|
+
const entries = Object.entries(raw.checks);
|
|
105
|
+
if (entries.length === 0)
|
|
106
|
+
errors.push("checks: declare at least one check");
|
|
107
|
+
for (const [name, value] of entries) {
|
|
108
|
+
const at = `checks.${name}`;
|
|
109
|
+
if (!CHECK_NAME_PATTERN.test(name)) {
|
|
110
|
+
errors.push(`${at}: invalid name; use letters, digits, ".", "_" or "-", starting with a letter or digit`);
|
|
111
|
+
}
|
|
112
|
+
if (RESERVED_NAMES.test(name)) {
|
|
113
|
+
errors.push(`${at}: "${name}" is a reserved device name on Windows; choose another name`);
|
|
114
|
+
}
|
|
115
|
+
const clash = entries.find(([other]) => other !== name && other.toLowerCase() === name.toLowerCase());
|
|
116
|
+
if (clash && clash[0] < name) {
|
|
117
|
+
errors.push(`${at}: differs from "${clash[0]}" only by case; log files would collide`);
|
|
118
|
+
}
|
|
119
|
+
if (!v.object(value, at))
|
|
120
|
+
continue;
|
|
121
|
+
v.keys(value, at, ["command", "paths", "exclude", "always", "dependsOn", "timeoutMs"]);
|
|
122
|
+
const command = v.string(value.command, `${at}.command`, true);
|
|
123
|
+
const paths = v.globs(value.paths, `${at}.paths`, false);
|
|
124
|
+
const exclude = v.globs(value.exclude, `${at}.exclude`, true) ?? [];
|
|
125
|
+
const always = v.boolean(value.always, `${at}.always`) ?? false;
|
|
126
|
+
const dependsOn = v.stringArray(value.dependsOn, `${at}.dependsOn`) ?? [];
|
|
127
|
+
const checkTimeout = v.integer(value.timeoutMs, `${at}.timeoutMs`, 1, MAX_TIMEOUT_MS);
|
|
128
|
+
if (always && paths) {
|
|
129
|
+
errors.push(`${at}: "always" and "paths" cannot be combined; an always-run check ignores paths`);
|
|
130
|
+
}
|
|
131
|
+
if (always && exclude.length > 0) {
|
|
132
|
+
errors.push(`${at}: "always" and "exclude" cannot be combined`);
|
|
133
|
+
}
|
|
134
|
+
checks.push({
|
|
135
|
+
name,
|
|
136
|
+
command: command ?? "",
|
|
137
|
+
paths,
|
|
138
|
+
exclude,
|
|
139
|
+
always,
|
|
140
|
+
dependsOn,
|
|
141
|
+
timeoutMs: checkTimeout ?? timeoutMs,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
validateDependencies(checks, errors);
|
|
145
|
+
}
|
|
146
|
+
if (errors.length > 0)
|
|
147
|
+
throw configError(file, errors);
|
|
148
|
+
return {
|
|
149
|
+
file,
|
|
150
|
+
root: path.dirname(file),
|
|
151
|
+
concurrency,
|
|
152
|
+
timeoutMs,
|
|
153
|
+
failFast,
|
|
154
|
+
keepRuns,
|
|
155
|
+
base,
|
|
156
|
+
invalidateAll,
|
|
157
|
+
checks,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function validateDependencies(checks, errors) {
|
|
161
|
+
const byName = new Map(checks.map((check) => [check.name, check]));
|
|
162
|
+
for (const check of checks) {
|
|
163
|
+
const seen = new Set();
|
|
164
|
+
for (const dependency of check.dependsOn) {
|
|
165
|
+
if (dependency === check.name) {
|
|
166
|
+
errors.push(`checks.${check.name}.dependsOn: a check cannot depend on itself`);
|
|
167
|
+
}
|
|
168
|
+
else if (!byName.has(dependency)) {
|
|
169
|
+
errors.push(`checks.${check.name}.dependsOn: unknown check "${dependency}"`);
|
|
170
|
+
}
|
|
171
|
+
else if (seen.has(dependency)) {
|
|
172
|
+
errors.push(`checks.${check.name}.dependsOn: "${dependency}" is listed twice`);
|
|
173
|
+
}
|
|
174
|
+
seen.add(dependency);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const cycle = findCycle(checks, byName);
|
|
178
|
+
if (cycle)
|
|
179
|
+
errors.push(`checks: dependency cycle ${cycle.join(" -> ")}`);
|
|
180
|
+
}
|
|
181
|
+
function findCycle(checks, byName) {
|
|
182
|
+
const state = new Map();
|
|
183
|
+
const stack = [];
|
|
184
|
+
const visit = (name) => {
|
|
185
|
+
if (state.get(name) === "done")
|
|
186
|
+
return null;
|
|
187
|
+
if (state.get(name) === "visiting")
|
|
188
|
+
return [...stack.slice(stack.indexOf(name)), name];
|
|
189
|
+
state.set(name, "visiting");
|
|
190
|
+
stack.push(name);
|
|
191
|
+
for (const dependency of byName.get(name)?.dependsOn ?? []) {
|
|
192
|
+
if (dependency === name || !byName.has(dependency))
|
|
193
|
+
continue;
|
|
194
|
+
const cycle = visit(dependency);
|
|
195
|
+
if (cycle)
|
|
196
|
+
return cycle;
|
|
197
|
+
}
|
|
198
|
+
stack.pop();
|
|
199
|
+
state.set(name, "done");
|
|
200
|
+
return null;
|
|
201
|
+
};
|
|
202
|
+
for (const check of checks) {
|
|
203
|
+
const cycle = visit(check.name);
|
|
204
|
+
if (cycle)
|
|
205
|
+
return cycle;
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
function configError(file, errors) {
|
|
210
|
+
return new VerifikaError(`invalid ${path.basename(file)}:\n${errors.map((error) => ` - ${error}`).join("\n")}`);
|
|
211
|
+
}
|
|
212
|
+
/** Small collecting validator; each method returns `undefined` for absent or invalid values. */
|
|
213
|
+
class Validator {
|
|
214
|
+
errors;
|
|
215
|
+
constructor(errors) {
|
|
216
|
+
this.errors = errors;
|
|
217
|
+
}
|
|
218
|
+
object(value, at) {
|
|
219
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value))
|
|
220
|
+
return true;
|
|
221
|
+
this.errors.push(`${at}: expected an object`);
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
keys(value, at, allowed) {
|
|
225
|
+
for (const key of Object.keys(value)) {
|
|
226
|
+
if (!allowed.includes(key)) {
|
|
227
|
+
this.errors.push(`${at}: unknown property "${key}" (allowed: ${allowed.join(", ")})`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
string(value, at, required = false) {
|
|
232
|
+
if (value === undefined) {
|
|
233
|
+
if (required)
|
|
234
|
+
this.errors.push(`${at}: required`);
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
if (typeof value === "string" && value.trim() !== "")
|
|
238
|
+
return value;
|
|
239
|
+
this.errors.push(`${at}: expected a non-empty string`);
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
boolean(value, at) {
|
|
243
|
+
if (value === undefined || typeof value === "boolean")
|
|
244
|
+
return value;
|
|
245
|
+
this.errors.push(`${at}: expected true or false`);
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
integer(value, at, min, max) {
|
|
249
|
+
if (value === undefined)
|
|
250
|
+
return undefined;
|
|
251
|
+
if (typeof value === "number" &&
|
|
252
|
+
Number.isInteger(value) &&
|
|
253
|
+
value >= min &&
|
|
254
|
+
(max === undefined || value <= max)) {
|
|
255
|
+
return value;
|
|
256
|
+
}
|
|
257
|
+
const range = max === undefined ? `>= ${min}` : `between ${min} and ${max}`;
|
|
258
|
+
this.errors.push(`${at}: expected an integer ${range}`);
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
stringArray(value, at) {
|
|
262
|
+
if (value === undefined)
|
|
263
|
+
return undefined;
|
|
264
|
+
if (Array.isArray(value) &&
|
|
265
|
+
value.every((item) => typeof item === "string" && item.trim() !== "")) {
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
this.errors.push(`${at}: expected an array of non-empty strings`);
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
/** Glob lists must be portable: forward slashes, relative to the config directory. */
|
|
272
|
+
globs(value, at, allowEmpty) {
|
|
273
|
+
const list = this.stringArray(value, at);
|
|
274
|
+
if (list === undefined)
|
|
275
|
+
return undefined;
|
|
276
|
+
if (list.length === 0 && !allowEmpty) {
|
|
277
|
+
this.errors.push(`${at}: must not be empty; omit it to match every file, or use "always": true`);
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
const normalized = [];
|
|
281
|
+
for (const pattern of list) {
|
|
282
|
+
if (pattern.includes("\\")) {
|
|
283
|
+
this.errors.push(`${at}: "${pattern}" contains a backslash; use forward slashes on every platform`);
|
|
284
|
+
}
|
|
285
|
+
else if (pattern.startsWith("/") || /^[A-Za-z]:/.test(pattern)) {
|
|
286
|
+
this.errors.push(`${at}: "${pattern}" is absolute; patterns are relative to the config directory`);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
normalized.push(pattern.replace(/^(\.\/)+/, ""));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return normalized;
|
|
293
|
+
}
|
|
294
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Exit codes are part of the public CLI contract; see docs/cli.md. */
|
|
2
|
+
export const ExitCode = {
|
|
3
|
+
Passed: 0,
|
|
4
|
+
Failed: 1,
|
|
5
|
+
Usage: 2,
|
|
6
|
+
Internal: 70,
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* A problem with how Verifika was invoked or configured. These never represent
|
|
10
|
+
* check results, so they always exit with the usage code rather than a pass.
|
|
11
|
+
*/
|
|
12
|
+
export class VerifikaError extends Error {
|
|
13
|
+
hint;
|
|
14
|
+
constructor(message, hint) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "VerifikaError";
|
|
17
|
+
this.hint = hint;
|
|
18
|
+
}
|
|
19
|
+
}
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Compact duration: `0.4s`, `12s`, `3m05s`. */
|
|
2
|
+
export function formatSeconds(ms) {
|
|
3
|
+
const seconds = ms / 1000;
|
|
4
|
+
if (seconds < 10)
|
|
5
|
+
return `${seconds.toFixed(1)}s`;
|
|
6
|
+
if (seconds < 120)
|
|
7
|
+
return `${Math.round(seconds)}s`;
|
|
8
|
+
const minutes = Math.floor(seconds / 60);
|
|
9
|
+
return `${minutes}m${String(Math.floor(seconds % 60)).padStart(2, "0")}s`;
|
|
10
|
+
}
|