vigiles 37.0.0 → 38.1.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/dist/audit-report.d.ts +8 -0
- package/dist/audit-report.js +12 -3
- package/dist/cli-main.d.ts +9 -0
- package/dist/cli-main.js +123 -25
- package/dist/core/CLAUDE.md.spec.js +0 -4
- package/dist/core/compile.d.ts +36 -6
- package/dist/core/compile.js +189 -54
- package/dist/core/config-schema.d.ts +10 -0
- package/dist/core/config-schema.js +16 -0
- package/dist/core/keyfiles-source.d.ts +9 -0
- package/dist/core/keyfiles-source.js +88 -0
- package/dist/core/keyfiles.d.ts +64 -0
- package/dist/core/keyfiles.js +93 -0
- package/dist/core/markdown.d.ts +17 -0
- package/dist/core/markdown.js +57 -0
- package/dist/core/orphans.js +1 -1
- package/dist/core/presets.d.ts +1 -1
- package/dist/core/presets.js +1 -0
- package/dist/core/rule-meta.js +21 -0
- package/dist/core/session.d.ts +7 -0
- package/dist/core/session.js +8 -0
- package/dist/core/spec.d.ts +59 -3
- package/dist/core/spec.js +16 -2
- package/dist/core/types.d.ts +34 -0
- package/dist/deprecated-key-files.d.ts +35 -0
- package/dist/deprecated-key-files.js +62 -0
- package/dist/research-index.d.ts +6 -6
- package/dist/research-index.js +6 -6
- package/dist/scan.d.ts +21 -0
- package/dist/scan.js +29 -2
- package/dist/setup-plan.d.ts +1 -1
- package/dist/setup-plan.js +3 -0
- package/dist/vigilesrc.schema.json +130 -0
- package/hooks/pre-edit.sh +1 -1
- package/package.json +1 -1
- package/skills/adopt-spec/SKILL.md +7 -12
- package/skills/edit-spec/SKILL.md +32 -25
package/dist/core/markdown.d.ts
CHANGED
|
@@ -118,4 +118,21 @@ export interface MarkdownRef {
|
|
|
118
118
|
* which is not recovered — the regexes this replaces did not recover it either.
|
|
119
119
|
*/
|
|
120
120
|
export declare function markdownRefs(src: string): MarkdownRef[];
|
|
121
|
+
/**
|
|
122
|
+
* The fragment ids a markdown file defines — what `file.md#<id>` can land on.
|
|
123
|
+
*
|
|
124
|
+
* Two sources, because both are how real files are linked into:
|
|
125
|
+
*
|
|
126
|
+
* - every HEADING, slugged the way GitHub slugs it (lowercase; drop everything
|
|
127
|
+
* that is not a letter, number, space, `-` or `_`; spaces to `-`; a repeated
|
|
128
|
+
* slug gets `-1`, `-2`, …), with the heading's text taken from the parser's
|
|
129
|
+
* inline token, so emphasis and code spans do not leak their markers in;
|
|
130
|
+
* - every explicit `id="…"` / `name="…"` on raw HTML (`<a id="why"></a>`), which
|
|
131
|
+
* is how a file in a non-Latin language gives its sections stable ASCII ids.
|
|
132
|
+
*
|
|
133
|
+
* Other renderers slug differently (GitLab, mkdocs). A link that resolves on
|
|
134
|
+
* GitHub and nowhere else is still a link this accepts; one that resolves
|
|
135
|
+
* NOWHERE — a renamed heading — is what it exists to catch.
|
|
136
|
+
*/
|
|
137
|
+
export declare function markdownAnchors(src: string): ReadonlySet<string>;
|
|
121
138
|
//# sourceMappingURL=markdown.d.ts.map
|
package/dist/core/markdown.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.fencedLineFlags = fencedLineFlags;
|
|
|
4
4
|
exports.proseLines = proseLines;
|
|
5
5
|
exports.fencedCodeBlocks = fencedCodeBlocks;
|
|
6
6
|
exports.markdownRefs = markdownRefs;
|
|
7
|
+
exports.markdownAnchors = markdownAnchors;
|
|
7
8
|
const MarkdownIt = () => require("markdown-it");
|
|
8
9
|
// One reusable parser; parse() is stateless across calls.
|
|
9
10
|
let mdCached = null;
|
|
@@ -233,4 +234,60 @@ function refsInInline(children, line, out) {
|
|
|
233
234
|
out.push({ kind: "code", value: child.content, line });
|
|
234
235
|
}
|
|
235
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* The fragment ids a markdown file defines — what `file.md#<id>` can land on.
|
|
239
|
+
*
|
|
240
|
+
* Two sources, because both are how real files are linked into:
|
|
241
|
+
*
|
|
242
|
+
* - every HEADING, slugged the way GitHub slugs it (lowercase; drop everything
|
|
243
|
+
* that is not a letter, number, space, `-` or `_`; spaces to `-`; a repeated
|
|
244
|
+
* slug gets `-1`, `-2`, …), with the heading's text taken from the parser's
|
|
245
|
+
* inline token, so emphasis and code spans do not leak their markers in;
|
|
246
|
+
* - every explicit `id="…"` / `name="…"` on raw HTML (`<a id="why"></a>`), which
|
|
247
|
+
* is how a file in a non-Latin language gives its sections stable ASCII ids.
|
|
248
|
+
*
|
|
249
|
+
* Other renderers slug differently (GitLab, mkdocs). A link that resolves on
|
|
250
|
+
* GitHub and nowhere else is still a link this accepts; one that resolves
|
|
251
|
+
* NOWHERE — a renamed heading — is what it exists to catch.
|
|
252
|
+
*/
|
|
253
|
+
function markdownAnchors(src) {
|
|
254
|
+
// The html-enabled parser: with the shared one, `<a id="x">` is paragraph
|
|
255
|
+
// text and its id would be invisible.
|
|
256
|
+
const tokens = mdProse().parse(src, {});
|
|
257
|
+
const headings = tokens.flatMap((t, i) => t.type === "heading_open" ? [headingSlug(tokens[i + 1])] : []);
|
|
258
|
+
const explicit = tokens
|
|
259
|
+
.flatMap(htmlOf)
|
|
260
|
+
.flatMap((h) => [...h.matchAll(HTML_ID)].map((m) => m[1]));
|
|
261
|
+
return new Set([...numberRepeats(headings), ...explicit]);
|
|
262
|
+
}
|
|
263
|
+
/** An explicit `id="…"` / `name="…"` on raw HTML. */
|
|
264
|
+
const HTML_ID = /\b(?:id|name)\s*=\s*["']([^"']+)["']/g;
|
|
265
|
+
/** A heading's GitHub slug before repeats are numbered, from its inline token. */
|
|
266
|
+
function headingSlug(inline) {
|
|
267
|
+
return (inline?.children ?? [])
|
|
268
|
+
.filter((c) => c.type === "text" || c.type === "code_inline")
|
|
269
|
+
.map((c) => c.content)
|
|
270
|
+
.join("")
|
|
271
|
+
.toLowerCase()
|
|
272
|
+
.replace(/[^\p{L}\p{N}\s_-]/gu, "")
|
|
273
|
+
.trim()
|
|
274
|
+
.replace(/\s/g, "-");
|
|
275
|
+
}
|
|
276
|
+
/** GitHub's numbering of a repeated slug: `x`, `x-1`, `x-2`, … */
|
|
277
|
+
function numberRepeats(bases) {
|
|
278
|
+
return bases.map((base, i) => {
|
|
279
|
+
const k = bases.slice(0, i).filter((b) => b === base).length;
|
|
280
|
+
return k === 0 ? base : `${base}-${String(k)}`;
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
/** The raw HTML a token carries: a block, an inline, or an inline run's HTML. */
|
|
284
|
+
function htmlOf(t) {
|
|
285
|
+
if (t.type === "html_block" || t.type === "html_inline")
|
|
286
|
+
return [t.content];
|
|
287
|
+
if (t.type !== "inline")
|
|
288
|
+
return [];
|
|
289
|
+
return (t.children ?? [])
|
|
290
|
+
.filter((c) => c.type === "html_inline")
|
|
291
|
+
.map((c) => c.content);
|
|
292
|
+
}
|
|
236
293
|
//# sourceMappingURL=markdown.js.map
|
package/dist/core/orphans.js
CHANGED
|
@@ -217,7 +217,7 @@ function formatOrphanReport(report) {
|
|
|
217
217
|
];
|
|
218
218
|
for (const o of report.orphans)
|
|
219
219
|
lines.push(` ${o}`);
|
|
220
|
-
lines.push(" Fix: link each from another .md (README,
|
|
220
|
+
lines.push(" Fix: link each from another .md (README, an instruction file, or a doc).", " To silence: add `<!-- vigiles-disable orphan-docs -->` to the doc, or", " exclude it via .vigilesrc.json → `orphans.exclude` (or narrow `orphans.include`).");
|
|
221
221
|
return lines.join("\n");
|
|
222
222
|
}
|
|
223
223
|
//# sourceMappingURL=orphans.js.map
|
package/dist/core/presets.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ import type { RulesConfig } from "./types.js";
|
|
|
30
30
|
* `untested-*`): a clean repo fails those until the work is done, so they stay
|
|
31
31
|
* an explicit opt-in (`init --strict`).
|
|
32
32
|
*/
|
|
33
|
-
export declare const RECOMMENDED_RULES: readonly ["subagent-tool-contract", "subagent-frontmatter", "hook-events", "hook-script-exists", "mcp-config", "mcp-tool-resolves", "mcp-hook-target-resolves", "disallowed-tools-contract", "description-overlap"];
|
|
33
|
+
export declare const RECOMMENDED_RULES: readonly ["subagent-tool-contract", "subagent-frontmatter", "hook-events", "hook-script-exists", "mcp-config", "mcp-tool-resolves", "mcp-hook-target-resolves", "disallowed-tools-contract", "description-overlap", "compile-budget"];
|
|
34
34
|
/** The preset names `extends` accepts. One today; the tuple is the enum. */
|
|
35
35
|
export declare const PRESET_NAMES: readonly ["vigiles:recommended"];
|
|
36
36
|
/** A preset name `extends` accepts. */
|
package/dist/core/presets.js
CHANGED
|
@@ -24,6 +24,7 @@ exports.RECOMMENDED_RULES = [
|
|
|
24
24
|
"mcp-hook-target-resolves",
|
|
25
25
|
"disallowed-tools-contract",
|
|
26
26
|
"description-overlap",
|
|
27
|
+
"compile-budget",
|
|
27
28
|
];
|
|
28
29
|
/** The preset names `extends` accepts. One today; the tuple is the enum. */
|
|
29
30
|
exports.PRESET_NAMES = ["vigiles:recommended"];
|
package/dist/core/rule-meta.js
CHANGED
|
@@ -226,6 +226,27 @@ exports.RULE_META = {
|
|
|
226
226
|
summary: "The always-loaded instruction weight did not move off its committed baseline.",
|
|
227
227
|
detector: "compareToBaseline",
|
|
228
228
|
},
|
|
229
|
+
"compile-budget": {
|
|
230
|
+
id: "compile-budget",
|
|
231
|
+
// Exact once a threshold is chosen: a length or a count read off the spec
|
|
232
|
+
// and its rendered output. The threshold is the judgment: the default stays
|
|
233
|
+
// `warn`, and `vigiles:recommended` sets `error`.
|
|
234
|
+
bucket: "structural-closed",
|
|
235
|
+
surface: ["instruction"],
|
|
236
|
+
defaultSeverity: "warn",
|
|
237
|
+
summary: "A compiled instruction file stays within its size budgets (command entries, sections, the Rules total).",
|
|
238
|
+
detector: "checkContentBudgets",
|
|
239
|
+
upstreamPrevention: "compile measures the spec before writing; a render policy (enforce render, guidance details) meets a budget without deleting a rule",
|
|
240
|
+
},
|
|
241
|
+
"deprecated-key-files": {
|
|
242
|
+
id: "deprecated-key-files",
|
|
243
|
+
bucket: "structural-closed",
|
|
244
|
+
surface: ["instruction"],
|
|
245
|
+
defaultSeverity: "warn",
|
|
246
|
+
summary: "An instruction spec doesn't use the deprecated keyFiles (always-loaded file map).",
|
|
247
|
+
detector: "findDeprecatedKeyFiles",
|
|
248
|
+
upstreamPrevention: "the @deprecated keyFiles field is removed next major",
|
|
249
|
+
},
|
|
229
250
|
"skill-description-budget": {
|
|
230
251
|
id: "skill-description-budget",
|
|
231
252
|
bucket: "heuristic-behavioral",
|
package/dist/core/session.d.ts
CHANGED
|
@@ -57,6 +57,13 @@ export declare function loadSpecSurface(basePath: string): SpecSurface;
|
|
|
57
57
|
/**
|
|
58
58
|
* Load keyFiles from compiled markdown files. Reads each target's
|
|
59
59
|
* "## Key Files" section and extracts the file paths.
|
|
60
|
+
*
|
|
61
|
+
* @deprecated Goes away together with the deprecated `keyFiles` spec field (see
|
|
62
|
+
* the `deprecated-key-files` rule), in the major after the one that deprecates
|
|
63
|
+
* it. It reads the field back out of COMPILED markdown with a regex rather than
|
|
64
|
+
* from the spec, and it is deliberately not ported in the release that
|
|
65
|
+
* deprecates the field: with the field gone, the session audit's "files the
|
|
66
|
+
* specs know about" is `surface.trackedInputs` (the sidecar manifests) alone.
|
|
60
67
|
*/
|
|
61
68
|
export declare function loadKeyFilesFromSpecs(basePath: string): Set<string>;
|
|
62
69
|
/** A finding about a file not tracked by any spec. */
|
package/dist/core/session.js
CHANGED
|
@@ -143,6 +143,13 @@ function loadSpecSurface(basePath) {
|
|
|
143
143
|
/**
|
|
144
144
|
* Load keyFiles from compiled markdown files. Reads each target's
|
|
145
145
|
* "## Key Files" section and extracts the file paths.
|
|
146
|
+
*
|
|
147
|
+
* @deprecated Goes away together with the deprecated `keyFiles` spec field (see
|
|
148
|
+
* the `deprecated-key-files` rule), in the major after the one that deprecates
|
|
149
|
+
* it. It reads the field back out of COMPILED markdown with a regex rather than
|
|
150
|
+
* from the spec, and it is deliberately not ported in the release that
|
|
151
|
+
* deprecates the field: with the field gone, the session audit's "files the
|
|
152
|
+
* specs know about" is `surface.trackedInputs` (the sidecar manifests) alone.
|
|
146
153
|
*/
|
|
147
154
|
function loadKeyFilesFromSpecs(basePath) {
|
|
148
155
|
const keyFiles = new Set();
|
|
@@ -175,6 +182,7 @@ function loadKeyFilesFromSpecs(basePath) {
|
|
|
175
182
|
function analyzeSession(basePath, baseRef) {
|
|
176
183
|
const changedFiles = gitChangedFiles(basePath, baseRef);
|
|
177
184
|
const surface = loadSpecSurface(basePath);
|
|
185
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated -- keyFiles is deprecated, not removed: it still compiles until the next major, and this reads it back until then
|
|
178
186
|
const keyFiles = loadKeyFilesFromSpecs(basePath);
|
|
179
187
|
const trackedChanges = [];
|
|
180
188
|
const untrackedChanges = [];
|
package/dist/core/spec.d.ts
CHANGED
|
@@ -94,6 +94,30 @@ export interface OpenToolVocabulary extends ToolVocabulary {
|
|
|
94
94
|
* untyped surface accepts any tools.
|
|
95
95
|
*/
|
|
96
96
|
export type AllowedAt<P extends AuthoredPurity | undefined, V extends ToolVocabulary> = P extends "pure" ? V["readOnly"] : P extends "bounded" ? V["bounded"] : string;
|
|
97
|
+
/**
|
|
98
|
+
* Where a rule's long text lives when the compiled file carries a short form —
|
|
99
|
+
* a `file()` reference (verified to exist, like every other one) and an
|
|
100
|
+
* optional anchor inside it (verified to be a heading or an explicit
|
|
101
|
+
* `<a id="…">` in that file). Rendered as a `[Details](path#anchor)` link.
|
|
102
|
+
*/
|
|
103
|
+
export interface RuleDetails {
|
|
104
|
+
readonly file: FileRef;
|
|
105
|
+
readonly anchor?: string;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* How much of an `enforce()` rule the compiled file carries (#355).
|
|
109
|
+
*
|
|
110
|
+
* - `"full"` (default) — the rule id and the whole `why`, as always.
|
|
111
|
+
* - `"line"` — the rule id and the first sentence of `why`.
|
|
112
|
+
* - `"id"` — the rule id only: the linter already holds the rule, so the prose
|
|
113
|
+
* is a second copy that costs tokens on every request and can drift.
|
|
114
|
+
*
|
|
115
|
+
* Whatever is rendered, the rule stays whole in the SPEC — lint, coverage, the
|
|
116
|
+
* audit rule map and `enforce-rules-format` read the spec, not the markdown —
|
|
117
|
+
* and the integrity hash is taken over the RENDERED file, so it stays a
|
|
118
|
+
* function of what the model actually sees.
|
|
119
|
+
*/
|
|
120
|
+
export type RuleRender = "full" | "line" | "id";
|
|
97
121
|
/** A rule delegated to an external tool (linter, ast-grep, dependency-cruiser, etc.) or to a vigiles-internal check. */
|
|
98
122
|
export interface EnforceRule {
|
|
99
123
|
readonly _kind: "enforce";
|
|
@@ -101,12 +125,29 @@ export interface EnforceRule {
|
|
|
101
125
|
readonly why: string;
|
|
102
126
|
/** Skip verification for this rule. Default: true (verify). */
|
|
103
127
|
readonly verify: boolean;
|
|
128
|
+
/** How much of the rule the compiled file carries. Default `"full"`. */
|
|
129
|
+
readonly render?: RuleRender;
|
|
130
|
+
/** Where the long text lives; rendered as a verified link. */
|
|
131
|
+
readonly details?: RuleDetails;
|
|
104
132
|
}
|
|
105
133
|
/** A guidance-only rule (prose, no enforcement). */
|
|
106
134
|
export interface GuidanceRule {
|
|
107
135
|
readonly _kind: "guidance";
|
|
108
136
|
readonly text: string;
|
|
137
|
+
/** Where the long text lives; rendered as a verified link after `text`. */
|
|
138
|
+
readonly details?: RuleDetails;
|
|
109
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* The `details` half of a rule's options. An `anchor` is only meaningful
|
|
142
|
+
* inside a `details` file, so the type refuses one without the other.
|
|
143
|
+
*/
|
|
144
|
+
export type RuleDetailsOptions = {
|
|
145
|
+
readonly details: FileRef;
|
|
146
|
+
readonly anchor?: string;
|
|
147
|
+
} | {
|
|
148
|
+
readonly details?: undefined;
|
|
149
|
+
readonly anchor?: never;
|
|
150
|
+
};
|
|
110
151
|
/** A reactive rule: runs a command when watched files change. */
|
|
111
152
|
export interface GuardRule {
|
|
112
153
|
readonly _kind: "guard";
|
|
@@ -130,13 +171,15 @@ export type Rule = EnforceRule | GuidanceRule | GuardRule;
|
|
|
130
171
|
*/
|
|
131
172
|
export declare function enforce(ref: NoInfer<StrictLinterRule> | VigilesRef, why: string, options?: {
|
|
132
173
|
verify?: boolean;
|
|
133
|
-
|
|
174
|
+
render?: RuleRender;
|
|
175
|
+
} & RuleDetailsOptions): EnforceRule;
|
|
134
176
|
/**
|
|
135
177
|
* Declare a guidance-only rule.
|
|
136
178
|
*
|
|
137
179
|
* guidance("Google unfamiliar APIs before implementing.")
|
|
180
|
+
* guidance("Short statement.", { details: file("docs/rules/x.md"), anchor: "why" })
|
|
138
181
|
*/
|
|
139
|
-
export declare function guidance(text: string): GuidanceRule;
|
|
182
|
+
export declare function guidance(text: string, options?: RuleDetailsOptions): GuidanceRule;
|
|
140
183
|
/**
|
|
141
184
|
* Declare a reactive guard: runs a command when watched files change.
|
|
142
185
|
*
|
|
@@ -309,7 +352,19 @@ export interface ClaudeSpec {
|
|
|
309
352
|
readonly target?: InstructionTarget | InstructionTarget[];
|
|
310
353
|
/** npm scripts / shell commands → descriptions. Verified against package.json. */
|
|
311
354
|
readonly commands?: Record<string, string>;
|
|
312
|
-
/**
|
|
355
|
+
/**
|
|
356
|
+
* File paths → descriptions, compiled into a "## Key Files" list. Verified via existsSync.
|
|
357
|
+
*
|
|
358
|
+
* @deprecated Move each entry out, then remove `keyFiles`. The list loads
|
|
359
|
+
* on every request to tell the agent where files are, which it finds itself.
|
|
360
|
+
* For each entry: delete it if the file or the docs already say it; move a
|
|
361
|
+
* fact about one file into that file's header comment; move a rule for one
|
|
362
|
+
* folder into that folder's nested instruction file; keep a one-line
|
|
363
|
+
* `file()` pointer (verified the same way) only if nothing else leads there.
|
|
364
|
+
* Still compiled for one major — the one that ships this notice — and
|
|
365
|
+
* removed in the major after it; `vigiles lint` reports every use as
|
|
366
|
+
* `deprecated-key-files`. Migration: docs/rules/deprecated-key-files.md.
|
|
367
|
+
*/
|
|
313
368
|
readonly keyFiles?: Record<string, string>;
|
|
314
369
|
/** Named prose sections — plain strings or tagged templates with file()/cmd()/ref(). */
|
|
315
370
|
readonly sections?: Record<string, string | InstructionFragment[]>;
|
|
@@ -336,6 +391,7 @@ export interface ClaudeSpec {
|
|
|
336
391
|
type ClaudeSpecBase = {
|
|
337
392
|
readonly target?: InstructionTarget | InstructionTarget[];
|
|
338
393
|
readonly commands?: Record<string, string>;
|
|
394
|
+
/** @deprecated Move each entry out, then remove `keyFiles` — see {@link ClaudeSpec.keyFiles}. Removed in the major after the one that deprecates it. */
|
|
339
395
|
readonly keyFiles?: Record<string, string>;
|
|
340
396
|
readonly maxTokens?: number;
|
|
341
397
|
readonly rules: Record<string, Rule>;
|
package/dist/core/spec.js
CHANGED
|
@@ -55,6 +55,13 @@ exports.BUILTIN_LINTERS = [
|
|
|
55
55
|
"golangci-lint",
|
|
56
56
|
"cedar",
|
|
57
57
|
];
|
|
58
|
+
const detailsOf = (o) => {
|
|
59
|
+
if (!o?.details)
|
|
60
|
+
return undefined;
|
|
61
|
+
return o.anchor === undefined
|
|
62
|
+
? { file: o.details }
|
|
63
|
+
: { file: o.details, anchor: o.anchor };
|
|
64
|
+
};
|
|
58
65
|
// ---------------------------------------------------------------------------
|
|
59
66
|
// Builder functions
|
|
60
67
|
// ---------------------------------------------------------------------------
|
|
@@ -72,21 +79,28 @@ exports.BUILTIN_LINTERS = [
|
|
|
72
79
|
* enforce("vigiles/orphan-docs", "No docs without spec references.")
|
|
73
80
|
*/
|
|
74
81
|
function enforce(ref, why, options) {
|
|
82
|
+
const details = detailsOf(options);
|
|
75
83
|
return {
|
|
76
84
|
_kind: "enforce",
|
|
77
85
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
78
86
|
linterRule: ref,
|
|
79
87
|
why,
|
|
80
88
|
verify: options?.verify ?? true,
|
|
89
|
+
...(options?.render === undefined ? {} : { render: options.render }),
|
|
90
|
+
...(details ? { details } : {}),
|
|
81
91
|
};
|
|
82
92
|
}
|
|
83
93
|
/**
|
|
84
94
|
* Declare a guidance-only rule.
|
|
85
95
|
*
|
|
86
96
|
* guidance("Google unfamiliar APIs before implementing.")
|
|
97
|
+
* guidance("Short statement.", { details: file("docs/rules/x.md"), anchor: "why" })
|
|
87
98
|
*/
|
|
88
|
-
function guidance(text) {
|
|
89
|
-
|
|
99
|
+
function guidance(text, options) {
|
|
100
|
+
const details = detailsOf(options);
|
|
101
|
+
return details
|
|
102
|
+
? { _kind: "guidance", text, details }
|
|
103
|
+
: { _kind: "guidance", text };
|
|
90
104
|
}
|
|
91
105
|
/**
|
|
92
106
|
* Declare a reactive guard: runs a command when watched files change.
|
package/dist/core/types.d.ts
CHANGED
|
@@ -99,6 +99,18 @@ export interface TestCoverageConfig {
|
|
|
99
99
|
*/
|
|
100
100
|
testExtension?: string;
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Thresholds for the `compile-budget` rule, in characters. A key left out
|
|
104
|
+
* keeps its default; `0` turns that budget off.
|
|
105
|
+
*/
|
|
106
|
+
export interface CompileBudgetConfig {
|
|
107
|
+
/** One `commands` description. Default 200. */
|
|
108
|
+
maxEntryChars?: number;
|
|
109
|
+
/** One named prose section. Default 15,000. */
|
|
110
|
+
maxSectionChars?: number;
|
|
111
|
+
/** The whole rendered Rules section. Default 20,000 (at "error"). */
|
|
112
|
+
maxRulesChars?: number;
|
|
113
|
+
}
|
|
102
114
|
export interface RulesConfig {
|
|
103
115
|
/**
|
|
104
116
|
* Opt-in: a doc in a configured dir (default `docs/`) that no other `.md`
|
|
@@ -130,6 +142,14 @@ export interface RulesConfig {
|
|
|
130
142
|
* `vigiles lint --update-baseline` records it. Default: "error".
|
|
131
143
|
*/
|
|
132
144
|
"instruction-weight"?: RuleSeverity;
|
|
145
|
+
/**
|
|
146
|
+
* An instruction spec still uses the deprecated `keyFiles` field — a flat
|
|
147
|
+
* path → description list compiled into the file loaded on every request.
|
|
148
|
+
* Reports the entry count and compiled size with the migration (a `file()`
|
|
149
|
+
* ref in the rule that needs the pointer). The spec is parsed, never run.
|
|
150
|
+
* Default "warn": a deprecation must not turn a consumer's CI red on upgrade.
|
|
151
|
+
*/
|
|
152
|
+
"deprecated-key-files"?: RuleSeverity;
|
|
133
153
|
/**
|
|
134
154
|
* Near-duplicate rules WITHIN one spec, by NCD similarity — spec bloat, two
|
|
135
155
|
* rules saying the same thing in different words.
|
|
@@ -271,6 +291,20 @@ export interface RulesConfig {
|
|
|
271
291
|
* `scan` (descriptionBudgetIssues).
|
|
272
292
|
*/
|
|
273
293
|
"skill-description-budget"?: RuleSeverity;
|
|
294
|
+
/**
|
|
295
|
+
* Size budgets on a COMPILED instruction file (`compile` from a `.spec.ts`):
|
|
296
|
+
* one `commands` entry, one prose section, and the whole Rules section. The
|
|
297
|
+
* whole always-loaded size is `instruction-weight`'s; `keyFiles` is
|
|
298
|
+
* deprecated, so neither is measured here.
|
|
299
|
+
*
|
|
300
|
+
* Default "warn", which is exactly the behaviour before this rule had a name:
|
|
301
|
+
* the per-entry and per-section budgets print as warnings and never stop a
|
|
302
|
+
* compile; the Rules total is not evaluated unless a threshold is configured.
|
|
303
|
+
* "error" (set by `vigiles:recommended`) makes every budget fail `compile` and
|
|
304
|
+
* `lint`. Options are thresholds — see {@link CompileBudgetConfig}. See
|
|
305
|
+
* docs/rules/compile-budget.md.
|
|
306
|
+
*/
|
|
307
|
+
"compile-budget"?: RuleWithOptions<CompileBudgetConfig>;
|
|
274
308
|
/**
|
|
275
309
|
* Flag a skill/agent whose `---` frontmatter block EXISTS but isn't valid YAML
|
|
276
310
|
* — fields may not parse as intended. CAVEAT: a real YAML parser (js-yaml) is
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type KeyFilesUsage } from "./core/keyfiles.js";
|
|
2
|
+
import type { RuleSeverity } from "./core/types.js";
|
|
3
|
+
/** One spec still using `keyFiles`, relative to the directory searched. */
|
|
4
|
+
export interface DeprecatedKeyFilesFinding extends KeyFilesUsage {
|
|
5
|
+
readonly path: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The detector: of the given instruction specs, the ones still using
|
|
9
|
+
* `keyFiles`, sorted by path. The file walk and the reads are the caller's
|
|
10
|
+
* (the composition root), so this stays free of I/O.
|
|
11
|
+
*/
|
|
12
|
+
export declare function findDeprecatedKeyFiles(io: {
|
|
13
|
+
/** Instruction specs (`**\/*.md.spec.ts`, the glob `compile` uses), relative paths. */
|
|
14
|
+
readonly specPaths: readonly string[];
|
|
15
|
+
readonly read: (path: string) => string;
|
|
16
|
+
}): readonly DeprecatedKeyFilesFinding[];
|
|
17
|
+
/**
|
|
18
|
+
* `lint`'s side of the rule. Runs ONCE over the repository, like the
|
|
19
|
+
* instruction-weight ratchet: a nested bundle's spec would otherwise be
|
|
20
|
+
* reported again by every bundle that encloses it. Warning by default — a
|
|
21
|
+
* deprecation that gated would turn a consumer's CI red on upgrade, which the
|
|
22
|
+
* one-major window exists to prevent.
|
|
23
|
+
*/
|
|
24
|
+
export declare function checkDeprecatedKeyFiles(opts: {
|
|
25
|
+
readonly severity: RuleSeverity;
|
|
26
|
+
/** Called only when the rule is on. */
|
|
27
|
+
readonly find: () => readonly DeprecatedKeyFilesFinding[];
|
|
28
|
+
readonly silent: boolean;
|
|
29
|
+
/** One per finding, with the spec's path relative to `root`. */
|
|
30
|
+
readonly annotate: (level: "error" | "warning", message: string, path: string) => void;
|
|
31
|
+
}): {
|
|
32
|
+
readonly issues: number;
|
|
33
|
+
readonly errors: number;
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=deprecated-key-files.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findDeprecatedKeyFiles = findDeprecatedKeyFiles;
|
|
4
|
+
exports.checkDeprecatedKeyFiles = checkDeprecatedKeyFiles;
|
|
5
|
+
/**
|
|
6
|
+
* The `deprecated-key-files` rule: every instruction spec that still uses the
|
|
7
|
+
* deprecated `keyFiles` field. ONE detector, two readers — `lint` (this file's
|
|
8
|
+
* `checkDeprecatedKeyFiles`) and `audit` (`scanPlugin`'s `deprecatedKeyFiles`)
|
|
9
|
+
* both call {@link findDeprecatedKeyFiles}, so they cannot disagree.
|
|
10
|
+
*
|
|
11
|
+
* The decisions — what a spec's `keyFiles` costs, what the finding says — are
|
|
12
|
+
* pure and live in `core/keyfiles.ts` / `core/keyfiles-source.ts`; this module
|
|
13
|
+
* owns what is left: matching specs to findings and printing them. The
|
|
14
|
+
* effects — walking the repository and reading files — happen in the composition root (`cli-main.ts`,
|
|
15
|
+
* `specsUsingKeyFiles`) and are handed in. Migration guide:
|
|
16
|
+
* docs/rules/deprecated-key-files.md.
|
|
17
|
+
*/
|
|
18
|
+
const keyfiles_source_js_1 = require("./core/keyfiles-source.js");
|
|
19
|
+
const keyfiles_js_1 = require("./core/keyfiles.js");
|
|
20
|
+
/**
|
|
21
|
+
* The detector: of the given instruction specs, the ones still using
|
|
22
|
+
* `keyFiles`, sorted by path. The file walk and the reads are the caller's
|
|
23
|
+
* (the composition root), so this stays free of I/O.
|
|
24
|
+
*/
|
|
25
|
+
function findDeprecatedKeyFiles(io) {
|
|
26
|
+
return [...io.specPaths].sort().flatMap((path) => {
|
|
27
|
+
const usage = (0, keyfiles_source_js_1.findKeyFilesInSpecSource)(io.read(path));
|
|
28
|
+
return usage === null ? [] : [{ path, ...usage }];
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/** The lint message, naming the nested file after the spec's own target. */
|
|
32
|
+
const messageFor = (f) => (0, keyfiles_js_1.keyFilesDeprecationMessage)(f, (0, keyfiles_js_1.instructionFileOfSpec)(f.path));
|
|
33
|
+
/**
|
|
34
|
+
* `lint`'s side of the rule. Runs ONCE over the repository, like the
|
|
35
|
+
* instruction-weight ratchet: a nested bundle's spec would otherwise be
|
|
36
|
+
* reported again by every bundle that encloses it. Warning by default — a
|
|
37
|
+
* deprecation that gated would turn a consumer's CI red on upgrade, which the
|
|
38
|
+
* one-major window exists to prevent.
|
|
39
|
+
*/
|
|
40
|
+
function checkDeprecatedKeyFiles(opts) {
|
|
41
|
+
const { severity } = opts;
|
|
42
|
+
if (!severity)
|
|
43
|
+
return { issues: 0, errors: 0 };
|
|
44
|
+
const found = opts.find();
|
|
45
|
+
const mark = severity === "error" ? "✗" : "⚠";
|
|
46
|
+
const level = severity === "error" ? "error" : "warning";
|
|
47
|
+
if (!opts.silent && found.length > 0)
|
|
48
|
+
console.log("\nDeprecated-key-files check:\n");
|
|
49
|
+
// Silent (`--json`, `--summary`) prints nothing at all, annotations
|
|
50
|
+
// included: they go to stdout and would corrupt the JSON.
|
|
51
|
+
if (!opts.silent)
|
|
52
|
+
found.forEach((f) => {
|
|
53
|
+
const message = messageFor(f);
|
|
54
|
+
console.log(` ${mark} ${f.path}: ${message}`);
|
|
55
|
+
opts.annotate(level, message, f.path);
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
issues: found.length,
|
|
59
|
+
errors: severity === "error" ? found.length : 0,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=deprecated-key-files.js.map
|
package/dist/research-index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Research-index completeness — the deterministic FLOOR keeping the `research/`
|
|
3
3
|
* corpus and its index (`research/CLAUDE.md.spec.ts`) in sync. The spec's
|
|
4
|
-
* `
|
|
5
|
-
* already verifies the OTHER direction (every
|
|
6
|
-
* `vigiles compile` fails), so the only open gap is a doc that was ADDED but
|
|
4
|
+
* `file()` references are the AGENT-FACING index of every research doc; the
|
|
5
|
+
* compiler already verifies the OTHER direction (every referenced path EXISTS,
|
|
6
|
+
* else `vigiles compile` fails), so the only open gap is a doc that was ADDED but
|
|
7
7
|
* never indexed. This check closes it: every `research/*.md` (except the
|
|
8
8
|
* human-facing `README.md`) must appear in the index, or the dogfood test fails.
|
|
9
9
|
*
|
|
@@ -17,13 +17,13 @@ export declare const INDEX_EXEMPT: readonly ["README.md"];
|
|
|
17
17
|
/**
|
|
18
18
|
* Research doc basenames (e.g. `roadmap.md`) NOT referenced anywhere in
|
|
19
19
|
* `indexContent`. A doc counts as indexed if its repo-relative path
|
|
20
|
-
* (`research/<name>.md`) appears in the index — the exact form
|
|
21
|
-
*
|
|
20
|
+
* (`research/<name>.md`) appears in the index — the exact form a `file()`
|
|
21
|
+
* reference takes. Exempt docs (the README) are never flagged.
|
|
22
22
|
*/
|
|
23
23
|
export declare function unindexedResearchDocs(docFilenames: readonly string[], indexContent: string, exempt?: readonly string[]): string[];
|
|
24
24
|
/**
|
|
25
25
|
* Index entries pointing at a `research/<name>.md` that no longer exists on
|
|
26
|
-
* disk. The compiler catches this at compile time (a missing `
|
|
26
|
+
* disk. The compiler catches this at compile time (a missing `file()` path is
|
|
27
27
|
* a compile error), so this is a belt-and-suspenders reader for a test that
|
|
28
28
|
* wants to assert it directly without invoking the compiler.
|
|
29
29
|
*/
|
package/dist/research-index.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Research-index completeness — the deterministic FLOOR keeping the `research/`
|
|
4
4
|
* corpus and its index (`research/CLAUDE.md.spec.ts`) in sync. The spec's
|
|
5
|
-
* `
|
|
6
|
-
* already verifies the OTHER direction (every
|
|
7
|
-
* `vigiles compile` fails), so the only open gap is a doc that was ADDED but
|
|
5
|
+
* `file()` references are the AGENT-FACING index of every research doc; the
|
|
6
|
+
* compiler already verifies the OTHER direction (every referenced path EXISTS,
|
|
7
|
+
* else `vigiles compile` fails), so the only open gap is a doc that was ADDED but
|
|
8
8
|
* never indexed. This check closes it: every `research/*.md` (except the
|
|
9
9
|
* human-facing `README.md`) must appear in the index, or the dogfood test fails.
|
|
10
10
|
*
|
|
@@ -22,15 +22,15 @@ exports.INDEX_EXEMPT = ["README.md"];
|
|
|
22
22
|
/**
|
|
23
23
|
* Research doc basenames (e.g. `roadmap.md`) NOT referenced anywhere in
|
|
24
24
|
* `indexContent`. A doc counts as indexed if its repo-relative path
|
|
25
|
-
* (`research/<name>.md`) appears in the index — the exact form
|
|
26
|
-
*
|
|
25
|
+
* (`research/<name>.md`) appears in the index — the exact form a `file()`
|
|
26
|
+
* reference takes. Exempt docs (the README) are never flagged.
|
|
27
27
|
*/
|
|
28
28
|
function unindexedResearchDocs(docFilenames, indexContent, exempt = exports.INDEX_EXEMPT) {
|
|
29
29
|
return docFilenames.filter((name) => !exempt.includes(name) && !indexContent.includes(`research/${name}`));
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* Index entries pointing at a `research/<name>.md` that no longer exists on
|
|
33
|
-
* disk. The compiler catches this at compile time (a missing `
|
|
33
|
+
* disk. The compiler catches this at compile time (a missing `file()` path is
|
|
34
34
|
* a compile error), so this is a belt-and-suspenders reader for a test that
|
|
35
35
|
* wants to assert it directly without invoking the compiler.
|
|
36
36
|
*/
|
package/dist/scan.d.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { type PluginLayout } from "./core/layout.js";
|
|
15
15
|
import type { ExcludeSet } from "./exclude.js";
|
|
16
|
+
import type { DeprecatedKeyFilesFinding } from "./deprecated-key-files.js";
|
|
16
17
|
import type { HarnessDialect } from "./core/dialect.js";
|
|
17
18
|
import type { ToolIssue } from "./core/tool-contract.js";
|
|
18
19
|
import { type HookEventIssue } from "./core/hook-events.js";
|
|
@@ -329,6 +330,19 @@ export interface ScanReport {
|
|
|
329
330
|
* on day one is switched off on day one.
|
|
330
331
|
*/
|
|
331
332
|
readonly instructionWeight: InstructionWeight | null;
|
|
333
|
+
/**
|
|
334
|
+
* Instruction specs still using the deprecated `keyFiles` — the
|
|
335
|
+
* `deprecated-key-files` rule's own detector (`findDeprecatedKeyFiles`),
|
|
336
|
+
* handed in through `opts` by `audit`, so `lint` and `audit` cannot
|
|
337
|
+
* disagree. Advisory: printed with its fix, never scored.
|
|
338
|
+
*
|
|
339
|
+
* ABSENT when there are none, never `[]`, for the parity reason
|
|
340
|
+
* `coverageCaveats` gives. ⚠️ The browser twin produces none: it would need the
|
|
341
|
+
* TypeScript parser in the site bundle, and the demo does not fetch spec
|
|
342
|
+
* sources. No vendored parity fixture carries a spec, so the gate stays
|
|
343
|
+
* byte-identical; a spec added to one would show this gap as a parity diff.
|
|
344
|
+
*/
|
|
345
|
+
readonly deprecatedKeyFiles?: readonly DeprecatedKeyFilesFinding[];
|
|
332
346
|
/** Skills/agents whose `---` block isn't valid YAML — informational (may still load via salvage). */
|
|
333
347
|
readonly malformedFrontmatter: readonly FrontmatterParseIssue[];
|
|
334
348
|
readonly warnings: readonly string[];
|
|
@@ -444,6 +458,13 @@ export declare function scanPlugin(dir: string, layout: PluginLayout, dialect: H
|
|
|
444
458
|
* context `overBundles` builds, so a new checker cannot forget it).
|
|
445
459
|
*/
|
|
446
460
|
excludes?: ExcludeSet;
|
|
461
|
+
/**
|
|
462
|
+
* The `deprecated-key-files` findings for this directory, from the rule's
|
|
463
|
+
* own detector (`findDeprecatedKeyFiles`), computed by the composition root
|
|
464
|
+
* — the one place allowed to walk the repository and read specs. `audit`
|
|
465
|
+
* passes them; every other caller leaves the field off the report.
|
|
466
|
+
*/
|
|
467
|
+
deprecatedKeyFiles?: readonly DeprecatedKeyFilesFinding[];
|
|
447
468
|
/**
|
|
448
469
|
* The repo's `.vigilesrc.json#harnesses`, resolved — every declared harness
|
|
449
470
|
* with its own layout, dialect and roots, IN DECLARATION ORDER.
|