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.
- 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 +6 -0
- 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 +6 -0
- 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 +82 -20
- package/dist/core/adapter.d.ts +23 -0
- 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/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 +36 -22
- package/dist/core/validate.js +88 -176
- package/dist/exclude.d.ts +20 -0
- package/dist/exclude.js +11 -1
- package/dist/layout-registry.d.ts +14 -0
- package/dist/layout-registry.js +40 -0
- package/dist/plugin-loader.d.ts +49 -1
- package/dist/plugin-loader.js +120 -14
- 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,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 (
|
|
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,
|