vigiles 28.0.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.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/dist/adapter-registry.d.ts +39 -0
  3. package/dist/adapter-registry.js +45 -0
  4. package/dist/adapter.d.ts +8 -0
  5. package/dist/adapter.js +10 -1
  6. package/dist/adapters/claude-code/adapter.js +6 -0
  7. package/dist/adapters/claude-code/layout.d.ts +5 -0
  8. package/dist/adapters/claude-code/plugin-loader.d.ts +10 -1
  9. package/dist/adapters/claude-code/plugin-loader.js +10 -1
  10. package/dist/adapters/codex/adapter.js +6 -0
  11. package/dist/adapters/codex/layout.d.ts +51 -5
  12. package/dist/adapters/codex/layout.js +13 -4
  13. package/dist/adapters/opencode/adapter.js +6 -0
  14. package/dist/audit-report.template.html +1 -1
  15. package/dist/audit-score.d.ts +7 -0
  16. package/dist/audit-score.js +49 -2
  17. package/dist/cli-main.js +82 -20
  18. package/dist/core/adapter.d.ts +23 -0
  19. package/dist/core/compile.js +11 -1
  20. package/dist/core/config-schema.d.ts +244 -0
  21. package/dist/core/config-schema.js +452 -0
  22. package/dist/core/refs.js +10 -1
  23. package/dist/core/surface-discovery.d.ts +270 -0
  24. package/dist/core/surface-discovery.js +425 -0
  25. package/dist/core/surface-scopes.d.ts +38 -1
  26. package/dist/core/surface-scopes.js +73 -1
  27. package/dist/core/symbols.d.ts +24 -2
  28. package/dist/core/symbols.js +66 -18
  29. package/dist/core/types.d.ts +36 -107
  30. package/dist/core/validate.d.ts +36 -22
  31. package/dist/core/validate.js +88 -176
  32. package/dist/exclude.d.ts +20 -0
  33. package/dist/exclude.js +11 -1
  34. package/dist/layout-registry.d.ts +14 -0
  35. package/dist/layout-registry.js +40 -0
  36. package/dist/plugin-loader.d.ts +49 -1
  37. package/dist/plugin-loader.js +120 -14
  38. package/dist/scan-core.d.ts +19 -0
  39. package/dist/scan-core.js +30 -0
  40. package/dist/scan-files.js +15 -5
  41. package/dist/scan.d.ts +63 -0
  42. package/dist/scan.js +68 -12
  43. package/dist/score-core.js +8 -0
  44. package/dist/setup-plan.d.ts +2 -1
  45. package/dist/setup-plan.js +7 -2
  46. package/dist/surface-discovery-fs.d.ts +12 -0
  47. package/dist/surface-discovery-fs.js +108 -0
  48. package/dist/vigilesrc.schema.json +1689 -0
  49. package/package.json +10 -6
