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.
@@ -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
@@ -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
@@ -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, a spec's Key Files, 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`).");
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
@@ -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. */
@@ -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"];
@@ -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",
@@ -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. */
@@ -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 = [];
@@ -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
- }): EnforceRule;
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
- /** File paths → descriptions. Verified via existsSync. */
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
- return { _kind: "guidance", text };
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.
@@ -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
@@ -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
- * `keyFiles` map is the AGENT-FACING index of every research doc; the compiler
5
- * already verifies the OTHER direction (every indexed path EXISTS, else
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 the spec's
21
- * `keyFiles` keys use. Exempt docs (the README) are never flagged.
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 `keyFiles` path is
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
  */
@@ -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
- * `keyFiles` map is the AGENT-FACING index of every research doc; the compiler
6
- * already verifies the OTHER direction (every indexed path EXISTS, else
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 the spec's
26
- * `keyFiles` keys use. Exempt docs (the README) are never flagged.
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 `keyFiles` path is
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.