vigiles 27.3.0 โ 29.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/README.md +1 -1
- package/dist/adapter-registry.d.ts +39 -0
- package/dist/adapter-registry.js +45 -0
- package/dist/adapter.d.ts +8 -0
- package/dist/adapter.js +10 -1
- package/dist/adapters/claude-code/adapter.js +9 -2
- package/dist/adapters/claude-code/layout.d.ts +5 -0
- package/dist/adapters/claude-code/plugin-loader.d.ts +10 -1
- package/dist/adapters/claude-code/plugin-loader.js +10 -1
- package/dist/adapters/codex/adapter.js +7 -2
- package/dist/adapters/codex/layout.d.ts +51 -5
- package/dist/adapters/codex/layout.js +13 -4
- package/dist/adapters/opencode/adapter.js +6 -0
- package/dist/audit-report.template.html +1 -1
- package/dist/audit-score.d.ts +7 -0
- package/dist/audit-score.js +49 -2
- package/dist/cli-main.js +162 -39
- package/dist/core/adapter.d.ts +45 -1
- package/dist/core/compile.js +11 -1
- package/dist/core/config-schema.d.ts +244 -0
- package/dist/core/config-schema.js +452 -0
- package/dist/core/hook-program.d.ts +43 -0
- package/dist/core/hook-program.js +32 -0
- package/dist/core/refs.js +10 -1
- package/dist/core/surface-discovery.d.ts +270 -0
- package/dist/core/surface-discovery.js +425 -0
- package/dist/core/surface-scopes.d.ts +38 -1
- package/dist/core/surface-scopes.js +73 -1
- package/dist/core/symbols.d.ts +24 -2
- package/dist/core/symbols.js +66 -18
- package/dist/core/types.d.ts +36 -107
- package/dist/core/validate.d.ts +46 -18
- package/dist/core/validate.js +98 -172
- package/dist/exclude.d.ts +20 -0
- package/dist/exclude.js +11 -1
- package/dist/harness-test.js +3 -3
- package/dist/hook-install.d.ts +53 -0
- package/dist/hook-install.js +60 -0
- package/dist/hook-runtime.d.ts +2 -2
- package/dist/hook-runtime.js +82 -63
- package/dist/layout-registry.d.ts +14 -0
- package/dist/layout-registry.js +40 -0
- package/dist/load-hook.d.ts +1 -1
- package/dist/load-hook.js +2 -2
- package/dist/plugin-loader.d.ts +49 -1
- package/dist/plugin-loader.js +120 -14
- package/dist/run-hook.js +17 -1
- package/dist/scan-core.d.ts +19 -0
- package/dist/scan-core.js +30 -0
- package/dist/scan-files.js +15 -5
- package/dist/scan.d.ts +63 -0
- package/dist/scan.js +68 -12
- package/dist/score-core.js +8 -0
- package/dist/setup-plan.d.ts +2 -1
- package/dist/setup-plan.js +7 -2
- package/dist/surface-discovery-fs.d.ts +12 -0
- package/dist/surface-discovery-fs.js +108 -0
- package/dist/vigilesrc.schema.json +1689 -0
- package/package.json +10 -6
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SURFACE_SHAPES = void 0;
|
|
4
|
+
exports.isDiscoveryRoot = isDiscoveryRoot;
|
|
5
|
+
exports.discoverSurfaces = discoverSurfaces;
|
|
6
|
+
exports.layoutLocations = layoutLocations;
|
|
7
|
+
exports.layoutClaims = layoutClaims;
|
|
8
|
+
exports.declaredRootClaims = declaredRootClaims;
|
|
9
|
+
exports.declaredRootDirs = declaredRootDirs;
|
|
10
|
+
exports.unresolvedDeclaredRoots = unresolvedDeclaredRoots;
|
|
11
|
+
exports.unclaimedSurfaces = unclaimedSurfaces;
|
|
12
|
+
exports.unclaimedDirs = unclaimedDirs;
|
|
13
|
+
exports.unclaimedSurfaceFindings = unclaimedSurfaceFindings;
|
|
14
|
+
/**
|
|
15
|
+
* Surface DISCOVERY โ WHERE a repo's loadable surfaces are, answered by SHAPE
|
|
16
|
+
* over a BOUNDED set of roots, BEFORE anything asks which harness this repo is.
|
|
17
|
+
*
|
|
18
|
+
* ๐ด WHY THIS MODULE EXISTS โ THE ORDER OF THE TWO QUESTIONS WAS BACKWARDS.
|
|
19
|
+
* The tool asked "which ONE harness is this?" first and then trusted the winning
|
|
20
|
+
* {@link PluginLayout} to name the directories. `PluginLayout` does two jobs at
|
|
21
|
+
* once โ it is the DIALECT (what the harness understands) and the SEARCH PATH
|
|
22
|
+
* (where to look) โ so a repo whose skills sit somewhere no shipped layout names
|
|
23
|
+
* was not merely unread, it was graded anyway. Measured by an outside adopter
|
|
24
|
+
* (#240, vigiles 27.2.0): 37 skills, 24 over-budget descriptions and 14 missing
|
|
25
|
+
* bundled resources under `.ai/` produced
|
|
26
|
+
*
|
|
27
|
+
* Detected harness: codex
|
|
28
|
+
* Harness health: A (100/100)
|
|
29
|
+
* โ no structural issues found
|
|
30
|
+
*
|
|
31
|
+
* while the same binary pointed straight at `<repo>/.ai` graded it F (0/100).
|
|
32
|
+
* Same repo, same commit, same version โ the grade decided by one positional
|
|
33
|
+
* argument, and the one nobody would question was the wrong one.
|
|
34
|
+
*
|
|
35
|
+
* โโ THE DEPENDENCY DIRECTION THIS BUYS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
36
|
+
* Discovery is the DOMAIN's job and an adapter cannot widen it. A harness
|
|
37
|
+
* adapter answers only the pure question `claims(path)` โ "is this path mine?" โ
|
|
38
|
+
* so it can LABEL what was already found and nothing else. The property:
|
|
39
|
+
* **registering a new adapter cannot make vigiles read more in anyone's
|
|
40
|
+
* repository.** The rejected alternative (each adapter declares roots, the audit
|
|
41
|
+
* walks the union) inverts that: adding a Cursor adapter would start reading
|
|
42
|
+
* `.cursor/rules` in every user's repo, and a surface no adapter declared would
|
|
43
|
+
* stay invisible โ the blindness preserved. See `research/audit-harness-dx.md`
|
|
44
|
+
* ยง9 for the four options and why this one.
|
|
45
|
+
*
|
|
46
|
+
* And the corollary that makes it a product statement rather than a refactor: a
|
|
47
|
+
* surface NOBODY claims is a FINDING, not silence. That is this tool's own thesis
|
|
48
|
+
* applied to itself โ a passing signal standing in for work nobody did.
|
|
49
|
+
*
|
|
50
|
+
* โโ THE BOUND, AND WHY IT IS NOT A PORT โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
51
|
+
* Walking the whole repo is not an option and the reporter of #240 said why
|
|
52
|
+
* before we did: an unbounded `**\/SKILL.md` in that repo finds 53 VENDORED
|
|
53
|
+
* third-party plugin skills beside the 37 real ones and grades them as one
|
|
54
|
+
* machine. So discovery looks in the repo root, plus each of its DEPTH-1
|
|
55
|
+
* DOT-DIRECTORIES ({@link isDiscoveryRoot}). The reason that set is the right shape โ
|
|
56
|
+
* every harness convention puts its tree in a top-level dot-directory
|
|
57
|
+
* (`.claude`, `.codex`, `.agents`, `.cursor`, `.gemini`, `.opencode`,
|
|
58
|
+
* `.windsurf`, and #240's `.ai`), and an ordinary source tree does not. Cost is
|
|
59
|
+
* one `readdir` of the repo root plus one per dot-directory, then descent ONLY
|
|
60
|
+
* into a directory named by {@link SURFACE_SHAPES} โ never into `src/`,
|
|
61
|
+
* `packages/` or anything else.
|
|
62
|
+
*
|
|
63
|
+
* It is a DOMAIN CONSTANT, not a port method, and that is load-bearing: if an
|
|
64
|
+
* adapter could add a root we would be back to the inverted dependency above.
|
|
65
|
+
*
|
|
66
|
+
* โ ๏ธ KNOWN LIMITATION, NAMED RATHER THAN FAKED โ the Codex WALK-UP is still not
|
|
67
|
+
* expressed. The vendor scans `.agents/skills` in EVERY directory from the cwd up
|
|
68
|
+
* to the repository root; this bound covers `<root>/.agents/skills` only, so a
|
|
69
|
+
* subdirectory's own `.agents/skills` stays invisible. Reaching it means walking
|
|
70
|
+
* arbitrary directories, which is exactly what the paragraph above refuses. For
|
|
71
|
+
* the normal case โ `vigiles audit` at the repo root โ the root IS the whole
|
|
72
|
+
* walk-up chain, so the gap is a monorepo subpackage, not the common shape.
|
|
73
|
+
*
|
|
74
|
+
* Pure, IO-free and Node-free ON PURPOSE, exactly as `surface-scopes.ts` is: the
|
|
75
|
+
* disk loader (`src/scan.ts` via `src/surface-discovery-fs.ts`) and the browser
|
|
76
|
+
* file-map twin (`src/scan-files.ts`) each enumerate paths from their own storage
|
|
77
|
+
* and call THIS for the decision, so the pair this repo has repeatedly been
|
|
78
|
+
* bitten by fixing on one side only cannot disagree about what a surface is.
|
|
79
|
+
*/
|
|
80
|
+
const layout_js_1 = require("./layout.js");
|
|
81
|
+
/**
|
|
82
|
+
* The SHAPE of each surface kind: the directory name that holds it, and the
|
|
83
|
+
* path tail that makes a file inside it loadable.
|
|
84
|
+
*
|
|
85
|
+
* These are the CROSS-VENDOR names, not one harness's: Codex's skills live at
|
|
86
|
+
* `.agents/skills`, Claude Code's at `skills` or `.claude/skills` โ the same
|
|
87
|
+
* `skills` segment under a different root, which is why the root is the variable
|
|
88
|
+
* and the shape name is not. The tails quote the classifier the scan already
|
|
89
|
+
* uses (`makeClassifier`, scan-core.ts): a skill is `<name>/SKILL.md`, an agent
|
|
90
|
+
* is read RECURSIVELY per {@link AGENT_FILE_LEAF_RE} (do not hard-code a depth
|
|
91
|
+
* here โ that header says why), a command is any `.md` below the dir.
|
|
92
|
+
*
|
|
93
|
+
* Codex's `prompts` is deliberately absent: it is claimed by `codexLayout` and
|
|
94
|
+
* read by the loader, and giving a non-dot word like `prompts` shape status at
|
|
95
|
+
* the repo root would match ordinary prompt-engineering directories that are not
|
|
96
|
+
* a harness surface at all.
|
|
97
|
+
*/
|
|
98
|
+
exports.SURFACE_SHAPES = [
|
|
99
|
+
{ dir: "skills", kind: "skill", tail: "[^/]+/SKILL\\.md" },
|
|
100
|
+
{ dir: "agents", kind: "agent", tail: layout_js_1.AGENT_FILE_LEAF_RE },
|
|
101
|
+
{ dir: "commands", kind: "command", tail: ".+\\.md" },
|
|
102
|
+
];
|
|
103
|
+
/**
|
|
104
|
+
* Is this repo-root-relative DIRECTORY NAME one of the bounded discovery roots?
|
|
105
|
+
*
|
|
106
|
+
* The repo root itself is always one (`""`). Everything else must be a
|
|
107
|
+
* DOT-directory โ see the module header for why that is the whole bound. The
|
|
108
|
+
* caller supplies the names (a `readdir` on disk, the key set in the browser),
|
|
109
|
+
* so this function stays IO-free.
|
|
110
|
+
*/
|
|
111
|
+
function isDiscoveryRoot(name) {
|
|
112
|
+
return name === "" || (name.startsWith(".") && name !== "." && name !== "..");
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* `<root>/<shape.dir>/<tail>` for every shape โ the one place the anchor lives.
|
|
116
|
+
*
|
|
117
|
+
* Anchored at the START: the shape dir sits DIRECTLY under a discovery root,
|
|
118
|
+
* never at arbitrary depth. That is what stops a React app's `src/hooks/โฆ`-class
|
|
119
|
+
* false positive and a monorepo's `packages/x/skills/โฆ` from being graded as
|
|
120
|
+
* this repo's harness. Built once โ these carry no `g` flag, so `exec` keeps no
|
|
121
|
+
* state between calls.
|
|
122
|
+
*/
|
|
123
|
+
const SHAPE_MATCHERS = exports.SURFACE_SHAPES.map((s) => ({
|
|
124
|
+
kind: s.kind,
|
|
125
|
+
dir: s.dir,
|
|
126
|
+
re: new RegExp(`^(?:([^/]+)/)?${s.dir}/${s.tail}$`),
|
|
127
|
+
}));
|
|
128
|
+
/**
|
|
129
|
+
* Every surface a set of repo-relative paths holds, by SHAPE โ harness-blind.
|
|
130
|
+
*
|
|
131
|
+
* `paths` must already be bounded by the caller's walk; this re-checks the root
|
|
132
|
+
* rule anyway so the browser twin (which hands over the whole fetched key set)
|
|
133
|
+
* gets the same answer as the disk walk.
|
|
134
|
+
*/
|
|
135
|
+
function discoverSurfaces(paths) {
|
|
136
|
+
const out = [];
|
|
137
|
+
for (const path of paths) {
|
|
138
|
+
for (const m of SHAPE_MATCHERS) {
|
|
139
|
+
const hit = m.re.exec(path);
|
|
140
|
+
if (hit === null)
|
|
141
|
+
continue;
|
|
142
|
+
const root = hit[1] ?? "";
|
|
143
|
+
if (!isDiscoveryRoot(root))
|
|
144
|
+
continue;
|
|
145
|
+
out.push({
|
|
146
|
+
kind: m.kind,
|
|
147
|
+
path,
|
|
148
|
+
root,
|
|
149
|
+
dir: root === "" ? m.dir : `${root}/${m.dir}`,
|
|
150
|
+
});
|
|
151
|
+
break; // one path is one surface; first shape wins, as the classifier does
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
/** Trim a trailing `/`, and treat `"."`/`""` as "no location at all". */
|
|
157
|
+
function located(dir) {
|
|
158
|
+
if (dir === undefined)
|
|
159
|
+
return null;
|
|
160
|
+
const d = dir.replace(/\/+$/, "");
|
|
161
|
+
return d === "" || d === "." ? null : d;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Every repo-relative location a layout READS โ the raw material for
|
|
165
|
+
* {@link layoutClaims}.
|
|
166
|
+
*
|
|
167
|
+
* Derived from the layout rather than listed per adapter, so a layout that moves
|
|
168
|
+
* (as `codexLayout` just did, `.codex/skills` โ `.agents/skills`) moves its claim
|
|
169
|
+
* with it and the two cannot drift.
|
|
170
|
+
*
|
|
171
|
+
* โ ๏ธ `materializeRoot` goes through {@link located} because `codexLayout` sets it
|
|
172
|
+
* to `""` and a directory prefix of `""` is meaningless. It is DEFENCE IN DEPTH,
|
|
173
|
+
* not the load-bearing guard, and the difference was MEASURED rather than
|
|
174
|
+
* assumed: removing this filter alone leaves `layoutClaims(codexLayout,
|
|
175
|
+
* "src/index.ts")` at `false`, because the boundary form `path.startsWith(`${d}/`)`
|
|
176
|
+
* in {@link layoutClaims} already refuses a `""` prefix (no repo-relative path
|
|
177
|
+
* starts with `/`). Both have to go before Codex claims the whole repository โ
|
|
178
|
+
* which is the combined mutation `surface-discovery.test.ts` pins, precisely
|
|
179
|
+
* because removing either one on its own is survivable and would otherwise read
|
|
180
|
+
* as "this line is protecting us".
|
|
181
|
+
*/
|
|
182
|
+
function layoutLocations(layout) {
|
|
183
|
+
const user = located(layout.userSurfaceRoot);
|
|
184
|
+
const dirs = new Set();
|
|
185
|
+
const add = (d) => {
|
|
186
|
+
if (d !== null)
|
|
187
|
+
dirs.add(d);
|
|
188
|
+
};
|
|
189
|
+
add(located(layout.materializeRoot));
|
|
190
|
+
add(user);
|
|
191
|
+
for (const s of layout.surfaceDirs) {
|
|
192
|
+
add(located(s));
|
|
193
|
+
if (user !== null)
|
|
194
|
+
add(located(`${user}/${s}`));
|
|
195
|
+
}
|
|
196
|
+
const rules = located(layout.rulesDir);
|
|
197
|
+
if (rules !== null) {
|
|
198
|
+
add(rules);
|
|
199
|
+
if (user !== null)
|
|
200
|
+
add(`${user}/${rules}`);
|
|
201
|
+
}
|
|
202
|
+
for (const p of [
|
|
203
|
+
layout.manifestPath,
|
|
204
|
+
layout.settingsPath,
|
|
205
|
+
layout.hooksConventionPath,
|
|
206
|
+
]) {
|
|
207
|
+
const at = p.lastIndexOf("/");
|
|
208
|
+
if (at > 0)
|
|
209
|
+
add(p.slice(0, at));
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
dirs: [...dirs],
|
|
213
|
+
files: [
|
|
214
|
+
layout.instructionFile,
|
|
215
|
+
layout.mcpConfigFile,
|
|
216
|
+
layout.manifestPath,
|
|
217
|
+
layout.settingsPath,
|
|
218
|
+
layout.hooksConventionPath,
|
|
219
|
+
].filter((f) => f !== ""),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Does this layout's harness READ this repo-relative path โ "is it mine?"
|
|
224
|
+
*
|
|
225
|
+
* The ONE answer behind every adapter's `claims`, so an adapter cannot express a
|
|
226
|
+
* claim its own layout contradicts. Pure: a string question about a string, with
|
|
227
|
+
* no filesystem and no knowledge of what else exists.
|
|
228
|
+
*/
|
|
229
|
+
function layoutClaims(layout, path) {
|
|
230
|
+
const { dirs, files } = layoutLocations(layout);
|
|
231
|
+
if (files.includes(path))
|
|
232
|
+
return true;
|
|
233
|
+
return dirs.some((d) => path === d || path.startsWith(`${d}/`));
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Does a repo owner's DECLARED root cause this path to be read, under the
|
|
237
|
+
* layout of the harness it was declared UNDER?
|
|
238
|
+
*
|
|
239
|
+
* ๐ด IT CLAIMS EXACTLY WHAT IT MAKES READABLE, AND NOT ONE PATH MORE. The set is
|
|
240
|
+
* `<root>/<surfaceDir>/โฆ` for that harness's own `surfaceDirs` โ which is
|
|
241
|
+
* verbatim the set `materializeSurfaces` reads for a declared scope. Derived
|
|
242
|
+
* from the same field rather than listed twice, so the two cannot drift.
|
|
243
|
+
*
|
|
244
|
+
* The consequence is the point: a declaration silences the finding only where it
|
|
245
|
+
* actually reaches. Declaring `.ai` under `"codex"` does not silence a
|
|
246
|
+
* `.ai/skills/` tree, because Codex's skill dir is `.agents/skills` and
|
|
247
|
+
* `.ai/skills` is still read by nobody. Under the flat `surfaceRoots` key that
|
|
248
|
+
* was where the story ended โ the tool stayed quiet about a declaration that
|
|
249
|
+
* changed nothing. Now the harness is part of the declaration, so the same fact
|
|
250
|
+
* is an ERROR the owner can act on ({@link unresolvedDeclaredRoots}).
|
|
251
|
+
*
|
|
252
|
+
* โ ๏ธ A declaration is the REPO OWNER's, never an adapter's. Nothing here lets a
|
|
253
|
+
* harness widen what vigiles reads in someone else's repository โ the property
|
|
254
|
+
* the module header exists to protect. See `research/audit-harness-dx.md` ยง9.
|
|
255
|
+
*/
|
|
256
|
+
function declaredRootClaims(layout, roots, path) {
|
|
257
|
+
return declaredRootDirs(layout, roots).some((dir) => path === dir || path.startsWith(`${dir}/`));
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The repo-relative dirs a declaration REACHES: `<root>/<surfaceDir>` for every
|
|
261
|
+
* declared root crossed with this layout's own surface dirs.
|
|
262
|
+
*
|
|
263
|
+
* ONE derivation, read two ways โ {@link declaredRootClaims} asks whether a
|
|
264
|
+
* found path is in the set, {@link unresolvedDeclaredRoots} asks whether any
|
|
265
|
+
* member of the set exists on disk. Two spellings of "where does this
|
|
266
|
+
* declaration reach" is how a declaration could be claimed by one and refused by
|
|
267
|
+
* the other.
|
|
268
|
+
*/
|
|
269
|
+
function declaredRootDirs(layout, roots) {
|
|
270
|
+
const out = [];
|
|
271
|
+
for (const r of roots)
|
|
272
|
+
for (const surface of layout.surfaceDirs) {
|
|
273
|
+
const dir = located(`${r}/${surface}`);
|
|
274
|
+
if (dir !== null && !out.includes(dir))
|
|
275
|
+
out.push(dir);
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Declared roots that reach NO directory on disk โ the one silent state the
|
|
281
|
+
* nested shape would otherwise keep (#240).
|
|
282
|
+
*
|
|
283
|
+
* ๐ด WHY THIS IS AN ERROR AND NOT A WARNING. A root under a harness whose layout
|
|
284
|
+
* keeps that surface somewhere else is not a near-miss, it is a statement that
|
|
285
|
+
* cannot be true: `{"codex": {"roots": [".ai"]}}` over a `.ai/skills/` tree makes
|
|
286
|
+
* vigiles look at `.ai/.agents/skills/` and `.ai/prompts/`, finds neither, reads
|
|
287
|
+
* nothing, and changes not one line of the report. The owner wrote a line
|
|
288
|
+
* believing their skills were now graded. Silence there is the tool agreeing.
|
|
289
|
+
*
|
|
290
|
+
* โ ๏ธ IT CHECKS THE DIRECTORY, NOT ITS CONTENTS, and the difference is
|
|
291
|
+
* deliberate: an EMPTY `.ai/skills/` is a real, correctly-declared home that
|
|
292
|
+
* happens to hold nothing today, and failing a build over an empty directory
|
|
293
|
+
* would be a gate on repo state rather than on the declaration. What is refused
|
|
294
|
+
* is a declaration that names no directory at all.
|
|
295
|
+
*
|
|
296
|
+
* `dirExists` is injected (repo-relative path in, boolean out) so the rule is
|
|
297
|
+
* pure, node-free and testable without a filesystem โ the same contract every
|
|
298
|
+
* other decision in this module keeps.
|
|
299
|
+
*/
|
|
300
|
+
function unresolvedDeclaredRoots(scopes, dirExists) {
|
|
301
|
+
const out = [];
|
|
302
|
+
for (const scope of scopes) {
|
|
303
|
+
for (const root of scope.roots) {
|
|
304
|
+
const looked = declaredRootDirs(scope.layout, [root]);
|
|
305
|
+
if (looked.some((d) => dirExists(d)))
|
|
306
|
+
continue;
|
|
307
|
+
out.push(`.vigilesrc.json: harnesses["${scope.harness}"].roots names "${root}", ` +
|
|
308
|
+
`but ${scope.harness} reads no surface there โ nothing would be graded and ` +
|
|
309
|
+
`nothing would be said.\n` +
|
|
310
|
+
` Looked for: ${looked.length === 0 ? "(this harness declares no surface dirs)" : looked.map((d) => `${d}/`).join(", ")}\n` +
|
|
311
|
+
` Either create one of those, or declare "${root}" under the harness whose ` +
|
|
312
|
+
`layout does read it.`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* The discovered surfaces NO registered harness claims โ the finding.
|
|
319
|
+
*
|
|
320
|
+
* `claimers` is every registered adapter's `claims`, so "unclaimed" means "no
|
|
321
|
+
* harness vigiles knows about reads this", not "the detected harness does not".
|
|
322
|
+
* That distinction is the whole point of discovering before detecting: a repo
|
|
323
|
+
* carrying both `.claude/skills` and `.agents/skills` has each half claimed by a
|
|
324
|
+
* different adapter and neither is a finding, while #240's `.ai/skills` is
|
|
325
|
+
* claimed by nobody and becomes one.
|
|
326
|
+
*/
|
|
327
|
+
function unclaimedSurfaces(surfaces, claimers) {
|
|
328
|
+
return surfaces.filter((s) => !claimers.some((c) => c(s.path)));
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* The unclaimed surfaces, grouped into ONE finding per directory.
|
|
332
|
+
*
|
|
333
|
+
* A finding per FILE would put 37 lines in #240's report for one mistake; the
|
|
334
|
+
* actionable unit is the directory nobody reads, so that is the unit counted and
|
|
335
|
+
* the unit graded.
|
|
336
|
+
*/
|
|
337
|
+
function unclaimedDirs(unclaimed) {
|
|
338
|
+
const by = new Map();
|
|
339
|
+
for (const s of unclaimed) {
|
|
340
|
+
const prev = by.get(s.dir);
|
|
341
|
+
if (prev === undefined)
|
|
342
|
+
by.set(s.dir, { root: s.root, kind: s.kind, count: 1 });
|
|
343
|
+
else
|
|
344
|
+
prev.count += 1;
|
|
345
|
+
}
|
|
346
|
+
return [...by.entries()]
|
|
347
|
+
.map(([dir, v]) => ({ dir, root: v.root, kind: v.kind, count: v.count }))
|
|
348
|
+
.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
349
|
+
}
|
|
350
|
+
/** Plural-aware surface noun โ `1 skill` / `37 skills`. */
|
|
351
|
+
function plural(kind, n) {
|
|
352
|
+
return `${String(n)} ${kind}${n === 1 ? "" : "s"}`;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Where the given layouts DO keep the surface of one kind โ the "move it here
|
|
356
|
+
* instead" half of the finding message.
|
|
357
|
+
*
|
|
358
|
+
* ๐ด DERIVED, NOT SPELLED OUT, and the repo's own `core-not-adapter` rule is
|
|
359
|
+
* what forced it: the first draft of the message listed "`.claude/skills/`,
|
|
360
|
+
* `skills/`, `.agents/skills/`" as a literal and the linter rejected it as a
|
|
361
|
+
* Claude Code constant hard-coded in harness-agnostic code. It was right beyond
|
|
362
|
+
* the letter of the rule โ a hand-written list of homes is exactly the thing
|
|
363
|
+
* that stops being true when a layout moves (as `codexLayout` did the same day,
|
|
364
|
+
* `.codex/skills` โ `.agents/skills`), and the finding would then tell the
|
|
365
|
+
* reader to move their skills somewhere no harness reads.
|
|
366
|
+
*/
|
|
367
|
+
function knownHomes(layouts, kind) {
|
|
368
|
+
const dirOf = (l) => ({ skill: l.skillDir, agent: l.agentDir, command: l.commandDir })[kind];
|
|
369
|
+
const homes = new Set();
|
|
370
|
+
for (const l of layouts) {
|
|
371
|
+
const dir = located(dirOf(l));
|
|
372
|
+
if (dir === null)
|
|
373
|
+
continue;
|
|
374
|
+
homes.add(dir);
|
|
375
|
+
const user = located(l.userSurfaceRoot);
|
|
376
|
+
if (user !== null)
|
|
377
|
+
homes.add(`${user}/${dir}`);
|
|
378
|
+
}
|
|
379
|
+
return [...homes].sort();
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* The whole discovery verdict in one call: paths in, findings out.
|
|
383
|
+
*
|
|
384
|
+
* The ENTRY POINT both producers use (`src/scan.ts` over a bounded disk walk,
|
|
385
|
+
* `src/scan-files.ts` over the fetched key set), so the wording, the grouping
|
|
386
|
+
* and the claim rule cannot differ between the CLI and the in-browser audit.
|
|
387
|
+
*
|
|
388
|
+
* It takes LAYOUTS rather than bare predicates because the answer needs both
|
|
389
|
+
* halves of the same fact: who claims a path, and where those claimants
|
|
390
|
+
* actually keep that kind of surface. Taking them apart is how the message and
|
|
391
|
+
* the claim rule would come to disagree.
|
|
392
|
+
*
|
|
393
|
+
* `declared` is the repo owner's opt-in: one entry per harness declared in
|
|
394
|
+
* `.vigilesrc.json#harnesses`, each carrying the roots declared under it and the
|
|
395
|
+
* LAYOUT those roots are read with. It joins the claimers rather than filtering
|
|
396
|
+
* the findings afterwards, so "no harness reads this" and "this is read" stay
|
|
397
|
+
* ONE question with one answer. Omitted by the browser twin, which has no
|
|
398
|
+
* config โ see {@link declaredRootClaims} for what it does and does not silence.
|
|
399
|
+
*
|
|
400
|
+
* โ ๏ธ `exclude` needs no mention here and that is structural, not an oversight:
|
|
401
|
+
* an excluded path never reaches `paths` (the walk drops it), so it can be
|
|
402
|
+
* neither a finding nor a graded surface however it was declared.
|
|
403
|
+
*/
|
|
404
|
+
function unclaimedSurfaceFindings(paths, layouts, declared) {
|
|
405
|
+
const claimers = layouts.map((l) => (path) => layoutClaims(l, path));
|
|
406
|
+
// ONE claimer per DECLARED HARNESS, not one for "the declaration". Under the
|
|
407
|
+
// flat shape there was a single (detected layout, global roots) pair, so a repo
|
|
408
|
+
// serving one tree to two harnesses could silence the finding for at most one
|
|
409
|
+
// of them; each declaration now carries the layout it was made under.
|
|
410
|
+
for (const scope of declared ?? [])
|
|
411
|
+
if (scope.roots.length > 0)
|
|
412
|
+
claimers.push((path) => declaredRootClaims(scope.layout, scope.roots, path));
|
|
413
|
+
return unclaimedDirs(unclaimedSurfaces(discoverSurfaces(paths), claimers)).map((d) => ({
|
|
414
|
+
...d,
|
|
415
|
+
message: `${d.dir}/ holds ${plural(d.kind, d.count)} that no harness vigiles knows about reads, ` +
|
|
416
|
+
`so none of it is in this grade. Three ways out: keep it where it is and say so in ` +
|
|
417
|
+
`.vigilesrc.json (\`{"harnesses":{"claude-code":{"roots":["${d.root === "" ? "." : d.root}"]}}}\` ` +
|
|
418
|
+
`โ see docs/configuration.md), audit it on its own ` +
|
|
419
|
+
`(\`vigiles audit ${d.root === "" ? "." : d.root}\`), or move it somewhere a harness ` +
|
|
420
|
+
`loads from (${knownHomes(layouts, d.kind)
|
|
421
|
+
.map((h) => `\`${h}/\``)
|
|
422
|
+
.join(", ")}).`,
|
|
423
|
+
}));
|
|
424
|
+
}
|
|
425
|
+
//# sourceMappingURL=surface-discovery.js.map
|
|
@@ -47,6 +47,14 @@ export interface SurfaceScope {
|
|
|
47
47
|
readonly materializeUnder: string;
|
|
48
48
|
/** Human label for warnings โ `plugin` (root) or `project` (`.claude/`). */
|
|
49
49
|
readonly label: string;
|
|
50
|
+
/**
|
|
51
|
+
* This scope exists only because the REPO OWNER named its base in
|
|
52
|
+
* `.vigilesrc.json#harnesses["<name>"].roots` โ it is not a location the harness itself
|
|
53
|
+
* reads. Marked so {@link multiScopeWarning} can stay about the ambiguity it
|
|
54
|
+
* describes (plugin-vs-project, both of which a real session loads) instead of
|
|
55
|
+
* claiming the harness loads a declared root too.
|
|
56
|
+
*/
|
|
57
|
+
readonly declared?: boolean;
|
|
50
58
|
}
|
|
51
59
|
/** Which shape the audited target is, and every scope to read from it. */
|
|
52
60
|
export type SurfaceSource = {
|
|
@@ -68,7 +76,36 @@ export interface SurfaceProbe {
|
|
|
68
76
|
readonly isPluginShaped: boolean;
|
|
69
77
|
/** Some `<root>/<userSurfaceRoot>/<surface>/` holds a loadable file. */
|
|
70
78
|
readonly userHasLoadable: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* The repo owner's declared roots, normalized by {@link normalizeSurfaceRoots},
|
|
81
|
+
* in declaration order.
|
|
82
|
+
*
|
|
83
|
+
* Unlike the flags above this is NOT a probe result โ the caller passes the
|
|
84
|
+
* declaration as written and does not check whether each root holds anything.
|
|
85
|
+
* It does not have to: a declared scope is appended AFTER the no-scope
|
|
86
|
+
* fallback, so an empty one adds an empty tree and changes nothing, while a
|
|
87
|
+
* filter here would be a guard with no observable behaviour to defend.
|
|
88
|
+
*/
|
|
89
|
+
readonly declaredRoots?: readonly string[];
|
|
71
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* The repo owner's `.vigilesrc.json#harnesses["<name>"].roots`, normalized โ or
|
|
93
|
+
* dropped. (The flat top-level `surfaceRoots` key this once read was removed in
|
|
94
|
+
* the same change that nested it under a harness name.)
|
|
95
|
+
*
|
|
96
|
+
* A DECLARATION BY THE REPO OWNER, never by an adapter: the rejected option B
|
|
97
|
+
* let each harness declare roots, which inverts the dependency (registering a
|
|
98
|
+
* Cursor adapter would start reading `.cursor/rules` in everyone's repo). A key
|
|
99
|
+
* in the repo's own config cannot do that โ it widens exactly one repository,
|
|
100
|
+
* the one whose owner wrote it. See `research/audit-harness-dx.md` ยง9.
|
|
101
|
+
*
|
|
102
|
+
* Dropped rather than errored, because the failure of a bad entry is harmless
|
|
103
|
+
* (nothing extra is read) while refusing to audit over a config typo is not:
|
|
104
|
+
* absolute paths, `.`/empty, and anything with a `..` segment, which would reach
|
|
105
|
+
* OUTSIDE the audited repo and is the only entry that could do real damage.
|
|
106
|
+
* Order is kept and duplicates collapse, so the scope list is stable.
|
|
107
|
+
*/
|
|
108
|
+
export declare function normalizeSurfaceRoots(roots: readonly string[] | undefined): readonly string[];
|
|
72
109
|
/**
|
|
73
110
|
* Classify the target and list every scope to read, HIGHEST-PRECEDENCE FIRST.
|
|
74
111
|
*
|
|
@@ -106,5 +143,5 @@ export declare function assertDistinctScopeKeys(scopes: readonly SurfaceScope[],
|
|
|
106
143
|
* `unregisteredSkillFiles` already warns about for inline arm files). Say so,
|
|
107
144
|
* rather than quietly relocating one on top of the other.
|
|
108
145
|
*/
|
|
109
|
-
export declare function multiScopeWarning(
|
|
146
|
+
export declare function multiScopeWarning(allScopes: readonly SurfaceScope[], counts: Record<string, number>): string | undefined;
|
|
110
147
|
//# sourceMappingURL=surface-scopes.d.ts.map
|
|
@@ -1,9 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeSurfaceRoots = normalizeSurfaceRoots;
|
|
3
4
|
exports.surfaceSource = surfaceSource;
|
|
4
5
|
exports.scopeKey = scopeKey;
|
|
5
6
|
exports.assertDistinctScopeKeys = assertDistinctScopeKeys;
|
|
6
7
|
exports.multiScopeWarning = multiScopeWarning;
|
|
8
|
+
/**
|
|
9
|
+
* The repo owner's `.vigilesrc.json#harnesses["<name>"].roots`, normalized โ or
|
|
10
|
+
* dropped. (The flat top-level `surfaceRoots` key this once read was removed in
|
|
11
|
+
* the same change that nested it under a harness name.)
|
|
12
|
+
*
|
|
13
|
+
* A DECLARATION BY THE REPO OWNER, never by an adapter: the rejected option B
|
|
14
|
+
* let each harness declare roots, which inverts the dependency (registering a
|
|
15
|
+
* Cursor adapter would start reading `.cursor/rules` in everyone's repo). A key
|
|
16
|
+
* in the repo's own config cannot do that โ it widens exactly one repository,
|
|
17
|
+
* the one whose owner wrote it. See `research/audit-harness-dx.md` ยง9.
|
|
18
|
+
*
|
|
19
|
+
* Dropped rather than errored, because the failure of a bad entry is harmless
|
|
20
|
+
* (nothing extra is read) while refusing to audit over a config typo is not:
|
|
21
|
+
* absolute paths, `.`/empty, and anything with a `..` segment, which would reach
|
|
22
|
+
* OUTSIDE the audited repo and is the only entry that could do real damage.
|
|
23
|
+
* Order is kept and duplicates collapse, so the scope list is stable.
|
|
24
|
+
*/
|
|
25
|
+
function normalizeSurfaceRoots(roots) {
|
|
26
|
+
const out = [];
|
|
27
|
+
for (const raw of roots ?? []) {
|
|
28
|
+
const r = raw
|
|
29
|
+
.split("\\")
|
|
30
|
+
.join("/")
|
|
31
|
+
.replace(/^(?:\.\/)+/, "")
|
|
32
|
+
.replace(/\/+$/, "")
|
|
33
|
+
.trim();
|
|
34
|
+
if (r === "" || r === "." || r.startsWith("/"))
|
|
35
|
+
continue;
|
|
36
|
+
if (r.split("/").includes(".."))
|
|
37
|
+
continue;
|
|
38
|
+
if (!out.includes(r))
|
|
39
|
+
out.push(r);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
7
43
|
/**
|
|
8
44
|
* Classify the target and list every scope to read, HIGHEST-PRECEDENCE FIRST.
|
|
9
45
|
*
|
|
@@ -52,6 +88,36 @@ function surfaceSource(layout, probe) {
|
|
|
52
88
|
label: "project",
|
|
53
89
|
});
|
|
54
90
|
}
|
|
91
|
+
// The repo owner's declared roots, read with the DETECTED layout's surface
|
|
92
|
+
// dirs โ `.ai` + `skills` for Claude Code. It does not invent a dialect: the
|
|
93
|
+
// declaration says WHERE, the layout still says what a surface is and how it
|
|
94
|
+
// is read. A root already serving as a scope's base is skipped (declaring
|
|
95
|
+
// `.claude` to Claude Code is a no-op, not a second reading of one tree), and
|
|
96
|
+
// so is one equal to `materializeRoot` โ that key belongs to the scope holding
|
|
97
|
+
// it, and two scopes minting one prefix is what {@link assertDistinctScopeKeys}
|
|
98
|
+
// exists to refuse. Each keeps its OWN base as the key prefix, like a non-first
|
|
99
|
+
// plugin scope, so nothing is relocated on top of anything.
|
|
100
|
+
//
|
|
101
|
+
// ๐ด APPENDED LAST, AFTER THE NO-SCOPE FALLBACK, AND THE ORDER IS THE POINT: a
|
|
102
|
+
// declaration may only ADD. Run before the fallback, a declared root makes the
|
|
103
|
+
// list non-empty and CANCELS it โ measured on a repo whose `.claude/skills`
|
|
104
|
+
// held only a non-loadable file: adding one config line naming another
|
|
105
|
+
// directory dropped `.claude/skills/alpha/README.md` out of the loaded files.
|
|
106
|
+
// Nobody would connect that to the line they wrote. Pinned by
|
|
107
|
+
// `plugin-loader.test.ts` ("an EMPTY declared root does not cancel the project
|
|
108
|
+
// scope"), which asserts the full key list both before and after filling it.
|
|
109
|
+
for (const base of probe.declaredRoots ?? []) {
|
|
110
|
+
if (base === layout.materializeRoot)
|
|
111
|
+
continue;
|
|
112
|
+
if (scopes.some((sc) => sc.base === base))
|
|
113
|
+
continue;
|
|
114
|
+
scopes.push({
|
|
115
|
+
base,
|
|
116
|
+
materializeUnder: base,
|
|
117
|
+
label: "declared",
|
|
118
|
+
declared: true,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
55
121
|
return { kind: "scopes", scopes };
|
|
56
122
|
}
|
|
57
123
|
/** The `LoadedPlugin.files` key for one file under one scope. */
|
|
@@ -86,7 +152,13 @@ function assertDistinctScopeKeys(scopes, layoutName) {
|
|
|
86
152
|
* `unregisteredSkillFiles` already warns about for inline arm files). Say so,
|
|
87
153
|
* rather than quietly relocating one on top of the other.
|
|
88
154
|
*/
|
|
89
|
-
function multiScopeWarning(
|
|
155
|
+
function multiScopeWarning(allScopes, counts) {
|
|
156
|
+
// Declared roots are excluded, and not for tidiness: every sentence below is
|
|
157
|
+
// about what a REAL SESSION loads under two names. A harness does not load a
|
|
158
|
+
// declared root at all โ vigiles reads it because the repo owner said to โ so
|
|
159
|
+
// including one here would make the warning state a falsehood about the
|
|
160
|
+
// harness the moment someone declares `harnesses.<name>.roots`.
|
|
161
|
+
const scopes = allScopes.filter((s) => s.declared !== true);
|
|
90
162
|
if (scopes.length < 2)
|
|
91
163
|
return undefined;
|
|
92
164
|
const total = Object.values(counts).reduce((a, b) => a + b, 0);
|
package/dist/core/symbols.d.ts
CHANGED
|
@@ -1,8 +1,30 @@
|
|
|
1
1
|
import { Lang } from "@ast-grep/napi";
|
|
2
|
+
/** Which optional grammars this process actually has. Exported so a report can say so. */
|
|
3
|
+
export declare function installedGrammars(): ReadonlySet<string>;
|
|
2
4
|
/** A language key accepted by ast-grep's `parse` (core enum or registered id). */
|
|
3
5
|
type LangKey = Lang | string;
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Whether this file's language can be parsed HERE, and if not, which of the two reasons.
|
|
8
|
+
*
|
|
9
|
+
* ๐ด THE THREE CASES ARE SEPARATE MEMBERS BECAUSE THEY ARE SEPARATE FACTS. The previous
|
|
10
|
+
* signature was `LangKey | null`, where `null` meant "extension not in the table" and callers
|
|
11
|
+
* printed "Unsupported language for symbol check". Making the grammars optional would have
|
|
12
|
+
* given that same `null` a second meaning โ "the language IS ours, the package is simply not
|
|
13
|
+
* installed" โ and both callers would have kept printing the first sentence. That is the
|
|
14
|
+
* failure this codebase exists to catch: a check that did not run, reported in the words of a
|
|
15
|
+
* check that did. A union makes the compiler demand the distinction at every call site.
|
|
16
|
+
*/
|
|
17
|
+
export type LangSupport = {
|
|
18
|
+
readonly kind: "ready";
|
|
19
|
+
readonly lang: LangKey;
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: "grammar-missing";
|
|
22
|
+
readonly id: string;
|
|
23
|
+
readonly pkg: string;
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: "unsupported";
|
|
26
|
+
};
|
|
27
|
+
export declare function langForFile(file: string): LangSupport;
|
|
6
28
|
/** A symbol definition found in a file. */
|
|
7
29
|
export interface SymbolDef {
|
|
8
30
|
/** The defined identifier, e.g. "parseConfig". */
|