@@ -0,0 +1,452 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.REPLACED_KEYS = exports.VigilesConfigError = exports.vigilesConfigSchema = exports.RULE_NAMES = void 0;
4
+ exports.replacedKeyMessage = replacedKeyMessage;
5
+ exports.formatConfigIssues = formatConfigIssues;
6
+ /**
7
+ * `.vigilesrc.json`, AS A SCHEMA — the one place the config's shape, its
8
+ * defaults and its error messages live.
9
+ *
10
+ * 🔴 WHY A SCHEMA AND NOT THE HAND-WRITTEN CHECKS IT REPLACED. `loadConfig` used
11
+ * to coerce the keys it happened to remember (`asStringArray` on three of them),
12
+ * spread everything else through untouched, and say nothing at all about a key
13
+ * it had never heard of. MEASURED on this repo's own CLI before the change:
14
+ *
15
+ * ```
16
+ * $ echo '{"surfaceRootz":[".ai"]}' > .vigilesrc.json && vigiles audit .
17
+ * (no complaint about the unknown key — exit 0)
18
+ * ```
19
+ *
20
+ * A key the tool does not read is a line the user believes is working. That is
21
+ * the product's own subject — a passing signal standing in for work nobody did —
22
+ * happening inside the tool, so the shape is now DECLARED and anything outside
23
+ * it is named out loud.
24
+ *
25
+ * THE TYPE IS DERIVED FROM THIS, not written beside it: `VigilesConfig` is
26
+ * `z.infer<typeof vigilesConfigSchema>` (see `./types.ts`), so a field cannot
27
+ * exist in the type and not in the validator, which is how `surfaceRoots` ended
28
+ * up documented in `docs/cli.md` for a week after it stopped being read.
29
+ *
30
+ * DEFAULTS LIVE HERE TOO, and that is what makes the derivation exact. Every key
31
+ * the loaded config is guaranteed to carry (`rules`, `files`, `ruleMarkers`)
32
+ * carries a Zod `.default(...)`, and Zod's inferred OUTPUT type for a defaulted
33
+ * field is non-optional — so `z.infer` reproduces the old
34
+ * `rules: Required<RulesConfig>` exactly, rather than approximating it. Parsing
35
+ * `{}` yields byte-for-byte the old `DEFAULT_CONFIG`.
36
+ *
37
+ * ⚠️ ZOD COSTS ~45 ms TO IMPORT (measured, Zod 4.6.5), AND IT IS IMPORTED
38
+ * NORMALLY — `src/core/validate.ts` has a top-level `import`, not a deferred
39
+ * `require`. The deferral was tried and is recorded there rather than here,
40
+ * because the reason it was dropped is a property of the two worlds this code
41
+ * runs in, not of this file. What matters here: the genuinely hot rail — a
42
+ * compiled hook's decision, `vigiles hook-runtime run-program`, one fresh
43
+ * process per matching tool call — never loads the verb barrel and therefore
44
+ * never loads this module (`src/cli.ts` branches first;
45
+ * `src/hook-runtime-graph.test.ts` fails the day that stops being true). The
46
+ * rails that DO load the barrel already pay ~316 ms of Node startup and ~85
47
+ * requires, against which 45 ms is ~13%.
48
+ */
49
+ const zod_1 = require("zod");
50
+ const edit_distance_js_1 = require("./edit-distance.js");
51
+ // ---------------------------------------------------------------------------
52
+ // Primitives
53
+ // ---------------------------------------------------------------------------
54
+ /**
55
+ * A rule's severity as the config may spell it, normalized to the three values
56
+ * the gate actually branches on.
57
+ *
58
+ * 🔴 THE ESLINT SPELLINGS ARE PART OF THE SCHEMA, not a pre-pass. `"off"`, `0`,
59
+ * `false`, `1`, `2` are what people type because every other linter takes them,
60
+ * and before this they fell through the validator untouched and RENDERED AS A
61
+ * WARN — so `"off"` did not turn a rule off and `2` did not make it gate
62
+ * (#112). Putting the transform in the schema means the parsed config only ever
63
+ * holds a real decision, and the "unrecognized value" case is a schema failure
64
+ * with a message rather than a silent downgrade.
65
+ */
66
+ const severitySchema = zod_1.z
67
+ .union([
68
+ zod_1.z.literal("warn"),
69
+ zod_1.z.literal("error"),
70
+ zod_1.z.literal(false),
71
+ zod_1.z.literal("off"),
72
+ zod_1.z.literal(0),
73
+ zod_1.z.literal(1),
74
+ zod_1.z.literal(2),
75
+ zod_1.z.literal(true),
76
+ ])
77
+ .transform((v) => {
78
+ if (v === "off" || v === 0 || v === false)
79
+ return false;
80
+ if (v === "error" || v === 2)
81
+ return "error";
82
+ return "warn";
83
+ });
84
+ /** `severity` alone, or `[severity, options]` — the rules that take options. */
85
+ const withOptions = (options) => zod_1.z.union([
86
+ severitySchema,
87
+ zod_1.z.tuple([zod_1.z.literal("warn"), options]),
88
+ zod_1.z.tuple([zod_1.z.literal("error"), options]),
89
+ ]);
90
+ /**
91
+ * A string, or a string ARRAY — coerced to an array.
92
+ *
93
+ * The bare-string case is the natural first-value mistake and it used to spread
94
+ * a string's CHARACTERS as globs: `"exclude": "bench"` became
95
+ * `["b","e","n","c","h"]`, which is a no-op at best and garbage `orphan` matches
96
+ * (`.`, `/`, `README.md`) at worst. Accepting it as a one-element array is what
97
+ * `asStringArray` did; the difference is that anything that is neither is now a
98
+ * schema error instead of a `console.warn` nobody reads.
99
+ */
100
+ const stringList = zod_1.z
101
+ .union([zod_1.z.string(), zod_1.z.array(zod_1.z.string())])
102
+ .transform((v) => (typeof v === "string" ? [v] : v));
103
+ // ---------------------------------------------------------------------------
104
+ // Rules
105
+ // ---------------------------------------------------------------------------
106
+ /** Min % thresholds for the `coverage` rule. */
107
+ const coverageThresholds = zod_1.z
108
+ .object({
109
+ /** Min % of enabled linter rules with `enforce()` declarations. */
110
+ linterRules: zod_1.z.number().optional(),
111
+ /** Min % of npm scripts documented in spec commands. */
112
+ scripts: zod_1.z.number().optional(),
113
+ })
114
+ .strict();
115
+ /** Discovery options shared by the three `untested-*` rules. */
116
+ const testCoverageConfig = zod_1.z
117
+ .object({
118
+ include: stringList.optional(),
119
+ exclude: stringList.optional(),
120
+ testExtension: zod_1.z.string().optional(),
121
+ })
122
+ .strict();
123
+ /**
124
+ * Every validation rule, with its shipped default severity.
125
+ *
126
+ * 🔴 THIS OBJECT IS THE RULE SET. `rules-docs-in-sync` already treats the
127
+ * `RulesConfig` keys as the single source of truth the docs must track; now they
128
+ * are also what the validator accepts, so a rule name that is not here is
129
+ * REJECTED with the near-miss suggested rather than silently ignored — which is
130
+ * the same class of bug as a misspelled harness name, and was equally quiet.
131
+ */
132
+ const rulesSchema = zod_1.z
133
+ .object({
134
+ "spec-refs": severitySchema.default("error"),
135
+ "orphan-docs": severitySchema.default("warn"),
136
+ "duplicate-rules": severitySchema.default("warn"),
137
+ "require-instructions-spec": severitySchema.default("warn"),
138
+ "require-skill-spec": severitySchema.default(false),
139
+ integrity: severitySchema.default("warn"),
140
+ coverage: withOptions(coverageThresholds).default(false),
141
+ "untested-skill": withOptions(testCoverageConfig).default("warn"),
142
+ "untested-subagent": withOptions(testCoverageConfig).default("warn"),
143
+ "untested-hook": withOptions(testCoverageConfig).default("warn"),
144
+ "unmarked-refs": severitySchema.default("warn"),
145
+ "subagent-tool-contract": severitySchema.default("warn"),
146
+ "hook-events": severitySchema.default("warn"),
147
+ "subagent-frontmatter": severitySchema.default("warn"),
148
+ "mcp-config": severitySchema.default("warn"),
149
+ "skill-frontmatter": severitySchema.default("warn"),
150
+ "mcp-tool-resolves": severitySchema.default("warn"),
151
+ "hook-script-exists": severitySchema.default("warn"),
152
+ "prefer-compiled-hooks": severitySchema.default(false),
153
+ "disallowed-tools-contract": severitySchema.default("warn"),
154
+ "description-overlap": severitySchema.default("warn"),
155
+ "skill-description-budget": severitySchema.default("warn"),
156
+ "frontmatter-valid": severitySchema.default("warn"),
157
+ "mcp-hook-target-resolves": severitySchema.default("warn"),
158
+ "lethal-trifecta": severitySchema.default("warn"),
159
+ "skill-resource-resolves": severitySchema.default("warn"),
160
+ "skill-missing-fence": severitySchema.default("warn"),
161
+ "plugin-dir-layout": severitySchema.default("warn"),
162
+ "delegation-trifecta": severitySchema.default("warn"),
163
+ "hook-block-ineffective": severitySchema.default("warn"),
164
+ "hook-matcher": severitySchema.default("warn"),
165
+ "doc-refs": severitySchema.default(false),
166
+ })
167
+ .strict();
168
+ /** The rule names, for the "did you mean" on an unknown one. */
169
+ exports.RULE_NAMES = Object.keys(rulesSchema.shape);
170
+ // ---------------------------------------------------------------------------
171
+ // Harnesses (#240)
172
+ // ---------------------------------------------------------------------------
173
+ /**
174
+ * ONE harness's entry in {@link vigilesConfigSchema}'s `harnesses`.
175
+ *
176
+ * `.strict()` is load-bearing here specifically: the whole reason this key
177
+ * exists is that a declaration which reaches nothing used to be silent, and
178
+ * `{"claude-code": {"root": ".ai"}}` (singular, no `s`) reaches nothing.
179
+ */
180
+ const harnessDeclarationSchema = zod_1.z
181
+ .object({
182
+ roots: stringList.optional(),
183
+ })
184
+ .strict();
185
+ // ---------------------------------------------------------------------------
186
+ // The config
187
+ // ---------------------------------------------------------------------------
188
+ /**
189
+ * The whole of `.vigilesrc.json`.
190
+ *
191
+ * `.strict()` at the top level is the check that did not exist: an unrecognized
192
+ * key is now a named error with a suggestion, where it used to be spread into
193
+ * the config object and never read.
194
+ */
195
+ exports.vigilesConfigSchema = zod_1.z
196
+ .object({
197
+ /** Which markdown constructs count as a rule — headings, checkboxes, or both. */
198
+ ruleMarkers: zod_1.z
199
+ .array(zod_1.z.enum(["headings", "checkboxes"]))
200
+ .default(["headings", "checkboxes"]),
201
+ rules: rulesSchema.prefault({}),
202
+ /** The instruction files to validate. */
203
+ files: zod_1.z.array(zod_1.z.string()).default(["CLAUDE.md"]),
204
+ maxRules: zod_1.z.number().optional(),
205
+ maxTokens: zod_1.z.number().optional(),
206
+ maxSectionLines: zod_1.z.number().optional(),
207
+ catalogOnly: zod_1.z.boolean().optional(),
208
+ linters: zod_1.z
209
+ .record(zod_1.z.string(), zod_1.z
210
+ .object({
211
+ // NOT `stringList`: this value is handed to the linter catalog
212
+ // layer as written (`string | string[]`), and normalizing it here
213
+ // would change a published shape for no gain — the consumer already
214
+ // handles both. Accepting exactly what it accepts is the point.
215
+ rulesDir: zod_1.z.union([zod_1.z.string(), zod_1.z.array(zod_1.z.string())]).optional(),
216
+ })
217
+ .strict())
218
+ .optional(),
219
+ bundles: zod_1.z.enum(["root", "all"]).optional(),
220
+ orphans: zod_1.z
221
+ .object({
222
+ include: stringList.optional(),
223
+ exclude: stringList.optional(),
224
+ })
225
+ .strict()
226
+ .optional(),
227
+ exclude: stringList.optional(),
228
+ sharedDirs: stringList.optional(),
229
+ harnesses: zod_1.z.record(zod_1.z.string(), harnessDeclarationSchema).optional(),
230
+ audit: zod_1.z.object({ measure: zod_1.z.boolean().optional() }).strict().optional(),
231
+ eval: zod_1.z.object({ apiVersion: zod_1.z.number().optional() }).strict().optional(),
232
+ nudge: zod_1.z.literal("dismissed").optional(),
233
+ /**
234
+ * The editor's pointer at the published JSON Schema. Accepted, never read.
235
+ *
236
+ * 🔴 IT IS DECLARED HERE RATHER THAN EXCUSED IN THE UNKNOWN-KEY WALKER, and
237
+ * the reason is that the walker is only half the surface. `dist/vigilesrc.
238
+ * schema.json` is generated FROM this object by
239
+ * `scripts/build-config-schema.mjs`, with `additionalProperties: false`, so
240
+ * a key missing here is refused TWICE: once by the CLI, and once by the
241
+ * editor being pointed at the schema. Measured before this key existed, on
242
+ * `{"$schema": <the schema's own $id>, "harnesses": {"claude-code": {}}}`:
243
+ *
244
+ * ```
245
+ * $ vigiles audit . --no-interactive
246
+ * ✗ .vigilesrc.json: unknown key "$schema" in (top level). Known: …
247
+ * (exit 2)
248
+ * $ # and the same document against dist/vigilesrc.schema.json:
249
+ * additionalProperties: should NOT have additional properties ($schema)
250
+ * ```
251
+ *
252
+ * An exception in the walker would have fixed the first line and left the
253
+ * second — a red squiggle on the one line whose entire job is to turn the
254
+ * squiggles on. One declaration, both halves, because both derive from here.
255
+ *
256
+ * It is LAST in the shape on purpose: `knownKeysAt` reads this object to
257
+ * build the "Known: …" candidate list, which is capped, so a key nobody
258
+ * misspells belongs past the cap rather than at the head of the suggestion.
259
+ */
260
+ $schema: zod_1.z.string().optional(),
261
+ })
262
+ .strict();
263
+ /**
264
+ * A `.vigilesrc.json` that cannot be honoured as written.
265
+ *
266
+ * 🔴 IT HAS ITS OWN CLASS BECAUSE THE READER CATCHES EVERYTHING ELSE. A missing
267
+ * file, an unreadable one and malformed JSON all mean "use the defaults", which
268
+ * is right — and a file that IS readable and says something we refuse must not
269
+ * join them, or the user's declaration vanishes into the defaults and the run
270
+ * looks clean. A distinct class is what lets the CLI print it as a config error
271
+ * and a hook rail downgrade it to a warning, from one throw site.
272
+ */
273
+ class VigilesConfigError extends Error {
274
+ name = "VigilesConfigError";
275
+ }
276
+ exports.VigilesConfigError = VigilesConfigError;
277
+ /**
278
+ * The two keys `harnesses` replaced, and the sentence each one gets (#240).
279
+ *
280
+ * They are listed here rather than left to `.strict()`'s "Unrecognized key"
281
+ * because the reader of that message is someone whose config USED to work: they
282
+ * need the new spelling, not the news that the old one is unknown. `.strict()`
283
+ * would tell them the truth in the least useful possible way.
284
+ */
285
+ exports.REPLACED_KEYS = [
286
+ {
287
+ key: "harness",
288
+ was: '"harness": ["claude-code", "codex"]',
289
+ now: "a KEY per harness",
290
+ },
291
+ {
292
+ key: "surfaceRoots",
293
+ was: '"surfaceRoots": [".ai"]',
294
+ now: '"roots" INSIDE the harness that reads them',
295
+ },
296
+ ];
297
+ /** The message a config written in the replaced shape gets. */
298
+ function replacedKeyMessage(present) {
299
+ return (`.vigilesrc.json: ${present.map((k) => `"${k.key}"`).join(" and ")} ` +
300
+ `${present.length === 1 ? "was" : "were"} replaced by one nested key, "harnesses".\n` +
301
+ ` Write: { "harnesses": { "claude-code": { "roots": [".ai"] }, "codex": {} } }\n` +
302
+ present.map((k) => ` - ${k.was} → ${k.now}`).join("\n") +
303
+ `\n The old harness ARRAY's order silently decided what got read: one order graded the ` +
304
+ `skills and read no instruction file, the other read the instruction file and found no ` +
305
+ `skills. Scoping each root under the harness that reads it removes the order.`);
306
+ }
307
+ // ---------------------------------------------------------------------------
308
+ // Messages — the half a schema library does NOT give you
309
+ // ---------------------------------------------------------------------------
310
+ /**
311
+ * The keys a given config path accepts, walked out of the schema itself.
312
+ *
313
+ * Derived rather than listed, for the reason every other derivation in this repo
314
+ * is: a hand-written candidate list is the copy that goes stale, and the message
315
+ * would then suggest a key the validator rejects. Returns `[]` where the path has
316
+ * no fixed key set (a record's own keys are open — the harness NAMES are checked
317
+ * by `resolveDeclaredHarnesses` against the adapter registry, which core may not
318
+ * import).
319
+ */
320
+ function knownKeysAt(path) {
321
+ let node = exports.vigilesConfigSchema;
322
+ for (const seg of path) {
323
+ const def = node._zod
324
+ ?.def;
325
+ if (def?.type === "object") {
326
+ node = def.shape?.[String(seg)];
327
+ }
328
+ else if (def?.type === "record") {
329
+ node = def.valueType;
330
+ }
331
+ else {
332
+ return [];
333
+ }
334
+ if (node === undefined)
335
+ return [];
336
+ node = unwrap(node);
337
+ }
338
+ const def = node._zod?.def;
339
+ return def?.type === "object"
340
+ ? Object.keys(def.shape)
341
+ : [];
342
+ }
343
+ /** Peel `.optional()` / `.default()` / `.prefault()` wrappers off a schema node. */
344
+ function unwrap(node) {
345
+ let cur = node;
346
+ for (let i = 0; i < 8; i++) {
347
+ const def = cur._zod?.def;
348
+ const inner = def?.innerType;
349
+ if (inner === undefined)
350
+ return cur;
351
+ cur = inner;
352
+ }
353
+ /* v8 ignore next -- eight wrappers deep is not a shape this schema has */
354
+ return cur;
355
+ }
356
+ /** How far a "did you mean" may reach before it starts mis-suggesting. */
357
+ const SUGGEST_MAX_DISTANCE = 3;
358
+ /** The nearest candidate to `name`, or undefined when none is close enough. */
359
+ function nearest(name, candidates) {
360
+ let best;
361
+ let bestD = SUGGEST_MAX_DISTANCE + 1;
362
+ for (const c of candidates) {
363
+ const d = (0, edit_distance_js_1.editDistance)(name.toLowerCase(), c.toLowerCase());
364
+ if (d < bestD) {
365
+ bestD = d;
366
+ best = c;
367
+ }
368
+ }
369
+ return bestD <= SUGGEST_MAX_DISTANCE ? best : undefined;
370
+ }
371
+ /** How many candidates a message prints before it stops being a list. */
372
+ const MAX_CANDIDATES_SHOWN = 12;
373
+ /** The candidate list, capped, with the remainder counted rather than dropped. */
374
+ function listCandidates(known) {
375
+ if (known.length <= MAX_CANDIDATES_SHOWN)
376
+ return known.join(", ");
377
+ const shown = known.slice(0, MAX_CANDIDATES_SHOWN).join(", ");
378
+ return `${shown}, … and ${String(known.length - MAX_CANDIDATES_SHOWN)} more`;
379
+ }
380
+ /** `harnesses.claude-code.roots` — a path a user can find in their own file. */
381
+ function pathLabel(path) {
382
+ return path.length === 0 ? "(top level)" : path.map(String).join(".");
383
+ }
384
+ /**
385
+ * Turn a Zod failure into the lines a human acts on — ONE per real problem.
386
+ *
387
+ * 🔴 THE FORMATTER IS THE POINT, because the library's own message is worse than
388
+ * what it replaced on the two things that matter. Measured on Zod 4.6.5 against
389
+ * this schema:
390
+ *
391
+ * ```
392
+ * {"rules":{"spec-refs":"errr"}}
393
+ * -> invalid_union, EIGHT branch errors: expected "warn" / "error" / false /
394
+ * "off" / 0 / 1 / 2 / true — one line per union member, none of them the
395
+ * sentence "these are the values this key takes"
396
+ * {"harnessez":{}}
397
+ * -> Unrecognized key: "harnessez" (names the culprit, suggests nothing)
398
+ * ```
399
+ *
400
+ * The first is CASCADE NOISE: a union failure is one problem, not eight, and
401
+ * printing the branches makes the schema's internals the user's problem. The
402
+ * second is the regression we refuse to ship — the line it would replace is
403
+ * `✗ Unknown harness "claud-code". Known: claude-code, codex.`, which names the
404
+ * candidates AND the near-miss. So a union collapses to one line listing what
405
+ * the key accepts, and an unknown key carries the candidate list plus a
406
+ * distance-bounded "did you mean".
407
+ */
408
+ function formatConfigIssues(issues) {
409
+ return issues.flatMap((issue) => {
410
+ if (issue.code === "unrecognized_keys")
411
+ return unknownKeyLines(issue);
412
+ if (issue.code === "invalid_union")
413
+ return [badValueLine(issue)];
414
+ return [
415
+ `.vigilesrc.json: ${pathLabel(issue.path ?? [])} — ${issue.message ?? "invalid"}.`,
416
+ ];
417
+ });
418
+ }
419
+ /** One line per unrecognized key, with a near-miss or the candidate list. */
420
+ function unknownKeyLines(issue) {
421
+ const known = knownKeysAt(issue.path ?? []);
422
+ const where = pathLabel(issue.path ?? []);
423
+ return (issue.keys ?? []).map((key) => {
424
+ // A near-miss REPLACES the candidate list rather than joining it. The rules
425
+ // object has 32 keys, and printing all of them beside
426
+ // `Did you mean "spec-refs"?` buries the one line that is the answer. With
427
+ // no near-miss the list IS the answer, so it is printed (capped — a wall of
428
+ // 32 names is not a list a reader uses either).
429
+ const near = nearest(key, known);
430
+ if (near !== undefined)
431
+ return `.vigilesrc.json: unknown key "${key}" in ${where}. Did you mean "${near}"?`;
432
+ if (known.length === 0)
433
+ return `.vigilesrc.json: unknown key "${key}" in ${where}.`;
434
+ return `.vigilesrc.json: unknown key "${key}" in ${where}. Known: ${listCandidates(known)}.`;
435
+ });
436
+ }
437
+ /** ONE line for a failed union — never one per branch. */
438
+ function badValueLine(issue) {
439
+ const accepted = acceptedValues(issue);
440
+ return (`.vigilesrc.json: ${pathLabel(issue.path ?? [])} is not one of the accepted values` +
441
+ (accepted.length > 0 ? ` (${accepted.join(", ")}).` : "."));
442
+ }
443
+ /** The literal values / types a union's branches accept, de-duplicated. */
444
+ function acceptedValues(issue) {
445
+ const named = (issue.errors ?? []).flat().flatMap((b) => {
446
+ if (b.values !== undefined)
447
+ return b.values.map((v) => JSON.stringify(v));
448
+ return b.expected !== undefined ? [b.expected] : [];
449
+ });
450
+ return [...new Set(named)];
451
+ }
452
+ //# sourceMappingURL=config-schema.js.map
package/dist/core/refs.js CHANGED
@@ -74,15 +74,24 @@ function verifySymbolRefs(markdown, basePath) {
74
74
  const errors = [];
75
75
  for (const ref of symbolRefs(markdown)) {
76
76
  const full = (0, node_path_1.resolve)(basePath, ref.file);
77
+ const support = (0, symbols_js_1.langForFile)(ref.file);
77
78
  if (!(0, node_fs_1.existsSync)(full)) {
78
79
  errors.push({ ...ref, reason: `File not found: "${ref.file}"` });
79
80
  }
80
- else if ((0, symbols_js_1.langForFile)(ref.file) === null) {
81
+ else if (support.kind === "unsupported") {
81
82
  errors.push({
82
83
  ...ref,
83
84
  reason: `Unsupported language for symbol check: "${ref.file}"`,
84
85
  });
85
86
  }
87
+ else if (support.kind === "grammar-missing") {
88
+ // NOT "unsupported": the language is one this tool parses, the optional grammar just is
89
+ // not installed here. Saying it the other way would report an un-run check as a verdict.
90
+ errors.push({
91
+ ...ref,
92
+ reason: `Symbol not checked: the ${support.id} grammar is not installed (npm i -D ${support.pkg})`,
93
+ });
94
+ }
86
95
  else if (!(0, symbols_js_1.fileDefinesSymbol)(full, ref.symbol)) {
87
96
  errors.push({
88
97
  ...ref,