vigiles 32.1.0 → 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.
@@ -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
- // Mirrors src/test-coverage.ts constants. VALUES are re-declared, never imported
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 (DEFAULT_TEST_SUFFIXES.some((s) => path.endsWith(s))) {
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
  }
@@ -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
- const DEFAULT_TEST_GLOBS = [
132
- `**/*.harness.${RUNNABLE_EXTS}`,
133
- `**/*.eval.${RUNNABLE_EXTS}`,
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 = /\.(test|spec)\.(ts|mts|cts|js|mjs|cjs)$/;
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": "32.1.0",
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",