vigiles 38.0.0 → 38.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/adapters/claude-code/dialect.js +5 -1
  2. package/dist/audit-report.d.ts +8 -0
  3. package/dist/audit-report.js +12 -3
  4. package/dist/cli-main.d.ts +7 -0
  5. package/dist/cli-main.js +53 -10
  6. package/dist/core/CLAUDE.md.spec.js +0 -4
  7. package/dist/core/compile.js +15 -9
  8. package/dist/core/config-schema.d.ts +6 -1
  9. package/dist/core/config-schema.js +36 -2
  10. package/dist/core/instruction-baseline.d.ts +26 -0
  11. package/dist/core/instruction-baseline.js +45 -1
  12. package/dist/core/instruction-remedies.d.ts +13 -0
  13. package/dist/core/instruction-remedies.js +24 -0
  14. package/dist/core/instruction-weight.d.ts +6 -1
  15. package/dist/core/keyfiles-source.d.ts +9 -0
  16. package/dist/core/keyfiles-source.js +88 -0
  17. package/dist/core/keyfiles.d.ts +64 -0
  18. package/dist/core/keyfiles.js +93 -0
  19. package/dist/core/orphans.js +1 -1
  20. package/dist/core/rule-meta.js +9 -0
  21. package/dist/core/session.d.ts +7 -0
  22. package/dist/core/session.js +8 -0
  23. package/dist/core/spec.d.ts +14 -1
  24. package/dist/core/types.d.ts +26 -1
  25. package/dist/deprecated-key-files.d.ts +35 -0
  26. package/dist/deprecated-key-files.js +62 -0
  27. package/dist/instruction-weight-ratchet.d.ts +17 -4
  28. package/dist/instruction-weight-ratchet.js +71 -22
  29. package/dist/research-index.d.ts +6 -6
  30. package/dist/research-index.js +6 -6
  31. package/dist/scan.d.ts +21 -0
  32. package/dist/scan.js +29 -2
  33. package/dist/setup-plan.d.ts +1 -1
  34. package/dist/setup-plan.js +3 -0
  35. package/dist/vigilesrc.schema.json +86 -0
  36. package/hooks/pre-edit.sh +1 -1
  37. package/package.json +1 -1
  38. package/skills/adopt-spec/SKILL.md +7 -12
  39. package/skills/edit-spec/SKILL.md +32 -25
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.findKeyFilesInSpecSource = findKeyFilesInSpecSource;
7
+ /**
8
+ * Reads `keyFiles` out of an instruction spec's SOURCE, without running it.
9
+ *
10
+ * `lint` and `audit` both report the deprecated field (`deprecated-key-files`),
11
+ * and neither may import a spec: `audit` is a side-effect-free read, and a spec
12
+ * is the user's program. So the source is PARSED (TypeScript syntax tree) and
13
+ * the field is read off the tree — a `keyFiles` inside a comment or a string is
14
+ * not usage, and a text search could not tell the difference.
15
+ *
16
+ * Pure: the file walk that feeds it is `src/deprecated-key-files.ts`.
17
+ */
18
+ const typescript_1 = __importDefault(require("typescript"));
19
+ const keyfiles_js_1 = require("./keyfiles.js");
20
+ /** Field used, size not knowable from the syntax alone. */
21
+ const UNMEASURED = (entries) => ({
22
+ entries,
23
+ chars: 0,
24
+ measured: false,
25
+ });
26
+ /** A literal key or value: `"a"`, `'a'`, `` `a` `` (and, for a key only, `a`). */
27
+ function literalText(node) {
28
+ return typescript_1.default.isStringLiteral(node) || typescript_1.default.isNoSubstitutionTemplateLiteral(node)
29
+ ? node.text
30
+ : undefined;
31
+ }
32
+ /** One `path: "description"` pair, or `undefined` when either side is not literal. */
33
+ function literalEntry(prop) {
34
+ if (!typescript_1.default.isPropertyAssignment(prop))
35
+ return undefined;
36
+ const key = typescript_1.default.isIdentifier(prop.name)
37
+ ? prop.name.text
38
+ : literalText(prop.name);
39
+ const value = literalText(prop.initializer);
40
+ return key === undefined || value === undefined ? undefined : [key, value];
41
+ }
42
+ /**
43
+ * Read a `{ path: "description" }` object literal. A computed key or value, or a
44
+ * spread, makes the map unmeasurable: its pairs are still counted (each one
45
+ * compiles to a line), its size is not guessed.
46
+ */
47
+ function measureLiteral(obj) {
48
+ const pairs = obj.properties.filter(typescript_1.default.isPropertyAssignment);
49
+ const literal = obj.properties
50
+ .map(literalEntry)
51
+ .filter((e) => e !== undefined);
52
+ return literal.length === obj.properties.length
53
+ ? (0, keyfiles_js_1.measureKeyFiles)(Object.fromEntries(literal))
54
+ : UNMEASURED(pairs.length);
55
+ }
56
+ /** The usage a node declares, if it is a `keyFiles` property. */
57
+ function usageAt(node) {
58
+ if (typescript_1.default.isShorthandPropertyAssignment(node))
59
+ return node.name.text === "keyFiles" ? UNMEASURED(0) : undefined;
60
+ if (!typescript_1.default.isPropertyAssignment(node))
61
+ return undefined;
62
+ const name = typescript_1.default.isIdentifier(node.name)
63
+ ? node.name.text
64
+ : literalText(node.name);
65
+ if (name !== "keyFiles")
66
+ return undefined;
67
+ return typescript_1.default.isObjectLiteralExpression(node.initializer)
68
+ ? measureLiteral(node.initializer)
69
+ : UNMEASURED(0);
70
+ }
71
+ /** Depth-first: the first `keyFiles` property under `node`. */
72
+ function firstUsage(node) {
73
+ return (usageAt(node) ??
74
+ typescript_1.default.forEachChild(node, (child) => firstUsage(child)) ??
75
+ undefined);
76
+ }
77
+ /**
78
+ * The `keyFiles` usage in one spec's source, or `null` when the spec does not
79
+ * use the field. The first `keyFiles:` property in any object literal counts —
80
+ * an instruction spec has one builder call, and nothing else in a spec is
81
+ * called that.
82
+ */
83
+ function findKeyFilesInSpecSource(source) {
84
+ const sf = typescript_1.default.createSourceFile("spec.ts", source, typescript_1.default.ScriptTarget.Latest,
85
+ /* setParentNodes */ false, typescript_1.default.ScriptKind.TS);
86
+ return firstUsage(sf) ?? null;
87
+ }
88
+ //# sourceMappingURL=keyfiles-source.js.map
@@ -0,0 +1,64 @@
1
+ /**
2
+ * `keyFiles` — DEPRECATED, removed in the major after the one that ships this
3
+ * notice (the one-major alias window in `core/spec.ts`).
4
+ *
5
+ * WHY. The field compiles a flat "## Key Files" path → description list into
6
+ * the instruction file the harness loads on EVERY request. Measured on this
7
+ * repository's own root spec (2026-10-11): 230 entries, 45,467 characters —
8
+ * 29% of a 157,872-character file, paid on every request, to tell an agent where files are
9
+ * when it finds them itself with Glob/Grep. Anthropic's own guidance lists
10
+ * "File-by-file descriptions of the codebase" under what to EXCLUDE from
11
+ * `CLAUDE.md` (https://code.claude.com/docs/en/best-practices).
12
+ *
13
+ * THE REPLACEMENT, decided per entry, in this order:
14
+ * - the file's own header, the docs or the path already say it → delete it;
15
+ * - a fact about that one file, written nowhere else → that file's header
16
+ * comment, where a reader meets it on opening the file;
17
+ * - a rule an agent must follow while working in one folder → that folder's
18
+ * nested instruction file (`<folder>/CLAUDE.md` from its own spec), which
19
+ * loads only when the agent works there;
20
+ * - only when nothing else would lead an agent to the file → a one-line
21
+ * `file()` pointer where the decision that needs it is written (verified
22
+ * exactly like a `keyFiles` path, by `validateFileRef`).
23
+ *
24
+ * This module is pure: the renderer `compile` uses, the measurement the
25
+ * `deprecated-key-files` rule reports, and its message — one wording for lint
26
+ * and audit.
27
+ */
28
+ /** Where the migration is written up. Printed in every finding. */
29
+ export declare const KEYFILES_DOCS_URL = "https://github.com/zernie/vigiles/blob/main/docs/rules/deprecated-key-files.md";
30
+ /**
31
+ * The compiled "## Key Files" section — the ONE renderer, so the size a
32
+ * finding reports is the size of what actually loads.
33
+ */
34
+ export declare function renderKeyFiles(keyFiles: Readonly<Record<string, string>>): string;
35
+ /**
36
+ * What a spec's `keyFiles` costs. `measured: false` when the map is not a
37
+ * literal (a variable, a computed key or value, a spread) — then the field is
38
+ * known to be USED and its size is not guessed.
39
+ */
40
+ export interface KeyFilesUsage {
41
+ readonly entries: number;
42
+ readonly chars: number;
43
+ readonly measured: boolean;
44
+ }
45
+ /** Measure a map the way it compiles. */
46
+ export declare function measureKeyFiles(keyFiles: Readonly<Record<string, string>>): KeyFilesUsage;
47
+ /** The cost half of a finding, e.g. `230 entries, 45,467 characters compiled into …`. */
48
+ export declare function keyFilesCost(u: KeyFilesUsage): string;
49
+ /**
50
+ * The instruction file a spec compiles to — `pkg/AGENTS.md.spec.ts` →
51
+ * `AGENTS.md` — so the fix names the nested file in the reader's own harness
52
+ * instead of assuming one.
53
+ */
54
+ export declare function instructionFileOfSpec(specPath: string): string;
55
+ /**
56
+ * The fix, one sentence — shared by the lint message and the audit line. The
57
+ * four places an entry can go, in the order to try them; the full recipe is
58
+ * the migration guide. `targets` are the instruction files the reported specs
59
+ * compile to (`CLAUDE.md`, `AGENTS.md`), so the nested file is named for real.
60
+ */
61
+ export declare function keyFilesFix(targets: readonly string[]): string;
62
+ /** The whole finding: what it costs, then where each entry goes and where it is written up. */
63
+ export declare function keyFilesDeprecationMessage(u: KeyFilesUsage, target: string): string;
64
+ //# sourceMappingURL=keyfiles.d.ts.map
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ /**
3
+ * `keyFiles` — DEPRECATED, removed in the major after the one that ships this
4
+ * notice (the one-major alias window in `core/spec.ts`).
5
+ *
6
+ * WHY. The field compiles a flat "## Key Files" path → description list into
7
+ * the instruction file the harness loads on EVERY request. Measured on this
8
+ * repository's own root spec (2026-10-11): 230 entries, 45,467 characters —
9
+ * 29% of a 157,872-character file, paid on every request, to tell an agent where files are
10
+ * when it finds them itself with Glob/Grep. Anthropic's own guidance lists
11
+ * "File-by-file descriptions of the codebase" under what to EXCLUDE from
12
+ * `CLAUDE.md` (https://code.claude.com/docs/en/best-practices).
13
+ *
14
+ * THE REPLACEMENT, decided per entry, in this order:
15
+ * - the file's own header, the docs or the path already say it → delete it;
16
+ * - a fact about that one file, written nowhere else → that file's header
17
+ * comment, where a reader meets it on opening the file;
18
+ * - a rule an agent must follow while working in one folder → that folder's
19
+ * nested instruction file (`<folder>/CLAUDE.md` from its own spec), which
20
+ * loads only when the agent works there;
21
+ * - only when nothing else would lead an agent to the file → a one-line
22
+ * `file()` pointer where the decision that needs it is written (verified
23
+ * exactly like a `keyFiles` path, by `validateFileRef`).
24
+ *
25
+ * This module is pure: the renderer `compile` uses, the measurement the
26
+ * `deprecated-key-files` rule reports, and its message — one wording for lint
27
+ * and audit.
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.KEYFILES_DOCS_URL = void 0;
31
+ exports.renderKeyFiles = renderKeyFiles;
32
+ exports.measureKeyFiles = measureKeyFiles;
33
+ exports.keyFilesCost = keyFilesCost;
34
+ exports.instructionFileOfSpec = instructionFileOfSpec;
35
+ exports.keyFilesFix = keyFilesFix;
36
+ exports.keyFilesDeprecationMessage = keyFilesDeprecationMessage;
37
+ /** Where the migration is written up. Printed in every finding. */
38
+ exports.KEYFILES_DOCS_URL = "https://github.com/zernie/vigiles/blob/main/docs/rules/deprecated-key-files.md";
39
+ /**
40
+ * The compiled "## Key Files" section — the ONE renderer, so the size a
41
+ * finding reports is the size of what actually loads.
42
+ */
43
+ function renderKeyFiles(keyFiles) {
44
+ return [
45
+ "## Key Files",
46
+ "",
47
+ ...Object.entries(keyFiles).map(([path, desc]) => `- \`${path}\` — ${desc}`),
48
+ ].join("\n");
49
+ }
50
+ /** Measure a map the way it compiles. */
51
+ function measureKeyFiles(keyFiles) {
52
+ return {
53
+ entries: Object.keys(keyFiles).length,
54
+ chars: renderKeyFiles(keyFiles).length,
55
+ measured: true,
56
+ };
57
+ }
58
+ const grouped = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
59
+ /** The cost half of a finding, e.g. `230 entries, 45,467 characters compiled into …`. */
60
+ function keyFilesCost(u) {
61
+ if (!u.measured)
62
+ return "not a literal map — size not measured";
63
+ const noun = u.entries === 1 ? "entry" : "entries";
64
+ return `${String(u.entries)} ${noun}, ${grouped(u.chars)} characters compiled into the instruction file`;
65
+ }
66
+ /**
67
+ * The instruction file a spec compiles to — `pkg/AGENTS.md.spec.ts` →
68
+ * `AGENTS.md` — so the fix names the nested file in the reader's own harness
69
+ * instead of assuming one.
70
+ */
71
+ function instructionFileOfSpec(specPath) {
72
+ return specPath.replace(/^.*[\\/]/, "").replace(/\.spec\.ts$/, "");
73
+ }
74
+ /**
75
+ * The fix, one sentence — shared by the lint message and the audit line. The
76
+ * four places an entry can go, in the order to try them; the full recipe is
77
+ * the migration guide. `targets` are the instruction files the reported specs
78
+ * compile to (`CLAUDE.md`, `AGENTS.md`), so the nested file is named for real.
79
+ */
80
+ function keyFilesFix(targets) {
81
+ // In the order given — the detector sorts findings by path — once each.
82
+ const nested = Array.from(new Set(targets), (t) => `<folder>/${t}`).join(" or ");
83
+ return ("For each entry: delete it if the file or the docs already say it · " +
84
+ "move a fact about one file into that file's header comment · " +
85
+ `move a rule for one folder into ${nested} · ` +
86
+ "keep a one-line file() pointer only if nothing else leads there");
87
+ }
88
+ /** The whole finding: what it costs, then where each entry goes and where it is written up. */
89
+ function keyFilesDeprecationMessage(u, target) {
90
+ return (`\`keyFiles\` is deprecated and goes away in the next major (${keyFilesCost(u)}). ` +
91
+ `${keyFilesFix([target])} (guide: ${exports.KEYFILES_DOCS_URL})`);
92
+ }
93
+ //# sourceMappingURL=keyfiles.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
@@ -238,6 +238,15 @@ exports.RULE_META = {
238
238
  detector: "checkContentBudgets",
239
239
  upstreamPrevention: "compile measures the spec before writing; a render policy (enforce render, guidance details) meets a budget without deleting a rule",
240
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
+ },
241
250
  "skill-description-budget": {
242
251
  id: "skill-description-budget",
243
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 = [];
@@ -352,7 +352,19 @@ export interface ClaudeSpec {
352
352
  readonly target?: InstructionTarget | InstructionTarget[];
353
353
  /** npm scripts / shell commands → descriptions. Verified against package.json. */
354
354
  readonly commands?: Record<string, string>;
355
- /** 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
+ */
356
368
  readonly keyFiles?: Record<string, string>;
357
369
  /** Named prose sections — plain strings or tagged templates with file()/cmd()/ref(). */
358
370
  readonly sections?: Record<string, string | InstructionFragment[]>;
@@ -379,6 +391,7 @@ export interface ClaudeSpec {
379
391
  type ClaudeSpecBase = {
380
392
  readonly target?: InstructionTarget | InstructionTarget[];
381
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. */
382
395
  readonly keyFiles?: Record<string, string>;
383
396
  readonly maxTokens?: number;
384
397
  readonly rules: Record<string, Rule>;
@@ -55,6 +55,19 @@ export interface CoverageThresholds {
55
55
  /** Min % of npm scripts documented in spec commands. */
56
56
  scripts?: number;
57
57
  }
58
+ /**
59
+ * Options for the `instruction-weight` rule: `["error", { max: 40000 }]`.
60
+ */
61
+ export interface InstructionWeightConfig {
62
+ /**
63
+ * An opt-in ABSOLUTE ceiling on the always-loaded instruction weight, in the
64
+ * harness's own unit (characters for Claude Code, bytes for Codex). A total
65
+ * above it is a finding at the rule's severity regardless of the baseline, and
66
+ * `vigiles lint --update-baseline` refuses to record a baseline above it.
67
+ * Omitted → only the ratchet applies. A positive integer.
68
+ */
69
+ max?: number;
70
+ }
58
71
  /**
59
72
  * Options for the orphan-docs check. The PRESENCE of this block in
60
73
  * `.vigilesrc.json` OPTS THE REPO IN — the scan is off unless declared,
@@ -140,8 +153,20 @@ export interface RulesConfig {
140
153
  * repo's own past, not a fixed budget. No baseline file → a one-line note,
141
154
  * never a finding, so turning this on fails no repository on day one.
142
155
  * `vigiles lint --update-baseline` records it. Default: "error".
156
+ *
157
+ * `["error", { max: 40000 }]` adds an opt-in absolute ceiling on top of the
158
+ * ratchet: a total above `max` is a finding whatever the baseline says, and
159
+ * the baseline cannot be re-recorded above it.
160
+ */
161
+ "instruction-weight"?: RuleWithOptions<InstructionWeightConfig>;
162
+ /**
163
+ * An instruction spec still uses the deprecated `keyFiles` field — a flat
164
+ * path → description list compiled into the file loaded on every request.
165
+ * Reports the entry count and compiled size with the migration (a `file()`
166
+ * ref in the rule that needs the pointer). The spec is parsed, never run.
167
+ * Default "warn": a deprecation must not turn a consumer's CI red on upgrade.
143
168
  */
144
- "instruction-weight"?: RuleSeverity;
169
+ "deprecated-key-files"?: RuleSeverity;
145
170
  /**
146
171
  * Near-duplicate rules WITHIN one spec, by NCD similarity — spec bloat, two
147
172
  * rules saying the same thing in different words.
@@ -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
@@ -24,23 +24,35 @@ export interface RatchetLine {
24
24
  }
25
25
  /**
26
26
  * Pure: the lines a run prints, given what was measured and what is on disk.
27
+ * `max` is the opt-in absolute ceiling; a bundle over it adds a finding of its
28
+ * own, whatever the baseline says (including when there is none, or it is
29
+ * unreadable).
27
30
  *
28
31
  * A bundle with no entry is a nudge while the repo has NO baseline file (it has
29
32
  * not opted in, and a fresh repo must not fail on day one) — but a FINDING once
30
33
  * the file exists: a bundle that appeared after the baseline was recorded is
31
34
  * weight nobody signed off on.
32
35
  */
33
- export declare function ratchetLines(onDisk: BaselineOnDisk, measured: readonly MeasuredBundle[]): readonly RatchetLine[];
34
- /** Run the ratchet for `lint`: print, annotate, count. */
35
- export declare function checkInstructionWeightRatchet(opts: {
36
+ export declare function ratchetLines(onDisk: BaselineOnDisk, measured: readonly MeasuredBundle[], max?: number): readonly RatchetLine[];
37
+ /**
38
+ * The bundles `--update-baseline` must not record: each one over the ceiling,
39
+ * as the refusal text. Empty when there is no ceiling or nothing exceeds it.
40
+ */
41
+ export declare function updateRefusals(measured: readonly MeasuredBundle[], max: number | undefined): readonly string[];
42
+ /** The options {@link checkInstructionWeightRatchet} takes. */
43
+ interface RatchetOptions {
36
44
  readonly root: string;
37
45
  readonly severity: RuleSeverity;
46
+ /** The opt-in absolute ceiling (`["error", { max }]`); `undefined` = ratchet only. */
47
+ readonly max?: number;
38
48
  readonly update: boolean;
39
49
  readonly silent: boolean;
40
50
  /** Called only when the rule is on or an update was asked for. */
41
51
  readonly measure: () => readonly MeasuredBundle[];
42
52
  readonly annotate: (level: "error" | "warning", message: string) => void;
43
- }): {
53
+ }
54
+ /** Run the ratchet for `lint`: print, annotate, count. */
55
+ export declare function checkInstructionWeightRatchet(opts: RatchetOptions): {
44
56
  issues: number;
45
57
  errors: number;
46
58
  };
@@ -50,4 +62,5 @@ export declare function checkInstructionWeightRatchet(opts: {
50
62
  * `lint` stays the single gate. `null` when there is nothing to weigh.
51
63
  */
52
64
  export declare function compileWeightLine(root: string, weight: InstructionWeight | null): string | null;
65
+ export {};
53
66
  //# sourceMappingURL=instruction-weight-ratchet.d.ts.map
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.readInstructionBaseline = readInstructionBaseline;
4
4
  exports.writeInstructionBaseline = writeInstructionBaseline;
5
5
  exports.ratchetLines = ratchetLines;
6
+ exports.updateRefusals = updateRefusals;
6
7
  exports.checkInstructionWeightRatchet = checkInstructionWeightRatchet;
7
8
  exports.compileWeightLine = compileWeightLine;
8
9
  /**
@@ -41,13 +42,29 @@ function writeInstructionBaseline(root, onDisk, measured) {
41
42
  }
42
43
  /**
43
44
  * Pure: the lines a run prints, given what was measured and what is on disk.
45
+ * `max` is the opt-in absolute ceiling; a bundle over it adds a finding of its
46
+ * own, whatever the baseline says (including when there is none, or it is
47
+ * unreadable).
44
48
  *
45
49
  * A bundle with no entry is a nudge while the repo has NO baseline file (it has
46
50
  * not opted in, and a fresh repo must not fail on day one) — but a FINDING once
47
51
  * the file exists: a bundle that appeared after the baseline was recorded is
48
52
  * weight nobody signed off on.
49
53
  */
50
- function ratchetLines(onDisk, measured) {
54
+ function ratchetLines(onDisk, measured, max) {
55
+ return [
56
+ ...baselineLines(onDisk, measured),
57
+ ...measured.flatMap(([at, weight]) => ceilingLine(at, weight, max)),
58
+ ];
59
+ }
60
+ /** One bundle's ceiling finding, or nothing when it is at or under `max`. */
61
+ function ceilingLine(at, weight, max) {
62
+ const over = weight === null ? undefined : (0, instruction_baseline_js_1.checkCeiling)(weight, max);
63
+ return over === undefined
64
+ ? []
65
+ : [{ at, status: "finding", text: (0, instruction_baseline_js_1.formatCeiling)(over, at) }];
66
+ }
67
+ function baselineLines(onDisk, measured) {
51
68
  if (onDisk.kind === "invalid") {
52
69
  return [
53
70
  {
@@ -77,41 +94,73 @@ function ratchetLines(onDisk, measured) {
77
94
  return [{ at, status, text: `${(0, instruction_baseline_js_1.formatVerdict)(verdict, at)}${unfollowed}` }];
78
95
  });
79
96
  }
97
+ /**
98
+ * The bundles `--update-baseline` must not record: each one over the ceiling,
99
+ * as the refusal text. Empty when there is no ceiling or nothing exceeds it.
100
+ */
101
+ function updateRefusals(measured, max) {
102
+ return measured.flatMap(([at, weight]) => {
103
+ const over = weight === null ? undefined : (0, instruction_baseline_js_1.checkCeiling)(weight, max);
104
+ return over === undefined ? [] : [(0, instruction_baseline_js_1.formatRefusal)(over, at)];
105
+ });
106
+ }
107
+ /**
108
+ * What `lint` compares against: the file as it is, or — under `--update-baseline`
109
+ * and when nothing is over the ceiling — the file just written. A baseline above
110
+ * the ceiling would bypass it (re-recording is how a ratchet gets loosened), so
111
+ * then nothing is written and the refusals are returned for the caller to fail on.
112
+ */
113
+ function recordOrRead(opts, measured) {
114
+ const before = readInstructionBaseline(opts.root);
115
+ const refusals = opts.update ? updateRefusals(measured, opts.max) : [];
116
+ const record = opts.update && refusals.length === 0;
117
+ return {
118
+ refusals,
119
+ onDisk: record
120
+ ? {
121
+ kind: "ok",
122
+ baseline: writeInstructionBaseline(opts.root, before, measured),
123
+ }
124
+ : before,
125
+ };
126
+ }
127
+ /** The human-readable report: what was recorded, then one line per bundle. */
128
+ function printReport(lines, severity, recorded) {
129
+ console.log("\nInstruction-weight ratchet:\n");
130
+ if (recorded)
131
+ console.log(` ✓ recorded ${instruction_baseline_js_1.INSTRUCTION_BASELINE_FILE}`);
132
+ const marks = {
133
+ finding: severity === "error" ? "✗" : "⚠",
134
+ ok: "✓",
135
+ note: "ℹ",
136
+ };
137
+ lines.forEach((l) => {
138
+ console.log(` ${marks[l.status]} ${l.text}`);
139
+ });
140
+ }
80
141
  /** Run the ratchet for `lint`: print, annotate, count. */
81
142
  function checkInstructionWeightRatchet(opts) {
82
143
  const { severity, update, silent } = opts;
83
144
  if (!severity && !update)
84
145
  return { issues: 0, errors: 0 };
85
146
  const measured = opts.measure();
86
- const before = readInstructionBaseline(opts.root);
87
- const onDisk = update
88
- ? {
89
- kind: "ok",
90
- baseline: writeInstructionBaseline(opts.root, before, measured),
91
- }
92
- : before;
93
- const lines = severity ? ratchetLines(onDisk, measured) : [];
147
+ const { onDisk, refusals } = recordOrRead(opts, measured);
148
+ refusals.forEach((message) => {
149
+ console.error(`✗ ${message}`);
150
+ opts.annotate("error", message);
151
+ });
152
+ const lines = severity ? ratchetLines(onDisk, measured, opts.max) : [];
94
153
  const findings = lines.filter((l) => l.status === "finding");
95
154
  if (!silent && (update || lines.length > 0)) {
96
- console.log("\nInstruction-weight ratchet:\n");
97
- if (update)
98
- console.log(` ✓ recorded ${instruction_baseline_js_1.INSTRUCTION_BASELINE_FILE}`);
99
- const marks = {
100
- finding: severity === "error" ? "✗" : "⚠",
101
- ok: "✓",
102
- note: "ℹ",
103
- };
104
- lines.forEach((l) => {
105
- console.log(` ${marks[l.status]} ${l.text}`);
106
- });
155
+ printReport(lines, severity, update && refusals.length === 0);
107
156
  }
108
157
  const level = severity === "error" ? "error" : "warning";
109
158
  findings.forEach((l) => {
110
159
  opts.annotate(level, l.text);
111
160
  });
112
161
  return {
113
- issues: findings.length,
114
- errors: severity === "error" ? findings.length : 0,
162
+ issues: findings.length + refusals.length,
163
+ errors: (severity === "error" ? findings.length : 0) + refusals.length,
115
164
  };
116
165
  }
117
166
  /**