vigiles 32.1.1 → 33.0.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/dist/adapters/claude-code/run-scripts.d.ts +19 -3
- package/dist/adapters/claude-code/run-scripts.js +12 -4
- package/dist/cli-main.js +53 -9
- package/dist/core/hook-program.js +1 -1
- package/dist/core/hook-providers.d.ts +1 -1
- package/dist/coverage-evidence.d.ts +1 -3
- package/dist/coverage-evidence.js +7 -35
- package/dist/eval-define.js +3 -3
- package/dist/eval-lock.js +2 -1
- package/dist/hook-install.d.ts +93 -4
- package/dist/hook-install.js +235 -16
- package/dist/hook-runtime.d.ts +1 -1
- package/dist/hook-runtime.js +8 -2
- package/dist/hook-state-store.d.ts +1 -1
- package/dist/hook.js +1 -1
- package/dist/load-hook.d.ts +1 -1
- package/dist/load-hook.js +1 -1
- package/dist/source-kinds.d.ts +128 -0
- package/dist/source-kinds.js +162 -0
- package/dist/test-coverage-files.js +2 -17
- package/dist/test-coverage.js +11 -8
- package/package.json +1 -1
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vigiles — run harness-test / eval script files via the CLI.
|
|
3
|
+
*
|
|
4
|
+
* `vigiles test` and `vigiles eval` discover `*.harness.*` / `*.eval.*`
|
|
5
|
+
* scripts and run each as a child `node` process, so the two-tier
|
|
6
|
+
* harness-testing API (`src/harness-test.ts`, `src/eval.ts`) works as a CI
|
|
7
|
+
* command, not just `node x.mjs`. Scripts may be authored in **JavaScript**
|
|
8
|
+
* (`.mjs` / `.cjs` / `.js`) **or TypeScript** (`.ts` / `.mts` / `.cts`) — a TS
|
|
9
|
+
* script is run through `tsx` when installed, else Node's built-in type
|
|
10
|
+
* stripping (Node >= 22.6). The scripts import from the built `dist/`, so they
|
|
11
|
+
* also run standalone — the CLI just discovers, runs, and aggregates exit codes.
|
|
12
|
+
*/
|
|
13
|
+
import { type TestTier } from "../../source-kinds.js";
|
|
1
14
|
import { type SurfaceProbe } from "../../check-count.js";
|
|
2
15
|
/**
|
|
3
16
|
* The outcome of running one script.
|
|
@@ -102,10 +115,13 @@ export declare function statusFor(code: number, checks: number | undefined, load
|
|
|
102
115
|
* retracting silently with no type error.
|
|
103
116
|
*/
|
|
104
117
|
export declare function loadFailed(r: ScriptRunResult): boolean;
|
|
105
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* Filename extensions accepted for harness/eval scripts (JS and TS). The table
|
|
120
|
+
* lives in `source-kinds.ts`; this re-export keeps the runner's name for it.
|
|
121
|
+
*/
|
|
106
122
|
export declare const SCRIPT_EXTS: readonly ["mjs", "cjs", "js", "mts", "cts", "ts"];
|
|
107
|
-
/**
|
|
108
|
-
export declare function scriptGlob(kind:
|
|
123
|
+
/** The glob that discovers one tier's scripts, derived from the same table as everything else that names a test. */
|
|
124
|
+
export declare function scriptGlob(kind: TestTier): string;
|
|
109
125
|
export { detectNodeCaps, canRunTypeScript, type NodeCaps, } from "../../ts-runner-caps.js";
|
|
110
126
|
import type { NodeCaps } from "../../ts-runner-caps.js";
|
|
111
127
|
import { type GlobIgnore } from "../../core/glob-ignore.js";
|
|
@@ -22,6 +22,7 @@ exports.formatScriptSummary = formatScriptSummary;
|
|
|
22
22
|
* stripping (Node >= 22.6). The scripts import from the built `dist/`, so they
|
|
23
23
|
* also run standalone — the CLI just discovers, runs, and aggregates exit codes.
|
|
24
24
|
*/
|
|
25
|
+
const source_kinds_js_1 = require("../../source-kinds.js");
|
|
25
26
|
const node_child_process_1 = require("node:child_process");
|
|
26
27
|
const node_os_1 = require("node:os");
|
|
27
28
|
const node_path_1 = require("node:path");
|
|
@@ -103,11 +104,14 @@ function statusFor(code, checks, load) {
|
|
|
103
104
|
function loadFailed(r) {
|
|
104
105
|
return r.status === "skip" && r.code !== exports.SKIP_EXIT_CODE;
|
|
105
106
|
}
|
|
106
|
-
/**
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Filename extensions accepted for harness/eval scripts (JS and TS). The table
|
|
109
|
+
* lives in `source-kinds.ts`; this re-export keeps the runner's name for it.
|
|
110
|
+
*/
|
|
111
|
+
exports.SCRIPT_EXTS = source_kinds_js_1.RUNNABLE_EXTS;
|
|
112
|
+
/** The glob that discovers one tier's scripts, derived from the same table as everything else that names a test. */
|
|
109
113
|
function scriptGlob(kind) {
|
|
110
|
-
return
|
|
114
|
+
return (0, source_kinds_js_1.testGlob)(kind);
|
|
111
115
|
}
|
|
112
116
|
const TS_EXT = /\.(?:m|c)?ts$/;
|
|
113
117
|
// The capability probe lives in `src/ts-runner-caps.ts` so the SUGGESTER
|
|
@@ -194,6 +198,10 @@ function discoverScripts(patterns, defaultGlob, cwd, ignore) {
|
|
|
194
198
|
ignore: (0, glob_ignore_js_1.withIgnored)([], ignore),
|
|
195
199
|
dot: true,
|
|
196
200
|
nodir: true,
|
|
201
|
+
// `glob` defaults this to TRUE on macOS and Windows, which made
|
|
202
|
+
// `a.hook.HARNESS.mjs` a test there and nothing on Linux. The kind of a
|
|
203
|
+
// name is decided case-sensitively (`source-kinds.ts`); the glob agrees.
|
|
204
|
+
nocase: false,
|
|
197
205
|
})) {
|
|
198
206
|
found.add(m);
|
|
199
207
|
}
|
package/dist/cli-main.js
CHANGED
|
@@ -5981,7 +5981,16 @@ async function refsHookCommand() {
|
|
|
5981
5981
|
*/
|
|
5982
5982
|
async function compileProviders() {
|
|
5983
5983
|
const names = [];
|
|
5984
|
-
|
|
5984
|
+
const { claimed, unclaimed } = (0, hook_install_js_1.discoverProviderFiles)(process.cwd());
|
|
5985
|
+
// A provider that lost (or never had) its marker is not silently skipped: a
|
|
5986
|
+
// hook's `provider()` ref to it would then fail to resolve with a message
|
|
5987
|
+
// about the REF, far from the cause.
|
|
5988
|
+
if (unclaimed.length > 0) {
|
|
5989
|
+
// Every one gets the ✗ (the catch adds the first): the others used to read
|
|
5990
|
+
// as a continuation of the first message.
|
|
5991
|
+
throw new hook_program_js_1.HookCompileError(unclaimed.map((f) => (0, hook_install_js_1.unclaimedReport)(f, "provider")).join("\n✗ "));
|
|
5992
|
+
}
|
|
5993
|
+
for (const file of claimed) {
|
|
5985
5994
|
const def = await (0, hook_runtime_js_1.loadProvider)(file);
|
|
5986
5995
|
if ((0, hook_providers_js_1.unsafeProvider)(def)) {
|
|
5987
5996
|
throw new hook_program_js_1.HookCompileError(`provider ${file} ("${def.run}") is not provably read-only — ` +
|
|
@@ -6117,8 +6126,8 @@ async function installHookFile(file, adapter, registeredProviders = []) {
|
|
|
6117
6126
|
* resolved from the `--harness=` flag, else `.vigilesrc.json#harnesses`, else auto-detect.
|
|
6118
6127
|
* Returns false if any hook failed to compile for any harness.
|
|
6119
6128
|
*/
|
|
6120
|
-
async function installHooks(hookFiles, harnessFlag, configHarness) {
|
|
6121
|
-
if (hookFiles.length === 0)
|
|
6129
|
+
async function installHooks(hookFiles, harnessFlag, configHarness, validateProvidersAnyway = false) {
|
|
6130
|
+
if (hookFiles.length === 0 && !validateProvidersAnyway)
|
|
6122
6131
|
return true;
|
|
6123
6132
|
const adapters = (0, adapter_registry_js_1.resolveHarnessAdapters)({
|
|
6124
6133
|
root: process.cwd(),
|
|
@@ -6138,6 +6147,8 @@ async function installHooks(hookFiles, harnessFlag, configHarness) {
|
|
|
6138
6147
|
}
|
|
6139
6148
|
throw e;
|
|
6140
6149
|
}
|
|
6150
|
+
if (hookFiles.length === 0)
|
|
6151
|
+
return true; // providers validated; nothing to wire
|
|
6141
6152
|
let ok = true;
|
|
6142
6153
|
for (const file of hookFiles) {
|
|
6143
6154
|
try {
|
|
@@ -6645,15 +6656,40 @@ async function main() {
|
|
|
6645
6656
|
.filter((f) => f.endsWith(".spec.ts"))
|
|
6646
6657
|
.map((f) => noteExplicitOverride(excludes, f, "compiling"))
|
|
6647
6658
|
: findSpecs(excludes);
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6659
|
+
// WHAT A HOOK IS is decided once, in `source-kinds.ts`: a bare `compile`
|
|
6660
|
+
// and `compile <file>` ask the same classifier, so they cannot disagree.
|
|
6661
|
+
// Only `.hook.` files are compiled; a runnable file that carries no marker
|
|
6662
|
+
// is refused below, out loud, with the command that fixes it.
|
|
6663
|
+
const found = restArgs.length > 0
|
|
6664
|
+
? (0, hook_install_js_1.partitionHookArgs)(restArgs.filter((f) => !f.endsWith(".spec.ts")))
|
|
6665
|
+
: (0, hook_install_js_1.discoveredHookArgs)(process.cwd());
|
|
6666
|
+
const hooks = [...found.hooks];
|
|
6667
|
+
const unclaimed = found.unclaimed;
|
|
6668
|
+
if (specs.length === 0 &&
|
|
6669
|
+
hooks.length === 0 &&
|
|
6670
|
+
unclaimed.length === 0 &&
|
|
6671
|
+
found.skipped.length === 0 &&
|
|
6672
|
+
found.invalid.length === 0) {
|
|
6652
6673
|
console.log("No .spec.ts or .vigiles/hooks/ hook files found.");
|
|
6653
6674
|
console.log("Run `vigiles init` to create one.");
|
|
6654
6675
|
process.exit(0);
|
|
6655
6676
|
}
|
|
6656
6677
|
let valid = true;
|
|
6678
|
+
// Refused files FIRST and on stderr: they are the news in this run, and a
|
|
6679
|
+
// line that scrolled past under forty `✓` lines is a silent skip with
|
|
6680
|
+
// extra steps. Compiling the rest still proceeds — one stray file must
|
|
6681
|
+
// not stop the hooks that are fine.
|
|
6682
|
+
for (const f of unclaimed) {
|
|
6683
|
+
console.error(`✗ ${(0, hook_install_js_1.unclaimedReport)(f, "hook")}`);
|
|
6684
|
+
valid = false;
|
|
6685
|
+
}
|
|
6686
|
+
for (const bad of found.invalid) {
|
|
6687
|
+
console.error(`✗ ${(0, hook_install_js_1.invalidArgMessage)(bad.path, bad.reason)}`);
|
|
6688
|
+
valid = false;
|
|
6689
|
+
}
|
|
6690
|
+
for (const s of found.skipped) {
|
|
6691
|
+
console.log(`- ${s.path} skipped: a ${s.kind}, not a hook.`);
|
|
6692
|
+
}
|
|
6657
6693
|
if (specs.length > 0)
|
|
6658
6694
|
valid =
|
|
6659
6695
|
(await compile(specs, config, excludes, { harnessFlag })) && valid;
|
|
@@ -6670,13 +6706,21 @@ async function main() {
|
|
|
6670
6706
|
console.log(`\n${weightLine}`);
|
|
6671
6707
|
}
|
|
6672
6708
|
valid =
|
|
6673
|
-
(await installHooks(hooks, harnessFlag, (0, adapter_registry_js_1.declaredHarnessNames)(config.harnesses)
|
|
6709
|
+
(await installHooks(hooks, harnessFlag, (0, adapter_registry_js_1.declaredHarnessNames)(config.harnesses),
|
|
6710
|
+
// A hook-side compile is under way even when every hook was refused:
|
|
6711
|
+
// validate the providers too, so ONE run shows every problem.
|
|
6712
|
+
unclaimed.length > 0)) && valid;
|
|
6674
6713
|
// Keep an existing whole-harness registry in sync (cheap, opt-in) so the
|
|
6675
6714
|
// user never hand-runs `generate-harness`. Skipped when no harness.gen.ts.
|
|
6676
6715
|
if (specs.length > 0)
|
|
6677
6716
|
valid = (await refreshHarnessGenIfPresent(harnessFlag)) && valid;
|
|
6678
6717
|
console.log("");
|
|
6679
|
-
if (valid) {
|
|
6718
|
+
if (valid && specs.length === 0 && hooks.length === 0) {
|
|
6719
|
+
// Skipped paths are not errors, but a run that compiled nothing must not
|
|
6720
|
+
// read like one that did.
|
|
6721
|
+
console.log("Nothing was compiled: every path given was skipped.");
|
|
6722
|
+
}
|
|
6723
|
+
else if (valid) {
|
|
6680
6724
|
console.log("Compilation complete.");
|
|
6681
6725
|
}
|
|
6682
6726
|
else {
|
|
@@ -515,7 +515,7 @@ function commandView(raw, root) {
|
|
|
515
515
|
// uses, and for the same reason. This matcher read only `leaves` (raw), so
|
|
516
516
|
// three ordinary rewrites walked past a guard that blocks the plain form.
|
|
517
517
|
// MEASURED 2026-09-02 on the shipped dogfood artifact behind the public
|
|
518
|
-
// 7/7 claim (`examples/harness/safe-bash-guard.mjs`):
|
|
518
|
+
// 7/7 claim (`examples/harness/safe-bash-guard.hook.mjs`):
|
|
519
519
|
// git push --force origin main exit 2 blocked
|
|
520
520
|
// git push "--force" origin main exit 0 ALLOWED
|
|
521
521
|
// sudo git push --force origin main exit 0 ALLOWED
|
|
@@ -54,7 +54,7 @@ export declare const provide: <const Name extends string>(name: Name, run: strin
|
|
|
54
54
|
export declare const dangerously: <const Name extends string>(name: Name, run: string) => InlineProvider<Name>;
|
|
55
55
|
/**
|
|
56
56
|
* A REGISTERED provider — a reusable, named fact authored once in
|
|
57
|
-
* `.vigiles/providers/<name>.{mjs,ts}` (default-exported via {@link defineProvider})
|
|
57
|
+
* `.vigiles/providers/<name>.provider.{mjs,ts}` (default-exported via {@link defineProvider})
|
|
58
58
|
* and referenced from many hooks by {@link provider} name. The reusable sibling of
|
|
59
59
|
* the one-off inline {@link provide}; same read-only-by-default rule.
|
|
60
60
|
*/
|
|
@@ -64,9 +64,7 @@ export interface PreparedTest {
|
|
|
64
64
|
export declare function isColocatedTest(surface: Pick<CoverableSurface, "name" | "path">, testPath: string): boolean;
|
|
65
65
|
/** Wrap a discovered path. Kept as a function so both twins share one shape. */
|
|
66
66
|
export declare function prepareTest(path: string): PreparedTest;
|
|
67
|
-
|
|
68
|
-
* helper (`node:path` on disk, `posix-path` in the browser twin). */
|
|
69
|
-
export declare function isEvalScript(filename: string): boolean;
|
|
67
|
+
export { isEvalScript } from "./source-kinds.js";
|
|
70
68
|
/**
|
|
71
69
|
* The root variables a hook command may be written against — the structural
|
|
72
70
|
* slice of `PluginLayout` this needs, taken by SHAPE so the browser twin does
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SURFACE_TOKEN = void 0;
|
|
3
|
+
exports.SURFACE_TOKEN = exports.isEvalScript = void 0;
|
|
4
4
|
exports.strongerEvidence = strongerEvidence;
|
|
5
5
|
exports.isColocatedTest = isColocatedTest;
|
|
6
6
|
exports.prepareTest = prepareTest;
|
|
7
|
-
exports.isEvalScript = isEvalScript;
|
|
8
7
|
exports.hookScriptRefs = hookScriptRefs;
|
|
9
8
|
exports.evidenceFor = evidenceFor;
|
|
10
9
|
exports.hasSurfaceToken = hasSurfaceToken;
|
|
@@ -185,39 +184,12 @@ function prepareTest(path) {
|
|
|
185
184
|
* mode here is under-counting, not accusation.
|
|
186
185
|
*/
|
|
187
186
|
const SCRIPT_RE = (0, source_refs_js_1.scriptRefPattern)();
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
*
|
|
195
|
-
* - As the full suffix `.eval.mjs` it was a MONEY HAZARD — `foo.eval.ts` fell
|
|
196
|
-
* into the free branch and would have spent real model calls on every push.
|
|
197
|
-
* - As the bare INFIX `.eval.` it made a FALSE GRANT — `parser.eval.test.ts`,
|
|
198
|
-
* an ordinary deterministic test discovered by an `include` of
|
|
199
|
-
* `**\/*.test.ts`, was credited to the paid tier and dropped from the free
|
|
200
|
-
* one. `vigiles eval` globs `**\/*.eval.{mjs,cjs,js,mts,cts,ts}`, so that name
|
|
201
|
-
* is not discoverable by the eval runner at all: the surface was reported
|
|
202
|
-
* covered by the tier that cannot run it, and uncovered by the tier that does.
|
|
203
|
-
*
|
|
204
|
-
* The rule that is wrong in neither direction is the one the runner itself uses:
|
|
205
|
-
* `.eval.` followed by a runnable extension AT THE END of the name. Anything
|
|
206
|
-
* else — including a name that merely contains `.eval.` — is deterministic, or
|
|
207
|
-
* is not a script at all.
|
|
208
|
-
*
|
|
209
|
-
* ⚠️ The extension list is RE-DECLARED, not imported: the authority is
|
|
210
|
-
* `SCRIPT_EXTS` in adapters/claude-code/run-scripts.ts, which pulls in
|
|
211
|
-
* `node:child_process` and `glob` and so cannot be imported by this
|
|
212
|
-
* browser-safe module. A test in test-coverage.test.ts asserts the two agree,
|
|
213
|
-
* so an extension added to the runner cannot silently fail to be billed here.
|
|
214
|
-
*/
|
|
215
|
-
const EVAL_SCRIPT_RE = /\.eval\.(?:mjs|cjs|js|mts|cts|ts)$/;
|
|
216
|
-
/** @param filename a BASENAME — callers strip the directory with their own path
|
|
217
|
-
* helper (`node:path` on disk, `posix-path` in the browser twin). */
|
|
218
|
-
function isEvalScript(filename) {
|
|
219
|
-
return EVAL_SCRIPT_RE.test(filename);
|
|
220
|
-
}
|
|
187
|
+
// Which runner owns a test file — the paid real-model one or the free one — is
|
|
188
|
+
// answered by the shared source-kind table (`source-kinds.ts`), which this
|
|
189
|
+
// browser-safe module can import because it imports nothing. See `isEvalScript`
|
|
190
|
+
// there for the two ways this question has been answered wrongly before.
|
|
191
|
+
var source_kinds_js_1 = require("./source-kinds.js");
|
|
192
|
+
Object.defineProperty(exports, "isEvalScript", { enumerable: true, get: function () { return source_kinds_js_1.isEvalScript; } });
|
|
221
193
|
/**
|
|
222
194
|
* The hook-script paths a manifest's `hooks` block names, repo-relative.
|
|
223
195
|
*
|
package/dist/eval-define.js
CHANGED
|
@@ -60,6 +60,7 @@ exports.defineEval = defineEval;
|
|
|
60
60
|
* never executes, so it is untouched.
|
|
61
61
|
*/
|
|
62
62
|
const node_path_1 = require("node:path");
|
|
63
|
+
const source_kinds_js_1 = require("./source-kinds.js");
|
|
63
64
|
/**
|
|
64
65
|
* Brand marking a value as built by {@link defineEval}. A registered symbol, so
|
|
65
66
|
* a descriptor still reads as one across two copies of the package on disk —
|
|
@@ -128,8 +129,6 @@ function moduleDefault(mod) {
|
|
|
128
129
|
const inner = outer?.default;
|
|
129
130
|
return isEvalDefinition(inner) ? inner : outer;
|
|
130
131
|
}
|
|
131
|
-
/** Filenames vigiles runs as evals — the runner's own glob, as a pattern. */
|
|
132
|
-
const EVAL_FILE = /\.eval\.(?:m|c)?[jt]s$/;
|
|
133
132
|
/**
|
|
134
133
|
* Was node pointed STRAIGHT at an eval file? `argv1` is `process.argv[1]`: the
|
|
135
134
|
* path node was started with, which no stray configuration can forge.
|
|
@@ -142,7 +141,8 @@ const EVAL_FILE = /\.eval\.(?:m|c)?[jt]s$/;
|
|
|
142
141
|
* Pure: a fact in, a boolean out.
|
|
143
142
|
*/
|
|
144
143
|
function ranAsEntry(argv1) {
|
|
145
|
-
return argv1 !== undefined &&
|
|
144
|
+
return (argv1 !== undefined &&
|
|
145
|
+
(0, source_kinds_js_1.isEvalScript)(argv1.replaceAll("\\", "/").split("/").pop() ?? ""));
|
|
146
146
|
}
|
|
147
147
|
/** The words shown when someone runs an eval file directly. Asserted by a test:
|
|
148
148
|
* a refusal that stops a run without saying what to do instead is a ticket. */
|
package/dist/eval-lock.js
CHANGED
|
@@ -58,6 +58,7 @@ const node_fs_1 = require("node:fs");
|
|
|
58
58
|
const node_path_1 = require("node:path");
|
|
59
59
|
const hash_js_1 = require("./core/hash.js");
|
|
60
60
|
const eval_cache_js_1 = require("./eval-cache.js");
|
|
61
|
+
const source_kinds_js_1 = require("./source-kinds.js");
|
|
61
62
|
/**
|
|
62
63
|
* On-disk lock-format version, salted into nothing (the lock is keyed by name,
|
|
63
64
|
* not by hash) but VALIDATED on read so an incompatible shape fails loud rather
|
|
@@ -162,7 +163,7 @@ function isEvalInputFile(path) {
|
|
|
162
163
|
const p = path.replace(/\\/g, "/");
|
|
163
164
|
if (/(^|\/)SKILL\.md$/.test(p))
|
|
164
165
|
return true;
|
|
165
|
-
return
|
|
166
|
+
return (0, source_kinds_js_1.isEvalScript)(p.split("/").pop() ?? "");
|
|
166
167
|
}
|
|
167
168
|
/**
|
|
168
169
|
* The NON-BLOCKING nudge to emit after an eval-input edit when committed locks
|
package/dist/hook-install.d.ts
CHANGED
|
@@ -1,11 +1,100 @@
|
|
|
1
|
+
import { type CompilableMarker, type SourceKind } from "./source-kinds.js";
|
|
1
2
|
/** The agnostic, committed home for hook SOURCE — one dir, cross-adapter. */
|
|
2
3
|
export declare const HOOKS_DIR = ".vigiles/hooks";
|
|
3
4
|
/** The committed home for registered context-provider SOURCE (v2). */
|
|
4
5
|
export declare const PROVIDERS_DIR = ".vigiles/providers";
|
|
5
|
-
/**
|
|
6
|
-
export
|
|
7
|
-
/**
|
|
8
|
-
|
|
6
|
+
/** What discovery found in one source directory, split by what `compile` owes each part. */
|
|
7
|
+
export interface Discovered {
|
|
8
|
+
/** Files this directory's role compiles: `.hook.` files in `.vigiles/hooks/`, `.provider.` files in `.vigiles/providers/`. */
|
|
9
|
+
readonly claimed: readonly string[];
|
|
10
|
+
/**
|
|
11
|
+
* Runnable files nothing claims here. `compile` REFUSES these out loud
|
|
12
|
+
* ({@link unclaimedMessage}); dropping them would make a hook that lost its
|
|
13
|
+
* marker vanish from the build without a word.
|
|
14
|
+
*/
|
|
15
|
+
readonly unclaimed: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
/** Discover hook sources under {@link HOOKS_DIR}: `.hook.` files, and the runnable files that are nobody's. */
|
|
18
|
+
export declare function discoverHookFiles(cwd: string): Discovered;
|
|
19
|
+
/** Discover registered-provider sources under {@link PROVIDERS_DIR}. */
|
|
20
|
+
export declare function discoverProviderFiles(cwd: string): Discovered;
|
|
21
|
+
/** Why an explicit `compile` argument is an error rather than a hook, a refusal or a skip. */
|
|
22
|
+
export type InvalidArgReason = "missing" | "directory" | "not-a-source";
|
|
23
|
+
/** What `vigiles compile <files…>` makes of the paths it was handed. */
|
|
24
|
+
export interface HookArgs {
|
|
25
|
+
readonly hooks: readonly string[];
|
|
26
|
+
/** Runnable files nothing claims (or a marked file in the wrong directory): refused, with the fix. */
|
|
27
|
+
readonly unclaimed: readonly string[];
|
|
28
|
+
/** Named on purpose, not compilable (a test, a stamp…): said out loud, not an error. */
|
|
29
|
+
readonly skipped: readonly {
|
|
30
|
+
readonly path: string;
|
|
31
|
+
readonly kind: SourceKind["kind"];
|
|
32
|
+
}[];
|
|
33
|
+
/** Not a file at all, or not a source: an error, the way it always was. */
|
|
34
|
+
readonly invalid: readonly {
|
|
35
|
+
readonly path: string;
|
|
36
|
+
readonly reason: InvalidArgReason;
|
|
37
|
+
}[];
|
|
38
|
+
}
|
|
39
|
+
/** What the file system says a path is. Injected so the rules below stay pure. */
|
|
40
|
+
export type PathProbe = (path: string) => "file" | "directory" | "missing" | "other";
|
|
41
|
+
/**
|
|
42
|
+
* Sort explicit `compile` arguments by the same classifier discovery uses, so
|
|
43
|
+
* `compile guard.mjs` and a bare `compile` give the same answer about it — for a
|
|
44
|
+
* path ANYWHERE, not just under `.vigiles/hooks/`: `compile .claude/hooks/x.mjs`
|
|
45
|
+
* or a plugin's `hooks/guard.mjs` is refused unless it carries `.hook.`.
|
|
46
|
+
*
|
|
47
|
+
* A path that is missing, a directory or not a regular file is an error (it used
|
|
48
|
+
* to fail, and the first version of the marker rule turned it into a green run).
|
|
49
|
+
* The one lenient case exists for a shell glob (`compile .vigiles/hooks/*`): a
|
|
50
|
+
* test, declaration or stamp is reported as skipped, and so is a README or
|
|
51
|
+
* `.gitkeep` — but only inside the two vigiles source directories, where a glob
|
|
52
|
+
* would sweep one up. A `/etc/passwd` is not a README.
|
|
53
|
+
*/
|
|
54
|
+
export declare function partitionHookArgs(args: readonly string[], probe?: PathProbe): HookArgs;
|
|
55
|
+
/** The line `compile` prints for an argument it cannot treat as a hook source at all. */
|
|
56
|
+
export declare function invalidArgMessage(path: string, reason: InvalidArgReason): string;
|
|
57
|
+
/**
|
|
58
|
+
* Discovery, shaped like {@link partitionHookArgs}'s answer, so `compile` handles
|
|
59
|
+
* "no arguments" and "these paths" through one code path afterwards.
|
|
60
|
+
*/
|
|
61
|
+
export declare function discoveredHookArgs(cwd: string): HookArgs;
|
|
62
|
+
/**
|
|
63
|
+
* The ONE shell command that renames an unclaimed file to its marked name and
|
|
64
|
+
* (for a hook) recompiles it. Every path is single-quoted: this is printed by a
|
|
65
|
+
* tool and pasted by a person, and a repository can plant a file name that is a
|
|
66
|
+
* command.
|
|
67
|
+
*
|
|
68
|
+
* 🔴 THE HOOK COMMAND COPIES FIRST AND DELETES LAST, and that order is the
|
|
69
|
+
* point. Already-wired hooks keep running after an upgrade: the runtime loads
|
|
70
|
+
* whatever path `settings.json` names, and never asks what a file is called. So
|
|
71
|
+
* the break is the NEXT compile, and a plain `mv` + `compile` has one bad
|
|
72
|
+
* moment — the old path is gone and the wiring still names it. For a gate
|
|
73
|
+
* wired `|| exit 2` that is a hook that cannot load, which blocks every Bash
|
|
74
|
+
* call (the repair is a file write, not a command). Compiling the renamed copy
|
|
75
|
+
* while the old file still exists, and deleting the old one only after that
|
|
76
|
+
* compile succeeded, has no such moment: if anything fails the old hook is
|
|
77
|
+
* still on disk and still wired. `compile` replaces the old wiring itself
|
|
78
|
+
* ({@link ownedRefs}), so nothing is left naming the deleted path.
|
|
79
|
+
*
|
|
80
|
+
* 🔴 `cp -n`, because the marked name may already exist: a plain `cp` replaced
|
|
81
|
+
* an edited `guard.hook.mjs` with the stale `guard.mjs`, and the weaker hook
|
|
82
|
+
* became the wired gate. {@link unclaimedMessage} checks first and prints no
|
|
83
|
+
* command at all in that case; `-n` is for the file that appears in between.
|
|
84
|
+
*/
|
|
85
|
+
export declare function renameCommand(path: string, role: CompilableMarker): string;
|
|
86
|
+
/**
|
|
87
|
+
* {@link unclaimedMessage} with the one fact it cannot know, read off the disk:
|
|
88
|
+
* whether the marked name is already there.
|
|
89
|
+
*/
|
|
90
|
+
export declare function unclaimedReport(path: string, role: CompilableMarker, cwd?: string): string;
|
|
91
|
+
/**
|
|
92
|
+
* The text `compile` prints for one file it will not compile, with the exact
|
|
93
|
+
* command to fix it. `targetExists` is whether the marked name is already on
|
|
94
|
+
* disk: then there is nothing safe to copy, and the message says to merge by
|
|
95
|
+
* hand instead.
|
|
96
|
+
*/
|
|
97
|
+
export declare function unclaimedMessage(path: string, role: CompilableMarker, targetExists?: boolean): string;
|
|
9
98
|
interface CommandHook {
|
|
10
99
|
readonly type: "command";
|
|
11
100
|
readonly command: string;
|
package/dist/hook-install.js
CHANGED
|
@@ -3,6 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.PROVIDERS_DIR = exports.HOOKS_DIR = void 0;
|
|
4
4
|
exports.discoverHookFiles = discoverHookFiles;
|
|
5
5
|
exports.discoverProviderFiles = discoverProviderFiles;
|
|
6
|
+
exports.partitionHookArgs = partitionHookArgs;
|
|
7
|
+
exports.invalidArgMessage = invalidArgMessage;
|
|
8
|
+
exports.discoveredHookArgs = discoveredHookArgs;
|
|
9
|
+
exports.renameCommand = renameCommand;
|
|
10
|
+
exports.unclaimedReport = unclaimedReport;
|
|
11
|
+
exports.unclaimedMessage = unclaimedMessage;
|
|
6
12
|
exports.normalizeHookRef = normalizeHookRef;
|
|
7
13
|
exports.hookRuntimeRef = hookRuntimeRef;
|
|
8
14
|
exports.hookGateRef = hookGateRef;
|
|
@@ -17,7 +23,8 @@ exports.serializeConfig = serializeConfig;
|
|
|
17
23
|
* The typed hook program is harness-NEUTRAL — it imports `vigiles/hook` and
|
|
18
24
|
* compiles to whatever harness — so its SOURCE lives in the agnostic,
|
|
19
25
|
* committed {@link HOOKS_DIR} (`.vigiles/hooks/`), never in a harness's own
|
|
20
|
-
* `.claude/`. `compile` discovers each hook there
|
|
26
|
+
* `.claude/`. `compile` discovers each hook there — a hook is a `.hook.` file
|
|
27
|
+
* and nothing else, decided by `source-kinds.ts` — compiles it, and MERGES the
|
|
21
28
|
* result into the active harness's native config (`.claude/settings.json` JSON
|
|
22
29
|
* / `config.toml` TOML) — so the harness is actually wired, not handed a
|
|
23
30
|
* paste-this block. The merge is idempotent: an entry is keyed by the runtime
|
|
@@ -29,6 +36,7 @@ exports.serializeConfig = serializeConfig;
|
|
|
29
36
|
*/
|
|
30
37
|
const node_fs_1 = require("node:fs");
|
|
31
38
|
const node_path_1 = require("node:path");
|
|
39
|
+
const source_kinds_js_1 = require("./source-kinds.js");
|
|
32
40
|
/**
|
|
33
41
|
* Lazy for the same reason as in `core/hook-program.ts` — and this module is
|
|
34
42
|
* reached from the hook runtime through `hook-state-store.ts` (`normalizeHookRef`),
|
|
@@ -40,25 +48,218 @@ const stringifyToml = (value) => require("@iarna/toml").stringify(value);
|
|
|
40
48
|
exports.HOOKS_DIR = ".vigiles/hooks";
|
|
41
49
|
/** The committed home for registered context-provider SOURCE (v2). */
|
|
42
50
|
exports.PROVIDERS_DIR = ".vigiles/providers";
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
/**
|
|
52
|
+
* The ONE place a directory's role meets {@link SourceKind}. The `switch` is
|
|
53
|
+
* exhaustive, so a kind added to the table in `source-kinds.ts` is a compile
|
|
54
|
+
* error here until someone decides what hooks and providers do with it.
|
|
55
|
+
*
|
|
56
|
+
* `leave` is for files that are in these directories on purpose and are none of
|
|
57
|
+
* `compile`'s business: tests, declarations, stamps, a README.
|
|
58
|
+
*/
|
|
59
|
+
function claimOf(role, found) {
|
|
60
|
+
switch (found.kind) {
|
|
61
|
+
case "hook":
|
|
62
|
+
case "provider":
|
|
63
|
+
// A provider in the hooks directory is as unclaimed as a stray helper:
|
|
64
|
+
// nothing here would ever load it, and saying so beats a silent miss.
|
|
65
|
+
return found.kind === role ? "claim" : "refuse";
|
|
66
|
+
case "unclaimed":
|
|
67
|
+
return "refuse";
|
|
68
|
+
case "vigiles-test":
|
|
69
|
+
case "declaration":
|
|
70
|
+
case "stamp":
|
|
71
|
+
case "non-source":
|
|
72
|
+
return "leave";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** List the files under `dir` (relative to cwd), split by what `role` does with each. */
|
|
76
|
+
function discoverSources(cwd, dir, role) {
|
|
47
77
|
const abs = (0, node_path_1.join)(cwd, dir);
|
|
48
78
|
if (!(0, node_fs_1.existsSync)(abs))
|
|
49
|
-
return [];
|
|
50
|
-
|
|
51
|
-
.filter((f) => HOOK_SOURCE_RE.test(f) && !f.endsWith(".d.ts"))
|
|
79
|
+
return { claimed: [], unclaimed: [] };
|
|
80
|
+
const verdicts = (0, node_fs_1.readdirSync)(abs)
|
|
52
81
|
.sort()
|
|
53
|
-
.map((f) => (
|
|
82
|
+
.map((f) => ({
|
|
83
|
+
path: (0, node_path_1.join)(dir, f),
|
|
84
|
+
claim: claimOf(role, (0, source_kinds_js_1.classifySource)(f)),
|
|
85
|
+
}));
|
|
86
|
+
const pick = (want) => verdicts.filter((v) => v.claim === want).map((v) => v.path);
|
|
87
|
+
return { claimed: pick("claim"), unclaimed: pick("refuse") };
|
|
54
88
|
}
|
|
55
|
-
/** Discover hook
|
|
89
|
+
/** Discover hook sources under {@link HOOKS_DIR}: `.hook.` files, and the runnable files that are nobody's. */
|
|
56
90
|
function discoverHookFiles(cwd) {
|
|
57
|
-
return discoverSources(cwd, exports.HOOKS_DIR);
|
|
91
|
+
return discoverSources(cwd, exports.HOOKS_DIR, "hook");
|
|
58
92
|
}
|
|
59
|
-
/** Discover registered-provider
|
|
93
|
+
/** Discover registered-provider sources under {@link PROVIDERS_DIR}. */
|
|
60
94
|
function discoverProviderFiles(cwd) {
|
|
61
|
-
return discoverSources(cwd, exports.PROVIDERS_DIR);
|
|
95
|
+
return discoverSources(cwd, exports.PROVIDERS_DIR, "provider");
|
|
96
|
+
}
|
|
97
|
+
const realProbe = (path) => {
|
|
98
|
+
const st = (0, node_fs_1.statSync)((0, node_path_1.resolve)(process.cwd(), path), { throwIfNoEntry: false });
|
|
99
|
+
if (st === undefined)
|
|
100
|
+
return "missing";
|
|
101
|
+
return st.isFile() ? "file" : st.isDirectory() ? "directory" : "other";
|
|
102
|
+
};
|
|
103
|
+
/** Is `path` directly inside `dir` (a repo-relative POSIX dir), either separator, any prefix? */
|
|
104
|
+
function insideDir(path, dir) {
|
|
105
|
+
const parent = (0, source_kinds_js_1.dirName)(path)
|
|
106
|
+
.replace(/\\/g, "/")
|
|
107
|
+
.replace(/\/$/, "")
|
|
108
|
+
.replace(/^\.\//, "");
|
|
109
|
+
return parent === dir || parent.endsWith(`/${dir}`);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Sort explicit `compile` arguments by the same classifier discovery uses, so
|
|
113
|
+
* `compile guard.mjs` and a bare `compile` give the same answer about it — for a
|
|
114
|
+
* path ANYWHERE, not just under `.vigiles/hooks/`: `compile .claude/hooks/x.mjs`
|
|
115
|
+
* or a plugin's `hooks/guard.mjs` is refused unless it carries `.hook.`.
|
|
116
|
+
*
|
|
117
|
+
* A path that is missing, a directory or not a regular file is an error (it used
|
|
118
|
+
* to fail, and the first version of the marker rule turned it into a green run).
|
|
119
|
+
* The one lenient case exists for a shell glob (`compile .vigiles/hooks/*`): a
|
|
120
|
+
* test, declaration or stamp is reported as skipped, and so is a README or
|
|
121
|
+
* `.gitkeep` — but only inside the two vigiles source directories, where a glob
|
|
122
|
+
* would sweep one up. A `/etc/passwd` is not a README.
|
|
123
|
+
*/
|
|
124
|
+
function partitionHookArgs(args, probe = realProbe) {
|
|
125
|
+
const hooks = [];
|
|
126
|
+
const unclaimed = [];
|
|
127
|
+
const skipped = [];
|
|
128
|
+
const invalid = [];
|
|
129
|
+
for (const path of args) {
|
|
130
|
+
const what = probe(path);
|
|
131
|
+
if (what === "missing" || what === "directory") {
|
|
132
|
+
invalid.push({ path, reason: what });
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (what === "other") {
|
|
136
|
+
invalid.push({ path, reason: "not-a-source" });
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const found = (0, source_kinds_js_1.classifySource)((0, source_kinds_js_1.baseName)(path));
|
|
140
|
+
switch (found.kind) {
|
|
141
|
+
case "hook":
|
|
142
|
+
hooks.push(path);
|
|
143
|
+
break;
|
|
144
|
+
case "unclaimed":
|
|
145
|
+
unclaimed.push(path);
|
|
146
|
+
break;
|
|
147
|
+
case "provider":
|
|
148
|
+
// Valid, and compile validates providers whenever it compiles a hook —
|
|
149
|
+
// except in the hooks directory, where nothing would ever load it.
|
|
150
|
+
if (insideDir(path, exports.HOOKS_DIR))
|
|
151
|
+
unclaimed.push(path);
|
|
152
|
+
else
|
|
153
|
+
skipped.push({ path, kind: found.kind });
|
|
154
|
+
break;
|
|
155
|
+
case "vigiles-test":
|
|
156
|
+
case "declaration":
|
|
157
|
+
case "stamp":
|
|
158
|
+
skipped.push({ path, kind: found.kind });
|
|
159
|
+
break;
|
|
160
|
+
case "non-source":
|
|
161
|
+
if (insideDir(path, exports.HOOKS_DIR) || insideDir(path, exports.PROVIDERS_DIR))
|
|
162
|
+
skipped.push({ path, kind: found.kind });
|
|
163
|
+
else
|
|
164
|
+
invalid.push({ path, reason: "not-a-source" });
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { hooks, unclaimed, skipped, invalid };
|
|
169
|
+
}
|
|
170
|
+
/** The line `compile` prints for an argument it cannot treat as a hook source at all. */
|
|
171
|
+
function invalidArgMessage(path, reason) {
|
|
172
|
+
switch (reason) {
|
|
173
|
+
case "missing":
|
|
174
|
+
return `${path} — no such file.`;
|
|
175
|
+
case "directory":
|
|
176
|
+
return `${path} — is a directory. Name hook files, or run \`vigiles compile\` with no arguments to discover them.`;
|
|
177
|
+
case "not-a-source":
|
|
178
|
+
return `${path} — not a hook source (a hook is a \`<name>.hook.<ext>\` file).`;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Discovery, shaped like {@link partitionHookArgs}'s answer, so `compile` handles
|
|
183
|
+
* "no arguments" and "these paths" through one code path afterwards.
|
|
184
|
+
*/
|
|
185
|
+
function discoveredHookArgs(cwd) {
|
|
186
|
+
const { claimed, unclaimed } = discoverHookFiles(cwd);
|
|
187
|
+
return { hooks: claimed, unclaimed, skipped: [], invalid: [] };
|
|
188
|
+
}
|
|
189
|
+
/** POSIX single-quote one word: safe for any file name, a newline and `$(…)` included. */
|
|
190
|
+
const shq = (word) => `'${word.replace(/'/g, `'\\''`)}'`;
|
|
191
|
+
/**
|
|
192
|
+
* The ONE shell command that renames an unclaimed file to its marked name and
|
|
193
|
+
* (for a hook) recompiles it. Every path is single-quoted: this is printed by a
|
|
194
|
+
* tool and pasted by a person, and a repository can plant a file name that is a
|
|
195
|
+
* command.
|
|
196
|
+
*
|
|
197
|
+
* 🔴 THE HOOK COMMAND COPIES FIRST AND DELETES LAST, and that order is the
|
|
198
|
+
* point. Already-wired hooks keep running after an upgrade: the runtime loads
|
|
199
|
+
* whatever path `settings.json` names, and never asks what a file is called. So
|
|
200
|
+
* the break is the NEXT compile, and a plain `mv` + `compile` has one bad
|
|
201
|
+
* moment — the old path is gone and the wiring still names it. For a gate
|
|
202
|
+
* wired `|| exit 2` that is a hook that cannot load, which blocks every Bash
|
|
203
|
+
* call (the repair is a file write, not a command). Compiling the renamed copy
|
|
204
|
+
* while the old file still exists, and deleting the old one only after that
|
|
205
|
+
* compile succeeded, has no such moment: if anything fails the old hook is
|
|
206
|
+
* still on disk and still wired. `compile` replaces the old wiring itself
|
|
207
|
+
* ({@link ownedRefs}), so nothing is left naming the deleted path.
|
|
208
|
+
*
|
|
209
|
+
* 🔴 `cp -n`, because the marked name may already exist: a plain `cp` replaced
|
|
210
|
+
* an edited `guard.hook.mjs` with the stale `guard.mjs`, and the weaker hook
|
|
211
|
+
* became the wired gate. {@link unclaimedMessage} checks first and prints no
|
|
212
|
+
* command at all in that case; `-n` is for the file that appears in between.
|
|
213
|
+
*/
|
|
214
|
+
function renameCommand(path, role) {
|
|
215
|
+
const target = (0, source_kinds_js_1.markedName)(path, role);
|
|
216
|
+
switch (role) {
|
|
217
|
+
case "hook":
|
|
218
|
+
return (`cp -n -- ${shq(path)} ${shq(target)} && ` +
|
|
219
|
+
`npx vigiles compile ${shq(target)} && ` +
|
|
220
|
+
`rm -f -- ${shq(path)} ${shq(`${exports.HOOKS_DIR}/${(0, node_path_1.basename)(path)}.json`)}`);
|
|
221
|
+
case "provider":
|
|
222
|
+
return `mv -n -- ${shq(path)} ${shq(target)}`;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* {@link unclaimedMessage} with the one fact it cannot know, read off the disk:
|
|
227
|
+
* whether the marked name is already there.
|
|
228
|
+
*/
|
|
229
|
+
function unclaimedReport(path, role, cwd = process.cwd()) {
|
|
230
|
+
return unclaimedMessage(path, role, (0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, (0, source_kinds_js_1.markedName)(path, role))));
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* The text `compile` prints for one file it will not compile, with the exact
|
|
234
|
+
* command to fix it. `targetExists` is whether the marked name is already on
|
|
235
|
+
* disk: then there is nothing safe to copy, and the message says to merge by
|
|
236
|
+
* hand instead.
|
|
237
|
+
*/
|
|
238
|
+
function unclaimedMessage(path, role, targetExists = false) {
|
|
239
|
+
const dir = (0, source_kinds_js_1.dirName)(path);
|
|
240
|
+
const target = (0, source_kinds_js_1.markedName)(path, role);
|
|
241
|
+
const found = (0, source_kinds_js_1.classifySource)((0, source_kinds_js_1.baseName)(path)).kind;
|
|
242
|
+
const head = `${path} — not compiled: a ${role} source must carry \`.${role}.\` before its extension (${(0, source_kinds_js_1.baseName)(target)}), and this name carries no marker vigiles knows.`;
|
|
243
|
+
const misplaced = found === "provider" || found === "hook"
|
|
244
|
+
? ` It is marked as a ${found}, which belongs in ${found === "hook" ? exports.HOOKS_DIR : exports.PROVIDERS_DIR}/ — move it there.`
|
|
245
|
+
: undefined;
|
|
246
|
+
const notIt = ` If it is not a ${role}, move it out of ${dir || "this directory"} — a test belongs in a \`.harness.\` or \`.eval.\` file.`;
|
|
247
|
+
if (misplaced !== undefined)
|
|
248
|
+
return [head, misplaced].join("\n");
|
|
249
|
+
if (targetExists)
|
|
250
|
+
return [
|
|
251
|
+
head,
|
|
252
|
+
` ${target} already exists beside it, so nothing is printed that could overwrite it. If ${path} is the ${role}, compare the two by hand, keep the content you want in ${target}, delete ${path}${role === "hook" ? ` and its stamp, and run \`npx vigiles compile ${target}\`` : ""}.`,
|
|
253
|
+
notIt,
|
|
254
|
+
].join("\n");
|
|
255
|
+
return [
|
|
256
|
+
head,
|
|
257
|
+
role === "hook"
|
|
258
|
+
? ` If it is a hook, rename it. Copy first and delete last, so a failure leaves the old hook in place and wired:`
|
|
259
|
+
: ` If it is a provider, rename it (providers are found by directory, never wired by path):`,
|
|
260
|
+
` ${renameCommand(path, role)}`,
|
|
261
|
+
notIt,
|
|
262
|
+
].join("\n");
|
|
62
263
|
}
|
|
63
264
|
/**
|
|
64
265
|
* The CANONICAL form of a hook-source reference — how the path is written into
|
|
@@ -126,7 +327,25 @@ function hookGateRef(ref, projectRootTokens) {
|
|
|
126
327
|
return token === undefined ? ref : `"${token}/${ref}"`;
|
|
127
328
|
}
|
|
128
329
|
/**
|
|
129
|
-
*
|
|
330
|
+
* The wiring references a hook file owns: its own, and — for `x.hook.ts` — the
|
|
331
|
+
* one an older vigiles wrote when the file was still called `x.ts`.
|
|
332
|
+
*
|
|
333
|
+
* 🔴 WITHOUT THE SECOND, RENAMING A HOOK TO CARRY ITS MARKER LEAVES A DEAD ENTRY.
|
|
334
|
+
* Wiring is keyed by path, so compiling `guard.hook.mjs` appended a new entry and
|
|
335
|
+
* left the one for `guard.mjs` in `settings.json`, pointing at a file that is
|
|
336
|
+
* about to be deleted. That entry cannot load; wired as a gate it blocks every
|
|
337
|
+
* Bash call, the repair included (see {@link unclaimedMessage}). The marker rename
|
|
338
|
+
* is the one rename vigiles itself asks of every consumer, so it is the one the
|
|
339
|
+
* merge is told about: the renamed file takes over the old file's wiring.
|
|
340
|
+
*/
|
|
341
|
+
function ownedRefs(hookPath) {
|
|
342
|
+
const ref = normalizeHookRef(hookPath);
|
|
343
|
+
const before = (0, source_kinds_js_1.preMarkerName)(ref, "hook");
|
|
344
|
+
return before === undefined ? [ref] : [ref, before];
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* True when an entry's command routes through the runtime for `hookPath`
|
|
348
|
+
* (or for the name it had before it carried its marker, see {@link ownedRefs}).
|
|
130
349
|
*
|
|
131
350
|
* Compares CANONICALIZED path tokens rather than testing for a raw substring:
|
|
132
351
|
* `./x.hook.ts` and `x.hook.ts` are the same file (so the entry is replaced,
|
|
@@ -134,10 +353,10 @@ function hookGateRef(ref, projectRootTokens) {
|
|
|
134
353
|
* `x.hook.ts` and `my-x.hook.ts` are not (a substring test said they were).
|
|
135
354
|
*/
|
|
136
355
|
function managesHook(entry, hookPath) {
|
|
137
|
-
const
|
|
356
|
+
const refs = ownedRefs(hookPath);
|
|
138
357
|
return entry.hooks.some((h) => h.command.split(/\s+/).some((token) => {
|
|
139
358
|
const bare = bareToken(token);
|
|
140
|
-
return bare !== "" && normalizeHookRef(bare)
|
|
359
|
+
return bare !== "" && refs.includes(normalizeHookRef(bare));
|
|
141
360
|
}));
|
|
142
361
|
}
|
|
143
362
|
/**
|
package/dist/hook-runtime.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ import { loadHook } from "./load-hook.js";
|
|
|
44
44
|
* loads in a test loads identically here (one loader, no drift).
|
|
45
45
|
*/
|
|
46
46
|
export declare const loadHookProgram: typeof loadHook;
|
|
47
|
-
/** Load a registered provider (`.vigiles/providers/<name>`) → its definition. */
|
|
47
|
+
/** Load a registered provider (`.vigiles/providers/<name>.provider.<ext>`) → its definition. */
|
|
48
48
|
export declare function loadProvider(file: string, root?: string): Promise<RegisteredProvider>;
|
|
49
49
|
/** Path of the tamper-evident stamp sidecar for a hook file. */
|
|
50
50
|
export declare function hookStampPath(file: string, root?: string): string;
|
package/dist/hook-runtime.js
CHANGED
|
@@ -75,7 +75,7 @@ function injectableEventsFor(root) {
|
|
|
75
75
|
* loads in a test loads identically here (one loader, no drift).
|
|
76
76
|
*/
|
|
77
77
|
exports.loadHookProgram = load_hook_js_1.loadHook;
|
|
78
|
-
/** Load a registered provider (`.vigiles/providers/<name>`) → its definition. */
|
|
78
|
+
/** Load a registered provider (`.vigiles/providers/<name>.provider.<ext>`) → its definition. */
|
|
79
79
|
async function loadProvider(file, root = process.cwd()) {
|
|
80
80
|
const abs = (0, node_path_1.resolve)(root, file);
|
|
81
81
|
const { pathToFileURL } = require("node:url");
|
|
@@ -155,7 +155,13 @@ async function gatherHookContext(program, file, root) {
|
|
|
155
155
|
*/
|
|
156
156
|
async function loadProviderRegistry(root) {
|
|
157
157
|
const registry = {};
|
|
158
|
-
|
|
158
|
+
// Tolerant on purpose, unlike `compile`: a provider file written before the
|
|
159
|
+
// `.provider.` marker existed is `unclaimed`, and a hook compiled yesterday
|
|
160
|
+
// still resolves `provider("x")` against it today. `compile` refuses such a
|
|
161
|
+
// file out loud; the runtime must not turn that into a silently empty value
|
|
162
|
+
// for a hook already wired. Tests and declarations are never loaded.
|
|
163
|
+
const { claimed, unclaimed } = (0, hook_install_js_1.discoverProviderFiles)(root);
|
|
164
|
+
for (const file of [...claimed, ...unclaimed]) {
|
|
159
165
|
try {
|
|
160
166
|
const def = await loadProvider(file, root);
|
|
161
167
|
registry[def.name] = def;
|
|
@@ -91,7 +91,7 @@ export interface HookStateHandle {
|
|
|
91
91
|
* OLD fact, and "old" is not something a test can produce by waiting.
|
|
92
92
|
*
|
|
93
93
|
* ```js
|
|
94
|
-
* const st = experimental_hookState(".vigiles/hooks/nag.mjs", { cwd });
|
|
94
|
+
* const st = experimental_hookState(".vigiles/hooks/nag.hook.mjs", { cwd });
|
|
95
95
|
* st.seed("retro.nagged", { ago: "4d" }); // → the hook speaks
|
|
96
96
|
* st.seed("retro.nagged", { ago: "10m" }); // → the hook stays quiet
|
|
97
97
|
* ```
|
package/dist/hook.js
CHANGED
|
@@ -143,7 +143,7 @@ Object.defineProperty(exports, "HookStateError", { enumerable: true, get: functi
|
|
|
143
143
|
// and `pipesToShell()` match over, so a guard written against the closed
|
|
144
144
|
// vocabulary sees the OPERATION rather than the literal tokens.
|
|
145
145
|
//
|
|
146
|
-
// It is exported because `examples/harness/safe-bash-guard-v2.mjs` needs it —
|
|
146
|
+
// It is exported because `examples/harness/safe-bash-guard-v2.hook.mjs` needs it —
|
|
147
147
|
// and that need is the finding, not the feature. v2 is an UNRUN experiment (no
|
|
148
148
|
// test executes it) covering three things the vocabulary cannot yet express: a
|
|
149
149
|
// flag with a value (`--index-url`), an env-assignment prefix, and a
|
package/dist/load-hook.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { type AnyHook } from "./core/hook-program.js";
|
|
|
10
10
|
* ```js
|
|
11
11
|
* import { loadHook, assertHookDenies } from "vigiles";
|
|
12
12
|
*
|
|
13
|
-
* const guard = await loadHook(".vigiles/hooks/guard.mjs");
|
|
13
|
+
* const guard = await loadHook(".vigiles/hooks/guard.hook.mjs");
|
|
14
14
|
* assertHookDenies(guard, {
|
|
15
15
|
* tool_name: "Bash",
|
|
16
16
|
* tool_input: { command: "git push --force" },
|
package/dist/load-hook.js
CHANGED
|
@@ -29,7 +29,7 @@ const hook_program_js_1 = require("./core/hook-program.js");
|
|
|
29
29
|
* ```js
|
|
30
30
|
* import { loadHook, assertHookDenies } from "vigiles";
|
|
31
31
|
*
|
|
32
|
-
* const guard = await loadHook(".vigiles/hooks/guard.mjs");
|
|
32
|
+
* const guard = await loadHook(".vigiles/hooks/guard.hook.mjs");
|
|
33
33
|
* assertHookDenies(guard, {
|
|
34
34
|
* tool_name: "Bash",
|
|
35
35
|
* tool_input: { command: "git push --force" },
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a file in a vigiles-owned source directory IS — decided in ONE place.
|
|
3
|
+
*
|
|
4
|
+
* `.vigiles/hooks/` and `.vigiles/providers/` hold several kinds of file side by
|
|
5
|
+
* side: the programs `vigiles compile` turns into wiring, the tests that exercise
|
|
6
|
+
* them, type declarations, and the `.json` stamp `compile` writes next to each
|
|
7
|
+
* hook. Discovery used to ask "does it end in a runnable extension?" and treat
|
|
8
|
+
* every yes as a hook (#278): a `task-list-nudge.harness.mjs` beside its
|
|
9
|
+
* `task-list-nudge.hook.ts` was compiled as a hook and failed the build. The
|
|
10
|
+
* obvious patch — subtract the names that bit (`.harness.`, `.test.`) — is a list
|
|
11
|
+
* of what a hook is NOT, and a list of that kind is open: the next helper file
|
|
12
|
+
* somebody colocates is the next report.
|
|
13
|
+
*
|
|
14
|
+
* So a hook is defined by what it IS. A file's kind comes from ONE marker, the
|
|
15
|
+
* LAST dot-separated segment before the extension (`guard.hook.mjs` → `hook`),
|
|
16
|
+
* looked up in ONE table ({@link MARKERS}). Because exactly one segment decides,
|
|
17
|
+
* a name has exactly one kind; because the table is closed, every consumer can
|
|
18
|
+
* `switch` over {@link SourceKind} exhaustively and the compiler names the next
|
|
19
|
+
* one that forgets a case. A runnable file whose marker is not in the table is
|
|
20
|
+
* `unclaimed` — a kind of its own, not an absence — so a renamed-away hook is a
|
|
21
|
+
* reported fact rather than a silent hole.
|
|
22
|
+
*
|
|
23
|
+
* The patterns test discovery uses (`vigiles test`, `vigiles eval`, the coverage
|
|
24
|
+
* defaults, the browser twin) are DERIVED from the same table. They used to be
|
|
25
|
+
* five hand-kept copies of one fact: `*.harness.*` and `*.eval.*` are tests.
|
|
26
|
+
*
|
|
27
|
+
* Pure and import-free on purpose. `hook-install.ts` imports this and is reached
|
|
28
|
+
* from the hook runtime, where every module is a per-call cost, and the browser
|
|
29
|
+
* twin of coverage detection needs it too — nothing here may touch `node:*`.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Every extension Node executes directly. `.mts`/`.cts` are real (TS 4.7+) and
|
|
33
|
+
* Node 22 strips their types with no toolchain — measured, not assumed.
|
|
34
|
+
*/
|
|
35
|
+
export declare const RUNNABLE_EXTS: readonly ["mjs", "cjs", "js", "mts", "cts", "ts"];
|
|
36
|
+
/** Which vigiles test runner takes a file: `vigiles test` or `vigiles eval`. */
|
|
37
|
+
export type TestTier = "harness" | "eval";
|
|
38
|
+
/**
|
|
39
|
+
* The closed union of what a file in a source directory can be. Exactly one per
|
|
40
|
+
* name; see {@link classifySource}.
|
|
41
|
+
*/
|
|
42
|
+
export type SourceKind =
|
|
43
|
+
/** A compiled-hook program: `guard.hook.mjs`. */
|
|
44
|
+
{
|
|
45
|
+
readonly kind: "hook";
|
|
46
|
+
}
|
|
47
|
+
/** A registered context provider: `k8s.provider.mjs`. */
|
|
48
|
+
| {
|
|
49
|
+
readonly kind: "provider";
|
|
50
|
+
}
|
|
51
|
+
/** A vigiles test: `guard.harness.mjs` (`vigiles test`) or `guard.eval.mjs` (`vigiles eval`). */
|
|
52
|
+
| {
|
|
53
|
+
readonly kind: "vigiles-test";
|
|
54
|
+
readonly test: TestTier;
|
|
55
|
+
}
|
|
56
|
+
/** A type declaration: `x.d.ts`, `x.d.mts`, `x.d.cts`. */
|
|
57
|
+
| {
|
|
58
|
+
readonly kind: "declaration";
|
|
59
|
+
}
|
|
60
|
+
/** The `.json` sidecar `compile` writes beside a source: `guard.hook.mjs.json`. */
|
|
61
|
+
| {
|
|
62
|
+
readonly kind: "stamp";
|
|
63
|
+
}
|
|
64
|
+
/** Not a runnable source at all: `README.md`, `.gitkeep`, a stray `.json`. */
|
|
65
|
+
| {
|
|
66
|
+
readonly kind: "non-source";
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A runnable file whose name carries no marker vigiles knows: `guard.mjs`
|
|
70
|
+
* (a hook from before the marker), `util.ts`, `guard.test.ts`. Nobody's — and
|
|
71
|
+
* therefore never compiled, and never silently ignored either.
|
|
72
|
+
*/
|
|
73
|
+
| {
|
|
74
|
+
readonly kind: "unclaimed";
|
|
75
|
+
};
|
|
76
|
+
/** The markers that name a file a vigiles consumer COMPILES (as opposed to runs or ignores). */
|
|
77
|
+
export type CompilableMarker = "hook" | "provider";
|
|
78
|
+
/**
|
|
79
|
+
* The kind of a file, from its BASENAME. Total: every string has an answer, and
|
|
80
|
+
* the answer is one member of {@link SourceKind}.
|
|
81
|
+
*
|
|
82
|
+
* Order matters and is the whole algorithm: a declaration is a declaration even
|
|
83
|
+
* when its infix says `hook` (`x.hook.d.ts`); a `.json` beside a source is its
|
|
84
|
+
* stamp; then the last infix is looked up. Case-sensitive, like the file systems
|
|
85
|
+
* these names mostly live on.
|
|
86
|
+
*/
|
|
87
|
+
export declare function classifySource(filename: string): SourceKind;
|
|
88
|
+
/**
|
|
89
|
+
* Is this basename an eval script — the PAID tier, the one that spends real
|
|
90
|
+
* model calls? It decides which of two runners owns a test file, and has been
|
|
91
|
+
* wrong in both directions:
|
|
92
|
+
*
|
|
93
|
+
* - As the full suffix `.eval.mjs`: `foo.eval.ts` fell into the free branch and
|
|
94
|
+
* would have spent real model calls on every push.
|
|
95
|
+
* - As the bare INFIX `.eval.`: `parser.eval.test.ts`, an ordinary deterministic
|
|
96
|
+
* test, was credited to the paid tier and dropped from the free one — though
|
|
97
|
+
* `vigiles eval` cannot discover that name at all.
|
|
98
|
+
*
|
|
99
|
+
* The answer that is wrong in neither direction is the runner's own: `.eval.`
|
|
100
|
+
* followed by a runnable extension AT THE END of the name, which is exactly what
|
|
101
|
+
* the last-infix rule of {@link classifySource} says.
|
|
102
|
+
*
|
|
103
|
+
* @param filename a BASENAME — callers strip the directory with their own path
|
|
104
|
+
* helper (`node:path` on disk, `posix-path` in the browser twin).
|
|
105
|
+
*/
|
|
106
|
+
export declare function isEvalScript(filename: string): boolean;
|
|
107
|
+
/** Is this basename a vigiles test of either tier? */
|
|
108
|
+
export declare function isVigilesTest(filename: string): boolean;
|
|
109
|
+
/** The glob `vigiles test` / `vigiles eval` discover a tier's scripts with. */
|
|
110
|
+
export declare function testGlob(tier: TestTier): string;
|
|
111
|
+
/** The patterns coverage credits tests with by default — one per tier, from the table. */
|
|
112
|
+
export declare const DEFAULT_TEST_GLOBS: readonly string[];
|
|
113
|
+
/** The last path segment, whichever separator precedes it. */
|
|
114
|
+
export declare const baseName: (path: string) => string;
|
|
115
|
+
/** Everything up to and including the last separator, or "" when there is none. */
|
|
116
|
+
export declare const dirName: (path: string) => string;
|
|
117
|
+
/**
|
|
118
|
+
* `guard.mjs` → `guard.hook.mjs`: the name a file must carry to be claimed as
|
|
119
|
+
* `marker`. Keeps the directory. The inverse of {@link preMarkerName}.
|
|
120
|
+
*/
|
|
121
|
+
export declare function markedName(path: string, marker: CompilableMarker): string;
|
|
122
|
+
/**
|
|
123
|
+
* `guard.hook.mjs` → `guard.mjs`: the name a file carried before the marker
|
|
124
|
+
* existed, or `undefined` when the name has no such marker. Used to recognise
|
|
125
|
+
* the wiring an older vigiles wrote for the same file.
|
|
126
|
+
*/
|
|
127
|
+
export declare function preMarkerName(path: string, marker: CompilableMarker): string | undefined;
|
|
128
|
+
//# sourceMappingURL=source-kinds.d.ts.map
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* What a file in a vigiles-owned source directory IS — decided in ONE place.
|
|
4
|
+
*
|
|
5
|
+
* `.vigiles/hooks/` and `.vigiles/providers/` hold several kinds of file side by
|
|
6
|
+
* side: the programs `vigiles compile` turns into wiring, the tests that exercise
|
|
7
|
+
* them, type declarations, and the `.json` stamp `compile` writes next to each
|
|
8
|
+
* hook. Discovery used to ask "does it end in a runnable extension?" and treat
|
|
9
|
+
* every yes as a hook (#278): a `task-list-nudge.harness.mjs` beside its
|
|
10
|
+
* `task-list-nudge.hook.ts` was compiled as a hook and failed the build. The
|
|
11
|
+
* obvious patch — subtract the names that bit (`.harness.`, `.test.`) — is a list
|
|
12
|
+
* of what a hook is NOT, and a list of that kind is open: the next helper file
|
|
13
|
+
* somebody colocates is the next report.
|
|
14
|
+
*
|
|
15
|
+
* So a hook is defined by what it IS. A file's kind comes from ONE marker, the
|
|
16
|
+
* LAST dot-separated segment before the extension (`guard.hook.mjs` → `hook`),
|
|
17
|
+
* looked up in ONE table ({@link MARKERS}). Because exactly one segment decides,
|
|
18
|
+
* a name has exactly one kind; because the table is closed, every consumer can
|
|
19
|
+
* `switch` over {@link SourceKind} exhaustively and the compiler names the next
|
|
20
|
+
* one that forgets a case. A runnable file whose marker is not in the table is
|
|
21
|
+
* `unclaimed` — a kind of its own, not an absence — so a renamed-away hook is a
|
|
22
|
+
* reported fact rather than a silent hole.
|
|
23
|
+
*
|
|
24
|
+
* The patterns test discovery uses (`vigiles test`, `vigiles eval`, the coverage
|
|
25
|
+
* defaults, the browser twin) are DERIVED from the same table. They used to be
|
|
26
|
+
* five hand-kept copies of one fact: `*.harness.*` and `*.eval.*` are tests.
|
|
27
|
+
*
|
|
28
|
+
* Pure and import-free on purpose. `hook-install.ts` imports this and is reached
|
|
29
|
+
* from the hook runtime, where every module is a per-call cost, and the browser
|
|
30
|
+
* twin of coverage detection needs it too — nothing here may touch `node:*`.
|
|
31
|
+
*/
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.dirName = exports.baseName = exports.DEFAULT_TEST_GLOBS = exports.RUNNABLE_EXTS = void 0;
|
|
34
|
+
exports.classifySource = classifySource;
|
|
35
|
+
exports.isEvalScript = isEvalScript;
|
|
36
|
+
exports.isVigilesTest = isVigilesTest;
|
|
37
|
+
exports.testGlob = testGlob;
|
|
38
|
+
exports.markedName = markedName;
|
|
39
|
+
exports.preMarkerName = preMarkerName;
|
|
40
|
+
/**
|
|
41
|
+
* Every extension Node executes directly. `.mts`/`.cts` are real (TS 4.7+) and
|
|
42
|
+
* Node 22 strips their types with no toolchain — measured, not assumed.
|
|
43
|
+
*/
|
|
44
|
+
exports.RUNNABLE_EXTS = ["mjs", "cjs", "js", "mts", "cts", "ts"];
|
|
45
|
+
/**
|
|
46
|
+
* The marker table: the LAST dot-segment before the extension → its kind.
|
|
47
|
+
* Adding a kind of file means adding a row here and handling the new
|
|
48
|
+
* {@link SourceKind} member in each consumer's `switch`.
|
|
49
|
+
*/
|
|
50
|
+
const MARKERS = [
|
|
51
|
+
{ infix: "hook", kind: { kind: "hook" } },
|
|
52
|
+
{ infix: "provider", kind: { kind: "provider" } },
|
|
53
|
+
{ infix: "harness", kind: { kind: "vigiles-test", test: "harness" } },
|
|
54
|
+
{ infix: "eval", kind: { kind: "vigiles-test", test: "eval" } },
|
|
55
|
+
];
|
|
56
|
+
/**
|
|
57
|
+
* The tiers a vigiles test can belong to, read off {@link MARKERS}. A tier is
|
|
58
|
+
* also its own infix (`guard.eval.mjs` is tier `eval`): `source-kinds.test.ts`
|
|
59
|
+
* asserts that for every tier, so a row that broke it fails there.
|
|
60
|
+
*/
|
|
61
|
+
const TEST_TIERS = MARKERS.flatMap((m) => m.kind.kind === "vigiles-test" ? [m.kind.test] : []);
|
|
62
|
+
const EXT_ALT = exports.RUNNABLE_EXTS.join("|");
|
|
63
|
+
const DECLARATION = /\.d\.(?:ts|mts|cts)$/;
|
|
64
|
+
const STAMP = new RegExp(`\\.(?:${EXT_ALT})\\.json$`);
|
|
65
|
+
/**
|
|
66
|
+
* `<stem>.<infix>.<ext>`, the infix being the LAST segment before the extension.
|
|
67
|
+
* The stem may be empty (`.harness.mjs`): the runner's glob matches that, so the
|
|
68
|
+
* classifier must too; {@link classifySource} decides which kinds may go without
|
|
69
|
+
* one. `[\\s\\S]`, not `.`: `.` stops at a newline, and a name with one in it
|
|
70
|
+
* used to fall out of every kind and be dropped without a word.
|
|
71
|
+
*/
|
|
72
|
+
const MARKED = new RegExp(`^([\\s\\S]*)\\.([^.]+)\\.(?:${EXT_ALT})$`);
|
|
73
|
+
const RUNNABLE = new RegExp(`^[\\s\\S]+\\.(?:${EXT_ALT})$`);
|
|
74
|
+
const UNCLAIMED = { kind: "unclaimed" };
|
|
75
|
+
/**
|
|
76
|
+
* The kind of a file, from its BASENAME. Total: every string has an answer, and
|
|
77
|
+
* the answer is one member of {@link SourceKind}.
|
|
78
|
+
*
|
|
79
|
+
* Order matters and is the whole algorithm: a declaration is a declaration even
|
|
80
|
+
* when its infix says `hook` (`x.hook.d.ts`); a `.json` beside a source is its
|
|
81
|
+
* stamp; then the last infix is looked up. Case-sensitive, like the file systems
|
|
82
|
+
* these names mostly live on.
|
|
83
|
+
*/
|
|
84
|
+
function classifySource(filename) {
|
|
85
|
+
if (DECLARATION.test(filename))
|
|
86
|
+
return { kind: "declaration" };
|
|
87
|
+
if (STAMP.test(filename))
|
|
88
|
+
return { kind: "stamp" };
|
|
89
|
+
if (!RUNNABLE.test(filename))
|
|
90
|
+
return { kind: "non-source" };
|
|
91
|
+
const parts = MARKED.exec(filename);
|
|
92
|
+
const row = MARKERS.find((m) => m.infix === parts?.[2]);
|
|
93
|
+
if (row === undefined)
|
|
94
|
+
return UNCLAIMED;
|
|
95
|
+
// A test needs no name to be run (`vigiles test` finds `.harness.mjs`); a hook
|
|
96
|
+
// or a provider must be nameable, so a stemless one is nobody's.
|
|
97
|
+
return parts?.[1] === "" && row.kind.kind !== "vigiles-test"
|
|
98
|
+
? UNCLAIMED
|
|
99
|
+
: row.kind;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Is this basename an eval script — the PAID tier, the one that spends real
|
|
103
|
+
* model calls? It decides which of two runners owns a test file, and has been
|
|
104
|
+
* wrong in both directions:
|
|
105
|
+
*
|
|
106
|
+
* - As the full suffix `.eval.mjs`: `foo.eval.ts` fell into the free branch and
|
|
107
|
+
* would have spent real model calls on every push.
|
|
108
|
+
* - As the bare INFIX `.eval.`: `parser.eval.test.ts`, an ordinary deterministic
|
|
109
|
+
* test, was credited to the paid tier and dropped from the free one — though
|
|
110
|
+
* `vigiles eval` cannot discover that name at all.
|
|
111
|
+
*
|
|
112
|
+
* The answer that is wrong in neither direction is the runner's own: `.eval.`
|
|
113
|
+
* followed by a runnable extension AT THE END of the name, which is exactly what
|
|
114
|
+
* the last-infix rule of {@link classifySource} says.
|
|
115
|
+
*
|
|
116
|
+
* @param filename a BASENAME — callers strip the directory with their own path
|
|
117
|
+
* helper (`node:path` on disk, `posix-path` in the browser twin).
|
|
118
|
+
*/
|
|
119
|
+
function isEvalScript(filename) {
|
|
120
|
+
const k = classifySource(filename);
|
|
121
|
+
return k.kind === "vigiles-test" && k.test === "eval";
|
|
122
|
+
}
|
|
123
|
+
/** Is this basename a vigiles test of either tier? */
|
|
124
|
+
function isVigilesTest(filename) {
|
|
125
|
+
return classifySource(filename).kind === "vigiles-test";
|
|
126
|
+
}
|
|
127
|
+
/** The glob `vigiles test` / `vigiles eval` discover a tier's scripts with. */
|
|
128
|
+
function testGlob(tier) {
|
|
129
|
+
return `**/*.${tier}.{${exports.RUNNABLE_EXTS.join(",")}}`;
|
|
130
|
+
}
|
|
131
|
+
/** The patterns coverage credits tests with by default — one per tier, from the table. */
|
|
132
|
+
exports.DEFAULT_TEST_GLOBS = TEST_TIERS.map(testGlob);
|
|
133
|
+
/** Split at the last directory separator — either one, so a Windows path works too. */
|
|
134
|
+
const splitDir = (path) => {
|
|
135
|
+
const cut = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")) + 1;
|
|
136
|
+
return [path.slice(0, cut), path.slice(cut)];
|
|
137
|
+
};
|
|
138
|
+
/** The last path segment, whichever separator precedes it. */
|
|
139
|
+
const baseName = (path) => splitDir(path)[1];
|
|
140
|
+
exports.baseName = baseName;
|
|
141
|
+
/** Everything up to and including the last separator, or "" when there is none. */
|
|
142
|
+
const dirName = (path) => splitDir(path)[0];
|
|
143
|
+
exports.dirName = dirName;
|
|
144
|
+
/**
|
|
145
|
+
* `guard.mjs` → `guard.hook.mjs`: the name a file must carry to be claimed as
|
|
146
|
+
* `marker`. Keeps the directory. The inverse of {@link preMarkerName}.
|
|
147
|
+
*/
|
|
148
|
+
function markedName(path, marker) {
|
|
149
|
+
const [dir, base] = splitDir(path);
|
|
150
|
+
return dir + base.replace(/\.([^.]+)$/, `.${marker}.$1`);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* `guard.hook.mjs` → `guard.mjs`: the name a file carried before the marker
|
|
154
|
+
* existed, or `undefined` when the name has no such marker. Used to recognise
|
|
155
|
+
* the wiring an older vigiles wrote for the same file.
|
|
156
|
+
*/
|
|
157
|
+
function preMarkerName(path, marker) {
|
|
158
|
+
const [dir, base] = splitDir(path);
|
|
159
|
+
const m = new RegExp(`^([\\s\\S]+)\\.${marker}\\.(${EXT_ALT})$`).exec(base);
|
|
160
|
+
return m === null ? undefined : `${dir}${m[1]}.${m[2]}`;
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=source-kinds.js.map
|
|
@@ -34,22 +34,7 @@ exports.findUntestedSurfacesInFiles = findUntestedSurfacesInFiles;
|
|
|
34
34
|
const posix_path_js_1 = require("./posix-path.js");
|
|
35
35
|
const layout_js_1 = require("./core/layout.js");
|
|
36
36
|
const coverage_evidence_js_1 = require("./coverage-evidence.js");
|
|
37
|
-
|
|
38
|
-
// — test-coverage.ts pulls in node:fs/glob, and this twin must stay browser-safe.
|
|
39
|
-
const DEFAULT_TEST_SUFFIXES = [
|
|
40
|
-
".harness.ts",
|
|
41
|
-
".harness.mts",
|
|
42
|
-
".harness.cts",
|
|
43
|
-
".harness.js",
|
|
44
|
-
".harness.mjs",
|
|
45
|
-
".harness.cjs",
|
|
46
|
-
".eval.ts",
|
|
47
|
-
".eval.mts",
|
|
48
|
-
".eval.cts",
|
|
49
|
-
".eval.js",
|
|
50
|
-
".eval.mjs",
|
|
51
|
-
".eval.cjs",
|
|
52
|
-
];
|
|
37
|
+
const source_kinds_js_1 = require("./source-kinds.js");
|
|
53
38
|
const IGNORE_MARKER = "vigiles:ignore-test";
|
|
54
39
|
/** Mirror of the DEFAULT_IGNORE globs (root-anchored), as a key predicate. */
|
|
55
40
|
function isIgnored(key) {
|
|
@@ -190,7 +175,7 @@ function discoverTests(files) {
|
|
|
190
175
|
for (const path of Object.keys(files)) {
|
|
191
176
|
if (isIgnored(path))
|
|
192
177
|
continue;
|
|
193
|
-
if (
|
|
178
|
+
if ((0, source_kinds_js_1.isVigilesTest)((0, posix_path_js_1.basename)(path))) {
|
|
194
179
|
out.push((0, coverage_evidence_js_1.prepareTest)(path));
|
|
195
180
|
}
|
|
196
181
|
}
|
package/dist/test-coverage.js
CHANGED
|
@@ -78,6 +78,7 @@ const ts_runner_caps_js_1 = require("./ts-runner-caps.js");
|
|
|
78
78
|
const glob_1 = require("glob");
|
|
79
79
|
const glob_ignore_js_1 = require("./core/glob-ignore.js");
|
|
80
80
|
const exclude_js_1 = require("./exclude.js");
|
|
81
|
+
const source_kinds_js_1 = require("./source-kinds.js");
|
|
81
82
|
const layout_js_1 = require("./core/layout.js");
|
|
82
83
|
const coverage_evidence_js_1 = require("./coverage-evidence.js");
|
|
83
84
|
const coverage_artifact_js_1 = require("./coverage-artifact.js");
|
|
@@ -98,9 +99,6 @@ function surfaceGlobs(dir, leaf, materializeRoot) {
|
|
|
98
99
|
// ---------------------------------------------------------------------------
|
|
99
100
|
// Internals
|
|
100
101
|
// ---------------------------------------------------------------------------
|
|
101
|
-
/** Every extension Node executes directly. `.mts`/`.cts` are real (TS 4.7+) and
|
|
102
|
-
* Node 22 strips their types with no toolchain — measured, not assumed. */
|
|
103
|
-
const RUNNABLE_EXTS = "{ts,mts,cts,js,mjs,cjs}";
|
|
104
102
|
/**
|
|
105
103
|
* 🔴 `*.test.*` USED TO BE HERE, AND REMOVING IT IS THE POINT.
|
|
106
104
|
*
|
|
@@ -128,10 +126,11 @@ const RUNNABLE_EXTS = "{ts,mts,cts,js,mjs,cjs}";
|
|
|
128
126
|
* for hooks/agents and dropping them for skills — because a rule with a per-kind
|
|
129
127
|
* exception is what this file just spent a day removing.
|
|
130
128
|
*/
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
129
|
+
// Read off the table in `source-kinds.ts` — the same one `vigiles test`,
|
|
130
|
+
// `vigiles eval` and the hook/provider discovery classify by — so the patterns
|
|
131
|
+
// that credit a test and the files a hook directory refuses to compile cannot
|
|
132
|
+
// drift apart.
|
|
133
|
+
const DEFAULT_TEST_GLOBS = source_kinds_js_1.DEFAULT_TEST_GLOBS;
|
|
135
134
|
const DEFAULT_IGNORE = [
|
|
136
135
|
"node_modules/**",
|
|
137
136
|
"dist/**",
|
|
@@ -298,6 +297,10 @@ function discoverTests(basePath, globs, ignore) {
|
|
|
298
297
|
cwd: basePath,
|
|
299
298
|
ignore,
|
|
300
299
|
dot: true,
|
|
300
|
+
// `glob` defaults this to TRUE on macOS and Windows, so `a.hook.HARNESS.mjs`
|
|
301
|
+
// was a test there and unclaimed on Linux — and the classifier is
|
|
302
|
+
// case-sensitive. One answer on every OS.
|
|
303
|
+
nocase: false,
|
|
301
304
|
});
|
|
302
305
|
// Prepared ONCE per file (comment-strip + declaration parse), not once per
|
|
303
306
|
// (surface × file) pair — the matching below is quadratic by nature.
|
|
@@ -737,7 +740,7 @@ function staleRunNote(report) {
|
|
|
737
740
|
];
|
|
738
741
|
}
|
|
739
742
|
/** Names a default vitest/jest run collects — the suffixes vigiles will not use. */
|
|
740
|
-
const FOREIGN_RUNNER_SUFFIX =
|
|
743
|
+
const FOREIGN_RUNNER_SUFFIX = new RegExp(`\\.(test|spec)\\.(${source_kinds_js_1.RUNNABLE_EXTS.join("|")})$`);
|
|
741
744
|
/**
|
|
742
745
|
* The would-be colocated tests that only their NAME disqualifies.
|
|
743
746
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "33.0.0",
|
|
4
4
|
"description": "Audit, test and measure the harness your AI agent runs on — grade your CLAUDE.md / AGENTS.md, skills, subagents and hooks, run them against a scripted model, and measure whether they actually fire.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|