vigiles 31.0.0 → 31.0.1
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 +5 -3
- package/dist/adapters/claude-code/run-scripts.js +5 -3
- package/dist/cli-main.d.ts +8 -0
- package/dist/cli-main.js +296 -191
- package/dist/core/coverage.d.ts +4 -3
- package/dist/core/coverage.js +4 -3
- package/dist/core/doc-refs.d.ts +3 -1
- package/dist/core/doc-refs.js +2 -1
- package/dist/core/frame.d.ts +90 -0
- package/dist/core/frame.js +58 -0
- package/dist/core/glob-ignore.d.ts +28 -0
- package/dist/core/glob-ignore.js +42 -0
- package/dist/core/orphans.d.ts +5 -3
- package/dist/core/orphans.js +5 -4
- package/dist/exclude.d.ts +3 -5
- package/dist/exclude.js +3 -3
- package/dist/scan.d.ts +3 -3
- package/dist/scan.js +8 -7
- package/dist/test-coverage.d.ts +20 -3
- package/dist/test-coverage.js +40 -14
- package/package.json +3 -2
package/dist/core/coverage.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Configurable thresholds in .vigilesrc.json trigger warnings or errors when
|
|
9
9
|
* coverage drops below the minimum.
|
|
10
10
|
*/
|
|
11
|
+
import { type GlobIgnore } from "./glob-ignore.js";
|
|
11
12
|
import type { CoverageThresholds } from "./types.js";
|
|
12
13
|
import type { ClaudeSpec } from "./spec.js";
|
|
13
14
|
export interface CoverageMetric {
|
|
@@ -35,11 +36,11 @@ export declare function readNpmScripts(basePath: string): string[];
|
|
|
35
36
|
* Collect commands documented in specs by loading spec source files directly.
|
|
36
37
|
* Reads the structured `commands` field — no markdown parsing.
|
|
37
38
|
*/
|
|
38
|
-
export declare function collectDocumentedCommands(basePath: string, specs: ClaudeSpec[] | undefined, ignore:
|
|
39
|
+
export declare function collectDocumentedCommands(basePath: string, specs: ClaudeSpec[] | undefined, ignore: GlobIgnore): Set<string>;
|
|
39
40
|
/**
|
|
40
41
|
* Compute script coverage: what % of npm scripts are documented in specs.
|
|
41
42
|
*/
|
|
42
|
-
export declare function computeScriptCoverage(basePath: string, threshold: number | undefined, specs: ClaudeSpec[] | undefined, ignore:
|
|
43
|
+
export declare function computeScriptCoverage(basePath: string, threshold: number | undefined, specs: ClaudeSpec[] | undefined, ignore: GlobIgnore): CoverageMetric;
|
|
43
44
|
/**
|
|
44
45
|
* Compute linter rule coverage from pre-computed totals.
|
|
45
46
|
* The actual linter scanning is done by the existing discover() in cli.ts.
|
|
@@ -48,7 +49,7 @@ export declare function computeLinterRuleCoverage(enabled: number, documented: n
|
|
|
48
49
|
/**
|
|
49
50
|
* Check all coverage metrics against thresholds.
|
|
50
51
|
*/
|
|
51
|
-
export declare function checkCoverage(basePath: string, thresholds: CoverageThresholds, linterEnabled: number, linterDocumented: number, specs: ClaudeSpec[] | undefined, ignore:
|
|
52
|
+
export declare function checkCoverage(basePath: string, thresholds: CoverageThresholds, linterEnabled: number, linterDocumented: number, specs: ClaudeSpec[] | undefined, ignore: GlobIgnore): CoverageReport;
|
|
52
53
|
/**
|
|
53
54
|
* Format coverage report as human-readable text.
|
|
54
55
|
*/
|
package/dist/core/coverage.js
CHANGED
|
@@ -20,6 +20,7 @@ const node_fs_1 = require("node:fs");
|
|
|
20
20
|
const integrity_js_1 = require("./integrity.js");
|
|
21
21
|
const node_path_1 = require("node:path");
|
|
22
22
|
const glob_1 = require("glob");
|
|
23
|
+
const glob_ignore_js_1 = require("./glob-ignore.js");
|
|
23
24
|
const compile_js_1 = require("./compile.js");
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
25
26
|
// Script coverage
|
|
@@ -50,12 +51,12 @@ function collectDocumentedCommands(basePath, specs, ignore) {
|
|
|
50
51
|
// Fallback: scan compiled markdown for spec file references, then
|
|
51
52
|
// load the compiled JS spec from dist/. If that fails, try to
|
|
52
53
|
// extract commands from the compiled output (last resort).
|
|
53
|
-
// `ignore` is the repo's ExcludeSet
|
|
54
|
-
//
|
|
54
|
+
// `ignore` is the repo's `ExcludeSet.globIgnore` (src/exclude.ts, correct
|
|
55
|
+
// from any cwd) or patterns relative to `basePath`, required so this fallback cannot walk a
|
|
55
56
|
// vendored corpus the repo excluded (#192). The CLI always passes `specs`,
|
|
56
57
|
// so this branch is reached by the library path and tests only.
|
|
57
58
|
const mdFiles = (0, glob_1.globSync)("**/*.md", {
|
|
58
|
-
ignore: [
|
|
59
|
+
ignore: (0, glob_ignore_js_1.withIgnored)([], ignore),
|
|
59
60
|
cwd: basePath,
|
|
60
61
|
});
|
|
61
62
|
for (const mdFile of mdFiles) {
|
package/dist/core/doc-refs.d.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* in markdown is explicitly out of scope — use eslint-plugin-markdown or
|
|
18
18
|
* twoslash for that.
|
|
19
19
|
*/
|
|
20
|
+
import { type GlobIgnore } from "./glob-ignore.js";
|
|
20
21
|
export type DocRefKind = "enforce" | "file" | "cmd" | "ref";
|
|
21
22
|
export interface DocRef {
|
|
22
23
|
readonly file: string;
|
|
@@ -40,7 +41,8 @@ export interface DocRefReport {
|
|
|
40
41
|
}
|
|
41
42
|
export interface FindDocRefsOptions {
|
|
42
43
|
readonly basePath?: string;
|
|
43
|
-
|
|
44
|
+
/** The repo exclude (`ExcludeSet.globIgnore`), or patterns relative to `basePath`. */
|
|
45
|
+
readonly ignore?: GlobIgnore;
|
|
44
46
|
}
|
|
45
47
|
interface ExtractResult {
|
|
46
48
|
refs: DocRef[];
|
package/dist/core/doc-refs.js
CHANGED
|
@@ -29,6 +29,7 @@ const node_fs_1 = require("node:fs");
|
|
|
29
29
|
const typescript_1 = __importDefault(require("typescript"));
|
|
30
30
|
const node_path_1 = require("node:path");
|
|
31
31
|
const glob_1 = require("glob");
|
|
32
|
+
const glob_ignore_js_1 = require("./glob-ignore.js");
|
|
32
33
|
const linters_js_1 = require("./linters.js");
|
|
33
34
|
const compile_js_1 = require("./compile.js");
|
|
34
35
|
// ---------------------------------------------------------------------------
|
|
@@ -239,7 +240,7 @@ function validateRefs(refs, basePath) {
|
|
|
239
240
|
*/
|
|
240
241
|
function findDocRefs(options = {}) {
|
|
241
242
|
const basePath = options.basePath ?? process.cwd();
|
|
242
|
-
const ignore =
|
|
243
|
+
const ignore = (0, glob_ignore_js_1.withIgnored)(DEFAULT_IGNORE, options.ignore);
|
|
243
244
|
const files = (0, glob_1.globSync)("**/*.md", { cwd: basePath, ignore });
|
|
244
245
|
const allRefs = [];
|
|
245
246
|
let filesIgnored = 0;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frame of a path — WHICH DIRECTORY it is relative to — named by its TYPE
|
|
3
|
+
* rather than by a comment (#281).
|
|
4
|
+
*
|
|
5
|
+
* 🔴 WHY THIS EXISTS. Every path the CLI handled was a bare `string`, so a path
|
|
6
|
+
* relative to a nested bundle, one relative to the repository and an absolute one
|
|
7
|
+
* were the same type, and nothing stopped a caller from printing the wrong one.
|
|
8
|
+
* `overBundles` handed each check ONE `root: string` that had to be the discovery
|
|
9
|
+
* base, the base of the config's globs AND the frame of every printed path at
|
|
10
|
+
* once. Under `bundles: "all"` that root was the nested bundle, so `include`
|
|
11
|
+
* resolved from the wrong directory and `::warning file=skills/x/SKILL.md` named a
|
|
12
|
+
* file that did not exist from where the command ran — while a root-bundle skill
|
|
13
|
+
* of the same name made the annotation land on the WRONG real file, which looks
|
|
14
|
+
* like success.
|
|
15
|
+
*
|
|
16
|
+
* Three frames, two of them branded:
|
|
17
|
+
* - {@link RepoPath} — relative to {@link Frame.root}, the directory whose
|
|
18
|
+
* `.vigilesrc.json` governs the run. Every printed or annotated path.
|
|
19
|
+
* - {@link BundlePath} — relative to one {@link Bundle}'s own directory. What a
|
|
20
|
+
* per-bundle scan reports.
|
|
21
|
+
* - an absolute path — a plain `string`, the only thing the filesystem takes.
|
|
22
|
+
*
|
|
23
|
+
* The brand is `Opaque` from `ts-essentials` (a `unique symbol` key, so no object
|
|
24
|
+
* literal can forge it), imported as a TYPE ONLY: nothing of it survives the
|
|
25
|
+
* build. It must not reach a public `.d.ts` either — this module is internal and
|
|
26
|
+
* the API report (`api-surface/*.api.md`) is the gate that would show a leak.
|
|
27
|
+
*
|
|
28
|
+
* 🔴 THE ONE CONVERSION POINT. A `RepoPath` is minted here and nowhere else:
|
|
29
|
+
* {@link Frame.repo} from an absolute path, {@link Bundle.path} from a
|
|
30
|
+
* `BundlePath`. A cast to `RepoPath` outside this file is refused by ESLint
|
|
31
|
+
* (`no-restricted-syntax` in `eslint.config.mjs`), so "where did this path get its
|
|
32
|
+
* frame" always has the answer "in frame.ts".
|
|
33
|
+
*
|
|
34
|
+
* WHAT IS UNREPRESENTABLE AND WHAT IS ONLY DETECTABLE. The CLI's output boundary
|
|
35
|
+
* (`ghAnnotate`, the per-bundle check context) takes `RepoPath`, so handing it a
|
|
36
|
+
* bundle-relative or absolute string is a tsc error. Upstream of that, scan-core
|
|
37
|
+
* keeps reporting plain bundle-relative strings, because its browser twin
|
|
38
|
+
* (`scan-files.ts`) shares those shapes and has no filesystem to have a frame
|
|
39
|
+
* over. {@link Bundle.scanned} is the seam where such a string gets its frame;
|
|
40
|
+
* a relative string that is secretly repo-relative still passes it, and that
|
|
41
|
+
* residue is caught by tests, not by the compiler.
|
|
42
|
+
*
|
|
43
|
+
* NODE-FREE: path ops come from `../posix-path.js`, and the caller supplies the
|
|
44
|
+
* working directory, so this module has no process state of its own.
|
|
45
|
+
*/
|
|
46
|
+
import type { Opaque } from "ts-essentials";
|
|
47
|
+
/** A `/`-separated path relative to {@link Frame.root}. `.` is the root itself. */
|
|
48
|
+
export type RepoPath = Opaque<string, "RepoPath">;
|
|
49
|
+
/** A `/`-separated path relative to one {@link Bundle}'s own directory. */
|
|
50
|
+
export type BundlePath = Opaque<string, "BundlePath">;
|
|
51
|
+
/** One scored directory: the lint target itself, or a nested plugin bundle. */
|
|
52
|
+
export interface Bundle {
|
|
53
|
+
/** Absolute directory of the bundle. */
|
|
54
|
+
readonly abs: string;
|
|
55
|
+
/** Where the bundle sits, from the frame root (`.` when it IS the root). */
|
|
56
|
+
readonly at: RepoPath;
|
|
57
|
+
/** A path this bundle's scan reported, re-expressed from the frame root. */
|
|
58
|
+
path(p: BundlePath): RepoPath;
|
|
59
|
+
/**
|
|
60
|
+
* The seam for scan-core's plain strings, which are bundle-relative by
|
|
61
|
+
* contract (see the header). An ABSOLUTE string is accepted too and converted
|
|
62
|
+
* directly — `hook-block-ineffective` reports its script that way.
|
|
63
|
+
*/
|
|
64
|
+
scanned(p: string): RepoPath;
|
|
65
|
+
}
|
|
66
|
+
/** The single owner of "relative to which directory", built once per command. */
|
|
67
|
+
export interface Frame {
|
|
68
|
+
/** Absolute directory every {@link RepoPath} is relative to. */
|
|
69
|
+
readonly root: string;
|
|
70
|
+
/** The one conversion from an absolute path. Throws on a relative input. */
|
|
71
|
+
repo(abs: string): RepoPath;
|
|
72
|
+
/** Describe the bundle at an absolute directory, in this frame. */
|
|
73
|
+
bundle(abs: string): Bundle;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The frame for a command run from `cwd` against `target` (absolute).
|
|
77
|
+
*
|
|
78
|
+
* The root is `cwd` — where `loadConfig()` read `.vigilesrc.json` (it does not
|
|
79
|
+
* walk up; measured 2026-09-23) — UNLESS the target lies outside it. A foreign
|
|
80
|
+
* target (`vigiles lint ../other`) is somebody else's repository, and its paths
|
|
81
|
+
* mean something from ITS root; relative to `cwd` they would all start with
|
|
82
|
+
* `../`, and a foreign lint must never let the caller's own files satisfy the
|
|
83
|
+
* target's `sharedDirs` references. That exception was `sharedDirsRootFor` in
|
|
84
|
+
* the CLI, applied to `sharedDirs` alone; it is owned here now, so `include`,
|
|
85
|
+
* `exclude`, `sharedDirs` and every printed path agree on one root.
|
|
86
|
+
*/
|
|
87
|
+
export declare function frameFor(cwd: string, target: string): Frame;
|
|
88
|
+
/** The frame rooted at an absolute directory. */
|
|
89
|
+
export declare function frameAt(root: string): Frame;
|
|
90
|
+
//# sourceMappingURL=frame.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.frameFor = frameFor;
|
|
4
|
+
exports.frameAt = frameAt;
|
|
5
|
+
const posix_path_js_1 = require("../posix-path.js");
|
|
6
|
+
/**
|
|
7
|
+
* Node on Windows hands out `C:\\repo`, and `../posix-path.js` recognises only a leading `/`
|
|
8
|
+
* as absolute. The frame works in `/`-separated form, so every path entering it passes here:
|
|
9
|
+
* backslashes become `/`, and a drive-letter path (`C:/…`) counts as absolute.
|
|
10
|
+
*/
|
|
11
|
+
function slashed(p) {
|
|
12
|
+
return p.replaceAll("\\", "/");
|
|
13
|
+
}
|
|
14
|
+
function isAbs(p) {
|
|
15
|
+
return (0, posix_path_js_1.isAbsolute)(p) || /^[A-Za-z]:\//.test(p);
|
|
16
|
+
}
|
|
17
|
+
/** `rel` is `abs`'s position below `root` when it does not climb out. */
|
|
18
|
+
function isBelow(rel) {
|
|
19
|
+
return rel === "" || (rel !== ".." && !rel.startsWith("../"));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The frame for a command run from `cwd` against `target` (absolute).
|
|
23
|
+
*
|
|
24
|
+
* The root is `cwd` — where `loadConfig()` read `.vigilesrc.json` (it does not
|
|
25
|
+
* walk up; measured 2026-09-23) — UNLESS the target lies outside it. A foreign
|
|
26
|
+
* target (`vigiles lint ../other`) is somebody else's repository, and its paths
|
|
27
|
+
* mean something from ITS root; relative to `cwd` they would all start with
|
|
28
|
+
* `../`, and a foreign lint must never let the caller's own files satisfy the
|
|
29
|
+
* target's `sharedDirs` references. That exception was `sharedDirsRootFor` in
|
|
30
|
+
* the CLI, applied to `sharedDirs` alone; it is owned here now, so `include`,
|
|
31
|
+
* `exclude`, `sharedDirs` and every printed path agree on one root.
|
|
32
|
+
*/
|
|
33
|
+
function frameFor(cwd, target) {
|
|
34
|
+
return frameAt(isBelow((0, posix_path_js_1.relative)(slashed(cwd), slashed(target))) ? cwd : target);
|
|
35
|
+
}
|
|
36
|
+
/** The frame rooted at an absolute directory. */
|
|
37
|
+
function frameAt(root) {
|
|
38
|
+
const base = (0, posix_path_js_1.normalize)(slashed(root));
|
|
39
|
+
const repo = (raw) => {
|
|
40
|
+
const abs = slashed(raw);
|
|
41
|
+
if (!isAbs(abs))
|
|
42
|
+
throw new Error(`frame.repo() takes an absolute path, got "${raw}" — a relative string has no frame to convert from`);
|
|
43
|
+
return ((0, posix_path_js_1.relative)(base, abs) || ".");
|
|
44
|
+
};
|
|
45
|
+
const bundle = (raw) => {
|
|
46
|
+
const dir = (0, posix_path_js_1.normalize)(slashed(raw));
|
|
47
|
+
const path = (p) => repo((0, posix_path_js_1.join)(dir, p));
|
|
48
|
+
return {
|
|
49
|
+
abs: dir,
|
|
50
|
+
at: repo(dir),
|
|
51
|
+
path,
|
|
52
|
+
// The ONE place a plain string is taken to be bundle-relative.
|
|
53
|
+
scanned: (p) => (isAbs(slashed(p)) ? repo(p) : path(p)),
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
return { root: base, repo, bundle };
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=frame.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One `ignore` for `globSync` out of a detector's own string floor plus the
|
|
3
|
+
* repository's exclude — glob takes EITHER a pattern list OR one `IgnoreLike`,
|
|
4
|
+
* never both, so a detector that has both needs this union.
|
|
5
|
+
*
|
|
6
|
+
* 🔴 WHY THE REPO EXCLUDE ARRIVES AS AN `IgnoreLike` AND NOT AS STRINGS (#281).
|
|
7
|
+
* `ExcludeSet` used to hand detectors a string list whose patterns were relative
|
|
8
|
+
* to the repository root, with the precondition "only correct for a glob rooted
|
|
9
|
+
* AT that root" written in a comment. Two callers globbed from a nested bundle
|
|
10
|
+
* and broke it silently: a root-relative `skills/lonely` dropped
|
|
11
|
+
* `plugins/p/skills/lonely`, and the real exclusion never reached the bundle.
|
|
12
|
+
* `ExcludeSet.globIgnore` computes each candidate's position from the repo root
|
|
13
|
+
* itself, so it is correct from ANY glob `cwd`; the string face was retired.
|
|
14
|
+
*
|
|
15
|
+
* A plain string list is still accepted — for a direct library caller whose
|
|
16
|
+
* patterns are, by that caller's own contract, relative to the `cwd` it globs
|
|
17
|
+
* from. That is the one frame a string can safely carry.
|
|
18
|
+
*/
|
|
19
|
+
import { type IgnoreLike } from "glob";
|
|
20
|
+
/** What a detector takes for "also skip these". */
|
|
21
|
+
export type GlobIgnore = readonly string[] | IgnoreLike;
|
|
22
|
+
/**
|
|
23
|
+
* `floor` (patterns relative to the glob's `cwd`) plus `extra`, in the form
|
|
24
|
+
* `globSync`'s `ignore` option accepts. Stays a plain list when both halves are
|
|
25
|
+
* lists, so a string-only caller sees exactly the glob behaviour it always had.
|
|
26
|
+
*/
|
|
27
|
+
export declare function withIgnored(floor: readonly string[], extra: GlobIgnore | undefined): string[] | IgnoreLike;
|
|
28
|
+
//# sourceMappingURL=glob-ignore.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withIgnored = withIgnored;
|
|
4
|
+
/**
|
|
5
|
+
* One `ignore` for `globSync` out of a detector's own string floor plus the
|
|
6
|
+
* repository's exclude — glob takes EITHER a pattern list OR one `IgnoreLike`,
|
|
7
|
+
* never both, so a detector that has both needs this union.
|
|
8
|
+
*
|
|
9
|
+
* 🔴 WHY THE REPO EXCLUDE ARRIVES AS AN `IgnoreLike` AND NOT AS STRINGS (#281).
|
|
10
|
+
* `ExcludeSet` used to hand detectors a string list whose patterns were relative
|
|
11
|
+
* to the repository root, with the precondition "only correct for a glob rooted
|
|
12
|
+
* AT that root" written in a comment. Two callers globbed from a nested bundle
|
|
13
|
+
* and broke it silently: a root-relative `skills/lonely` dropped
|
|
14
|
+
* `plugins/p/skills/lonely`, and the real exclusion never reached the bundle.
|
|
15
|
+
* `ExcludeSet.globIgnore` computes each candidate's position from the repo root
|
|
16
|
+
* itself, so it is correct from ANY glob `cwd`; the string face was retired.
|
|
17
|
+
*
|
|
18
|
+
* A plain string list is still accepted — for a direct library caller whose
|
|
19
|
+
* patterns are, by that caller's own contract, relative to the `cwd` it globs
|
|
20
|
+
* from. That is the one frame a string can safely carry.
|
|
21
|
+
*/
|
|
22
|
+
const glob_1 = require("glob");
|
|
23
|
+
function isPatternList(x) {
|
|
24
|
+
return Array.isArray(x);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* `floor` (patterns relative to the glob's `cwd`) plus `extra`, in the form
|
|
28
|
+
* `globSync`'s `ignore` option accepts. Stays a plain list when both halves are
|
|
29
|
+
* lists, so a string-only caller sees exactly the glob behaviour it always had.
|
|
30
|
+
*/
|
|
31
|
+
function withIgnored(floor, extra) {
|
|
32
|
+
if (extra === undefined)
|
|
33
|
+
return [...floor];
|
|
34
|
+
if (isPatternList(extra))
|
|
35
|
+
return [...floor, ...extra];
|
|
36
|
+
const own = new glob_1.Ignore([...floor], {});
|
|
37
|
+
return {
|
|
38
|
+
ignored: (p) => own.ignored(p) || extra.ignored?.(p) === true,
|
|
39
|
+
childrenIgnored: (p) => own.childrenIgnored(p) || extra.childrenIgnored?.(p) === true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=glob-ignore.js.map
|
package/dist/core/orphans.d.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* references (markdown links and backtick paths). Works against source
|
|
12
12
|
* README plus compiled CLAUDE.md — no spec loading required.
|
|
13
13
|
*/
|
|
14
|
+
import { type GlobIgnore } from "./glob-ignore.js";
|
|
14
15
|
import type { PluginLayout } from "./layout.js";
|
|
15
16
|
export interface OrphanReport {
|
|
16
17
|
/** Include globs that were scanned. */
|
|
@@ -35,12 +36,13 @@ export interface FindOrphansOptions {
|
|
|
35
36
|
/** Glob patterns to exclude within the include scope (orphan CANDIDACY only). */
|
|
36
37
|
readonly exclude?: readonly string[];
|
|
37
38
|
/**
|
|
38
|
-
* The repo-wide `.vigilesrc.json#exclude` (
|
|
39
|
-
* src/exclude.ts
|
|
39
|
+
* The repo-wide `.vigilesrc.json#exclude` (`ExcludeSet.globIgnore`,
|
|
40
|
+
* src/exclude.ts — correct from any glob cwd; a plain list is relative to
|
|
41
|
+
* `basePath`). Applied to BOTH walks — candidates AND the reference scan —
|
|
40
42
|
* so an excluded corpus can neither be an orphan nor keep one alive (#192).
|
|
41
43
|
* The CLI always passes it; a direct library caller may omit it.
|
|
42
44
|
*/
|
|
43
|
-
readonly repoExclude?:
|
|
45
|
+
readonly repoExclude?: GlobIgnore;
|
|
44
46
|
/**
|
|
45
47
|
* Harnesses whose surface files (instruction file, `SKILL.md`, subagents,
|
|
46
48
|
* commands) are load-bearing by location and thus never orphan CANDIDATES
|
package/dist/core/orphans.js
CHANGED
|
@@ -18,6 +18,7 @@ exports.formatOrphanReport = formatOrphanReport;
|
|
|
18
18
|
const node_fs_1 = require("node:fs");
|
|
19
19
|
const node_path_1 = require("node:path");
|
|
20
20
|
const glob_1 = require("glob");
|
|
21
|
+
const glob_ignore_js_1 = require("./glob-ignore.js");
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
22
23
|
// Internals
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
@@ -100,7 +101,7 @@ function isOrphanExempt(absPath) {
|
|
|
100
101
|
function collectDocs(basePath, include, ignore, layouts) {
|
|
101
102
|
const docs = new Set();
|
|
102
103
|
for (const pattern of include) {
|
|
103
|
-
for (const p of (0, glob_1.globSync)(pattern, { cwd: basePath, ignore
|
|
104
|
+
for (const p of (0, glob_1.globSync)(pattern, { cwd: basePath, ignore })) {
|
|
104
105
|
if (isHarnessLoadedFile(p, layouts))
|
|
105
106
|
continue; // harness files are never orphans
|
|
106
107
|
if (isOrphanExempt((0, node_path_1.resolve)(basePath, p)))
|
|
@@ -160,13 +161,13 @@ function findOrphanDocs(options = {}) {
|
|
|
160
161
|
// CANDIDATE nor a SOURCE of references (a link from inside it must not keep a
|
|
161
162
|
// doc alive). The rule's own `exclude` only narrows candidacy: a doc kept out
|
|
162
163
|
// of the orphan list can still reference others. Union, never override.
|
|
163
|
-
const repoExclude = options.repoExclude
|
|
164
|
-
const ignore = [...DEFAULT_IGNORE, ...
|
|
164
|
+
const repoExclude = options.repoExclude;
|
|
165
|
+
const ignore = (0, glob_ignore_js_1.withIgnored)([...DEFAULT_IGNORE, ...userExclude], repoExclude);
|
|
165
166
|
const layouts = options.layouts ?? [];
|
|
166
167
|
const allDocs = collectDocs(basePath, include, ignore, layouts);
|
|
167
168
|
const allMarkdown = (0, glob_1.globSync)("**/*.md", {
|
|
168
169
|
cwd: basePath,
|
|
169
|
-
ignore:
|
|
170
|
+
ignore: (0, glob_ignore_js_1.withIgnored)(DEFAULT_IGNORE, repoExclude),
|
|
170
171
|
});
|
|
171
172
|
const referencedBy = new Map();
|
|
172
173
|
for (const mdPath of allMarkdown) {
|
package/dist/exclude.d.ts
CHANGED
|
@@ -8,12 +8,10 @@ export interface ExcludeSet {
|
|
|
8
8
|
/** The user's patterns, as written (for messages). */
|
|
9
9
|
readonly patterns: readonly string[];
|
|
10
10
|
/**
|
|
11
|
-
* The
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* The function face for `globSync`, correct whatever the glob's `cwd` is. A
|
|
12
|
+
* bare directory name excludes its subtree (`bench` → `bench`, `bench/**`),
|
|
13
|
+
* which is what "tsconfig-style" promises.
|
|
14
14
|
*/
|
|
15
|
-
readonly ignore: readonly string[];
|
|
16
|
-
/** The function face for `globSync`, correct whatever the glob's `cwd` is. */
|
|
17
15
|
readonly globIgnore: IgnoreLike;
|
|
18
16
|
/** Is this root-relative path excluded (floor or user pattern)? */
|
|
19
17
|
matches(rel: string): boolean;
|
package/dist/exclude.js
CHANGED
|
@@ -4,8 +4,9 @@ exports.excludesNothing = exports.EXCLUDE_FLOOR = void 0;
|
|
|
4
4
|
exports.excludeSet = excludeSet;
|
|
5
5
|
exports.excludedBy = excludedBy;
|
|
6
6
|
/**
|
|
7
|
-
* The ONE exclusion policy for every walk that polices the user's repository (#192) — the parsed `.vigilesrc.json#exclude` as an `ExcludeSet`, built ONCE where `loadConfig()` runs and taken as a REQUIRED parameter by every in-scope discovery (`findSpecs`, `findInstructionFiles`, `discoverNestedBundles`, `collectDocumentedRules`, `gatherInstructionFiles` in cli.ts; the
|
|
8
|
-
*
|
|
7
|
+
* The ONE exclusion policy for every walk that polices the user's repository (#192) — the parsed `.vigilesrc.json#exclude` as an `ExcludeSet`, built ONCE where `loadConfig()` runs and taken as a REQUIRED parameter by every in-scope discovery (`findSpecs`, `findInstructionFiles`, `discoverNestedBundles`, `collectDocumentedRules`, `gatherInstructionFiles` in cli.ts; the `globIgnore` face handed to `findDocRefs`, `findOrphanDocs` (`repoExclude`), `discoverScripts`, `computeScriptCoverage`; the whole set to `findUntestedSurfaces`/`skillTestNudge`/`scanPlugin`).
|
|
8
|
+
* Faces: `globIgnore` (an `IgnoreLike` keyed on the path's position relative to the REPO root, so a glob rooted anywhere — `vigiles lint some/dir`, a nested bundle — still applies a root-relative exclude), `matches`/`explain` (a root-relative path), and `excludedBy` below (an absolute path).
|
|
9
|
+
* 🔴 There is deliberately NO string-list face any more (#281). It was `ignore`, correct only for a glob rooted AT `root` — a precondition that lived in a comment, and two callers that globbed from a nested bundle broke it: the repo exclude never reached the bundle, and a root-relative pattern aliased into it. `core/glob-ignore.ts` unions a detector's own string floor with `globIgnore` instead.
|
|
9
10
|
* A bare directory name excludes its subtree, as tsconfig/ESLint do — measured 2026-09-03: glob's own string `ignore` treated `bench` and `bench/` as matching NOTHING while the minimatch helper in `discoverNestedBundles` accepted them, so the two walks that honoured `exclude` disagreed.
|
|
10
11
|
* The floor (node_modules/dist/.git/.vigiles) lives here, not per walk.
|
|
11
12
|
* `exclude` filters DISCOVERY only: an explicitly named path is processed and ONE line names the pattern it matched (rg/tsc semantics with ESLint's loudness; never prettier's silent 'all clean').
|
|
@@ -57,7 +58,6 @@ function excludeSet(root, patterns) {
|
|
|
57
58
|
return {
|
|
58
59
|
root,
|
|
59
60
|
patterns: user,
|
|
60
|
-
ignore: [...exports.EXCLUDE_FLOOR, ...user.flatMap((p) => [p, `${p}/**`])],
|
|
61
61
|
globIgnore: {
|
|
62
62
|
ignored: (p) => matches(relOf(p)),
|
|
63
63
|
childrenIgnored: (p) => matches(relOf(p)),
|
package/dist/scan.d.ts
CHANGED
|
@@ -423,9 +423,9 @@ export declare function scanPlugin(dir: string, layout: PluginLayout, dialect: H
|
|
|
423
423
|
* would be twenty-odd mechanical edits for one behavioural change.
|
|
424
424
|
*
|
|
425
425
|
* ⚠️ Omitting it is NOT "the repo excludes nothing" — it is "this caller has
|
|
426
|
-
* no ExcludeSet to give", and the walk then reads everything.
|
|
427
|
-
* `
|
|
428
|
-
*
|
|
426
|
+
* no ExcludeSet to give", and the walk then reads everything. `audit` and
|
|
427
|
+
* every `lint` rule checker supply one (the checkers through the per-bundle
|
|
428
|
+
* context `overBundles` builds, so a new checker cannot forget it).
|
|
429
429
|
*/
|
|
430
430
|
excludes?: ExcludeSet;
|
|
431
431
|
/**
|
package/dist/scan.js
CHANGED
|
@@ -230,13 +230,14 @@ function scanPlugin(dir, layout, dialect, opts = {}) {
|
|
|
230
230
|
const coverage = (0, test_coverage_js_1.findUntestedSurfaces)({
|
|
231
231
|
basePath: dir,
|
|
232
232
|
layout: lay,
|
|
233
|
-
// The SAME `.vigilesrc.json#exclude
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
|
|
233
|
+
// The SAME `.vigilesrc.json#exclude`. Untested-surface discovery is a
|
|
234
|
+
// second walk over the same trees, so leaving it out would have excluded a
|
|
235
|
+
// skill from the GRADE while still naming it in "Untested surfaces: 1" — a
|
|
236
|
+
// report contradicting itself about whether the file exists. Passed WHOLE,
|
|
237
|
+
// not as a pattern list: `dir` is often not the repo root (`audit plugins/p`),
|
|
238
|
+
// and a root-relative list globbed from here matched nothing it named —
|
|
239
|
+
// `inventory.skills: 1` beside `untested: 2` (#281, D7).
|
|
240
|
+
excludes: opts.excludes,
|
|
240
241
|
});
|
|
241
242
|
const caveats = (0, test_coverage_js_1.coverageCaveats)(coverage);
|
|
242
243
|
return {
|
package/dist/test-coverage.d.ts
CHANGED
|
@@ -60,6 +60,8 @@
|
|
|
60
60
|
* magnitude in cost without saying which. {@link UntestedReport} therefore carries
|
|
61
61
|
* a per-tier {@link CoverageTier} alongside the (unchanged) union fields.
|
|
62
62
|
*/
|
|
63
|
+
import { type ExcludeSet } from "./exclude.js";
|
|
64
|
+
import type { RepoPath } from "./core/frame.js";
|
|
63
65
|
import { type PluginLayout } from "./core/layout.js";
|
|
64
66
|
import { type CoverageEvidence, type EvidenceCounts } from "./coverage-evidence.js";
|
|
65
67
|
export type SurfaceKind = "skill" | "agent" | "hook";
|
|
@@ -185,8 +187,14 @@ export interface UntestedReport {
|
|
|
185
187
|
readonly evals: CoverageTier;
|
|
186
188
|
}
|
|
187
189
|
export interface TestCoverageOptions {
|
|
188
|
-
/**
|
|
190
|
+
/** The bundle whose surfaces are discovered. Defaults to `process.cwd()`. */
|
|
189
191
|
readonly basePath?: string;
|
|
192
|
+
/**
|
|
193
|
+
* The directory `include`, `exclude`, the coverage artifact and every REPORTED
|
|
194
|
+
* path are relative to — where `.vigilesrc.json` lives. Defaults to `basePath`.
|
|
195
|
+
* Differs from it for a nested bundle under `bundles: "all"` (#281).
|
|
196
|
+
*/
|
|
197
|
+
readonly root?: string;
|
|
190
198
|
/** Scan skills under `skills/` and `.claude/skills/`. Default true. */
|
|
191
199
|
readonly skills?: boolean;
|
|
192
200
|
/** Scan subagents under `agents/` and `.claude/agents/`. Default true. */
|
|
@@ -210,8 +218,17 @@ export interface TestCoverageOptions {
|
|
|
210
218
|
* `mjs` was silently ignored on any TypeScript-shaped repo.
|
|
211
219
|
*/
|
|
212
220
|
readonly testExtension?: string;
|
|
213
|
-
/**
|
|
221
|
+
/**
|
|
222
|
+
* The rule's own `exclude` globs, relative to `root` (added to
|
|
223
|
+
* node_modules/dist/.git/.vigiles). NARROWS on top of {@link excludes}.
|
|
224
|
+
*/
|
|
214
225
|
readonly exclude?: readonly string[];
|
|
226
|
+
/**
|
|
227
|
+
* The repo-wide `.vigilesrc.json#exclude`. It carries its own root, so it is
|
|
228
|
+
* applied correctly whichever directory is being discovered — a string list
|
|
229
|
+
* here was correct only when that directory WAS the repo root (#281).
|
|
230
|
+
*/
|
|
231
|
+
readonly excludes?: ExcludeSet;
|
|
215
232
|
/**
|
|
216
233
|
* Harness layout — where skills/agents live, the plugin-root token, the
|
|
217
234
|
* manifest/settings paths. Defaults to Claude Code; a non-CC adapter passes its
|
|
@@ -266,7 +283,7 @@ export declare function coverageEvidenceCounts(report: UntestedReport): Evidence
|
|
|
266
283
|
* Returns `null` when the edited file isn't a skill/agent surface, or when it is
|
|
267
284
|
* covered on both tiers. Never throws — a nudge must not disrupt an edit.
|
|
268
285
|
*/
|
|
269
|
-
export declare function skillTestNudge(filePath:
|
|
286
|
+
export declare function skillTestNudge(filePath: RepoPath, options: TestCoverageOptions): string | null;
|
|
270
287
|
/**
|
|
271
288
|
* What the PAID eval tier measures for a surface of this kind — or `null` when
|
|
272
289
|
* it measures nothing for it.
|
package/dist/test-coverage.js
CHANGED
|
@@ -71,10 +71,13 @@ exports.coverageCaveats = coverageCaveats;
|
|
|
71
71
|
exports.formatUntestedReport = formatUntestedReport;
|
|
72
72
|
const node_fs_1 = require("node:fs");
|
|
73
73
|
const node_path_1 = require("node:path");
|
|
74
|
+
const minimatch_1 = require("minimatch");
|
|
74
75
|
const test_file_ext_js_1 = require("./core/test-file-ext.js");
|
|
75
76
|
const assert_never_js_1 = require("./core/assert-never.js");
|
|
76
77
|
const ts_runner_caps_js_1 = require("./ts-runner-caps.js");
|
|
77
78
|
const glob_1 = require("glob");
|
|
79
|
+
const glob_ignore_js_1 = require("./core/glob-ignore.js");
|
|
80
|
+
const exclude_js_1 = require("./exclude.js");
|
|
78
81
|
const layout_js_1 = require("./core/layout.js");
|
|
79
82
|
const coverage_evidence_js_1 = require("./coverage-evidence.js");
|
|
80
83
|
const coverage_artifact_js_1 = require("./coverage-artifact.js");
|
|
@@ -427,27 +430,47 @@ function tierOf(considered, tests, index, tier, globs) {
|
|
|
427
430
|
*/
|
|
428
431
|
function findUntestedSurfaces(options) {
|
|
429
432
|
const basePath = options.basePath ?? process.cwd();
|
|
433
|
+
const root = options.root ?? basePath;
|
|
430
434
|
const { layout } = options;
|
|
431
|
-
const
|
|
435
|
+
const ruleIgnore = [...DEFAULT_IGNORE, ...(options.exclude ?? [])];
|
|
436
|
+
const ignore = (0, glob_ignore_js_1.withIgnored)(ruleIgnore, options.excludes?.globIgnore);
|
|
432
437
|
const globs = options.include ?? DEFAULT_TEST_GLOBS;
|
|
433
|
-
|
|
438
|
+
// #281: surfaces are FOUND under the bundle and then re-expressed from `root`,
|
|
439
|
+
// so every later comparison (colocation, `{surface}` globs, the run index,
|
|
440
|
+
// exclude, the printed path) happens in ONE frame.
|
|
441
|
+
const prefix = (0, node_path_1.relative)(root, basePath).split(node_path_1.sep).join("/");
|
|
442
|
+
const toRoot = (s) => prefix === "" ? s : { ...s, path: `${prefix}/${s.path}` };
|
|
443
|
+
const repoExcluded = (0, exclude_js_1.excludedBy)(options.excludes);
|
|
444
|
+
const excluded = (p) => ruleIgnore.some((g) => (0, minimatch_1.minimatch)(p, g, { dot: true })) ||
|
|
445
|
+
repoExcluded((0, node_path_1.join)(root, p));
|
|
446
|
+
// Discovery globs from the BUNDLE, so it gets only the frame-free floor; the
|
|
447
|
+
// rule's patterns are root-relative and the repo's carry their own root, so
|
|
448
|
+
// both are applied below, to the root-relative path.
|
|
449
|
+
const floor = [...DEFAULT_IGNORE];
|
|
450
|
+
const found = [];
|
|
434
451
|
if (options.skills !== false)
|
|
435
|
-
|
|
452
|
+
found.push(...discoverSkills(basePath, floor, layout));
|
|
436
453
|
if (options.agents !== false)
|
|
437
|
-
|
|
454
|
+
found.push(...discoverAgents(basePath, floor, layout));
|
|
438
455
|
if (options.hooks !== false)
|
|
439
|
-
|
|
456
|
+
found.push(...discoverHooks(basePath, layout));
|
|
457
|
+
// Hooks were never subject to `ignore` (a compiled hook lives under the
|
|
458
|
+
// default-ignored `.vigiles/`); keep that, and re-apply exclude to the kinds
|
|
459
|
+
// whose discovery glob used it — now from `root`, where the patterns live.
|
|
460
|
+
const surfaces = found
|
|
461
|
+
.map(toRoot)
|
|
462
|
+
.filter((s) => s.kind === "hook" || !excluded(s.path));
|
|
440
463
|
// Every skill/agent/hook is held to the requirement — only an explicit
|
|
441
464
|
// `vigiles:ignore-test` marker exempts a surface (a visible, deliberate skip).
|
|
442
465
|
const considered = surfaces.filter((s) => !s.ignored);
|
|
443
466
|
const exempt = surfaces.length - considered.length;
|
|
444
|
-
const tests = discoverTests(
|
|
467
|
+
const tests = discoverTests(root, globs, ignore);
|
|
445
468
|
const split = partitionTests(tests);
|
|
446
469
|
// The run record, if there is one. NO artifact ⇒ an empty index ⇒ every
|
|
447
470
|
// decision below falls through to colocation, byte-for-byte as before: a fresh
|
|
448
471
|
// clone and someone else's repo must not get one extra nudge from this tier.
|
|
449
|
-
const runIndex = (0, coverage_artifact_js_1.indexRuns)((0, coverage_artifact_js_1.readCoverageArtifact)(
|
|
450
|
-
const abs = (0, node_path_1.join)(
|
|
472
|
+
const runIndex = (0, coverage_artifact_js_1.indexRuns)((0, coverage_artifact_js_1.readCoverageArtifact)(root), (p) => {
|
|
473
|
+
const abs = (0, node_path_1.join)(root, p);
|
|
451
474
|
return (0, node_fs_1.existsSync)(abs) ? (0, coverage_artifact_js_1.surfaceSha)(read(abs)) : null;
|
|
452
475
|
},
|
|
453
476
|
// The SCRIPT that did the exercising has to still be here too — a deleted or
|
|
@@ -455,7 +478,7 @@ function findUntestedSurfaces(options) {
|
|
|
455
478
|
// record is permanent, unfalsifiable coverage. `canonicalScript` first: one
|
|
456
479
|
// file has several legitimate spellings (`x.mjs`, `./x.mjs`, absolute), and
|
|
457
480
|
// the artifact records whichever one was typed.
|
|
458
|
-
(by) => (0, node_fs_1.existsSync)((0, node_path_1.join)(
|
|
481
|
+
(by) => (0, node_fs_1.existsSync)((0, node_path_1.join)(root, (0, coverage_artifact_js_1.canonicalScript)(by, root))));
|
|
459
482
|
const union = tierOf(considered, tests, runIndex, undefined, globs);
|
|
460
483
|
return {
|
|
461
484
|
total: considered.length,
|
|
@@ -477,8 +500,8 @@ function findUntestedSurfaces(options) {
|
|
|
477
500
|
}),
|
|
478
501
|
legacyCoversFiles: tests
|
|
479
502
|
.map((t) => t.path)
|
|
480
|
-
.filter((path) => read((0, node_path_1.join)(
|
|
481
|
-
retiredTestNames: retiredTestNamesFor(
|
|
503
|
+
.filter((path) => read((0, node_path_1.join)(root, path)).includes(LEGACY_COVERS)),
|
|
504
|
+
retiredTestNames: retiredTestNamesFor(root, union.untested),
|
|
482
505
|
decisions: union.decisions,
|
|
483
506
|
harness: tierOf(considered, split.harness, runIndex, "harness", globs),
|
|
484
507
|
evals: tierOf(considered, split.evals, runIndex, "eval", globs),
|
|
@@ -549,9 +572,12 @@ function skillTestNudge(filePath, options) {
|
|
|
549
572
|
catch {
|
|
550
573
|
return null; // a broken scan must never surface as a broken edit
|
|
551
574
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
575
|
+
// 🔴 EQUALITY, IN ONE FRAME (#281, D6). This used to accept any path ENDING in
|
|
576
|
+
// a surface's path, so editing `plugins/p/skills/x/SKILL.md` matched the root
|
|
577
|
+
// bundle's `skills/x/SKILL.md` and reported THAT skill's coverage as the
|
|
578
|
+
// edited one's. The caller now hands a `RepoPath` and the report's paths are
|
|
579
|
+
// relative to the same `root`, so a suffix has nothing left to rescue.
|
|
580
|
+
const isTarget = (s) => s.path === filePath;
|
|
555
581
|
const untested = report.untested.find(isTarget);
|
|
556
582
|
if (untested)
|
|
557
583
|
return (`vigiles: you edited ${untested.path}, and nothing measures whether it ` +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "31.0.
|
|
3
|
+
"version": "31.0.1",
|
|
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",
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
"@typescript-eslint/eslint-plugin": "^8.58.0",
|
|
110
110
|
"@typescript-eslint/parser": "^8.58.0",
|
|
111
111
|
"@vitest/coverage-v8": "^4.1.8",
|
|
112
|
-
"conventional-changelog-conventionalcommits": "
|
|
112
|
+
"conventional-changelog-conventionalcommits": "9.3.1",
|
|
113
113
|
"eslint": "^10.1.0",
|
|
114
114
|
"eslint-import-resolver-typescript": "^4.4.5",
|
|
115
115
|
"eslint-plugin-boundaries": "^6.0.2",
|
|
@@ -118,6 +118,7 @@
|
|
|
118
118
|
"jest": "^30.4.2",
|
|
119
119
|
"picomatch": "^4.0.4",
|
|
120
120
|
"prettier": "^3.8.1",
|
|
121
|
+
"semantic-release": "25.0.9",
|
|
121
122
|
"tsx": "^4.21.0",
|
|
122
123
|
"typedoc": "^0.28.19",
|
|
123
124
|
"typescript": "^5.9.3",
|