vigiles 38.2.0 → 39.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.
Files changed (129) hide show
  1. package/action.yml +18 -0
  2. package/dist/adapters/claude-code/event-capability.js +24 -8
  3. package/dist/adapters/claude-code/hook-protocol.js +2 -1
  4. package/dist/adapters/codex/hook-protocol.js +2 -1
  5. package/dist/audit-report.d.ts +14 -1
  6. package/dist/audit-report.js +21 -5
  7. package/dist/audit-report.template.html +33 -28
  8. package/dist/audit-score.d.ts +4 -1
  9. package/dist/audit-score.js +3 -1
  10. package/dist/audit-verdict.d.ts +3 -1
  11. package/dist/cli-flag-check.d.ts +36 -0
  12. package/dist/cli-flag-check.js +76 -1
  13. package/dist/cli-flags.d.ts +1 -1
  14. package/dist/cli-main.d.ts +2 -2
  15. package/dist/cli-main.js +684 -434
  16. package/dist/core/CLAUDE.md.spec.js +2 -2
  17. package/dist/core/apply-rules.d.ts +71 -0
  18. package/dist/core/apply-rules.js +48 -0
  19. package/dist/core/bash-effects.d.ts +0 -57
  20. package/dist/core/bash-effects.js +0 -147
  21. package/dist/core/compile-generator.d.ts +6 -6
  22. package/dist/core/compile-generator.js +4 -4
  23. package/dist/core/compile.d.ts +72 -8
  24. package/dist/core/compile.js +276 -133
  25. package/dist/core/config-migrations.d.ts +69 -0
  26. package/dist/core/config-migrations.js +145 -0
  27. package/dist/core/config-schema.d.ts +166 -160
  28. package/dist/core/config-schema.js +207 -244
  29. package/dist/core/coverage.d.ts +16 -0
  30. package/dist/core/coverage.js +22 -0
  31. package/dist/core/doc-refs.d.ts +3 -2
  32. package/dist/core/doc-refs.js +2 -2
  33. package/dist/core/enforce-marker.d.ts +28 -0
  34. package/dist/core/enforce-marker.js +34 -0
  35. package/dist/core/eslint-inventory.d.ts +96 -0
  36. package/dist/core/eslint-inventory.js +251 -0
  37. package/dist/core/event-capability.d.ts +5 -1
  38. package/dist/core/frame.d.ts +43 -2
  39. package/dist/core/frame.js +85 -2
  40. package/dist/core/generate-schema.d.ts +3 -2
  41. package/dist/core/generate-schema.js +2 -2
  42. package/dist/core/generate-types.d.ts +11 -2
  43. package/dist/core/generate-types.js +12 -5
  44. package/dist/core/hook-command.d.ts +172 -0
  45. package/dist/core/hook-command.js +277 -0
  46. package/dist/core/hook-delivery.d.ts +133 -0
  47. package/dist/core/hook-delivery.js +150 -0
  48. package/dist/core/hook-paths.d.ts +14 -0
  49. package/dist/core/hook-paths.js +22 -0
  50. package/dist/core/hook-program.d.ts +4 -4
  51. package/dist/core/hook-program.js +13 -25
  52. package/dist/core/hook-state.d.ts +1 -1
  53. package/dist/core/hook-state.js +1 -1
  54. package/dist/core/instruction-remedies.d.ts +35 -0
  55. package/dist/core/instruction-remedies.js +70 -1
  56. package/dist/core/integrity-header.d.ts +54 -0
  57. package/dist/core/integrity-header.js +90 -0
  58. package/dist/core/integrity.d.ts +1 -25
  59. package/dist/core/integrity.js +16 -74
  60. package/dist/core/linter-adapter.d.ts +28 -4
  61. package/dist/core/linters.d.ts +8 -1
  62. package/dist/core/linters.js +116 -81
  63. package/dist/core/orphans.d.ts +3 -5
  64. package/dist/core/orphans.js +9 -10
  65. package/dist/core/presets.d.ts +31 -34
  66. package/dist/core/presets.js +25 -38
  67. package/dist/core/rule-catalog.d.ts +11 -12
  68. package/dist/core/rule-catalog.js +25 -94
  69. package/dist/core/rule.d.ts +213 -0
  70. package/dist/core/rule.js +37 -0
  71. package/dist/core/skill-resources.d.ts +85 -15
  72. package/dist/core/skill-resources.js +63 -37
  73. package/dist/core/source-refs.d.ts +3 -23
  74. package/dist/core/source-refs.js +3 -27
  75. package/dist/core/test-utils.d.ts +13 -0
  76. package/dist/core/test-utils.js +40 -1
  77. package/dist/core/types.d.ts +19 -329
  78. package/dist/core/validate.d.ts +14 -14
  79. package/dist/core/validate.js +87 -73
  80. package/dist/coverage-evidence.js +2 -3
  81. package/dist/deprecated-key-files.d.ts +6 -0
  82. package/dist/deprecated-key-files.js +7 -6
  83. package/dist/exclude.d.ts +3 -1
  84. package/dist/exclude.js +9 -33
  85. package/dist/harness-assert.d.ts +18 -0
  86. package/dist/harness-assert.js +38 -0
  87. package/dist/hook-check-install.js +15 -6
  88. package/dist/hook-check.d.ts +1 -3
  89. package/dist/hook-check.js +3 -5
  90. package/dist/hook-install.d.ts +7 -41
  91. package/dist/hook-install.js +29 -96
  92. package/dist/hook-runtime.js +2 -1
  93. package/dist/load-hook.d.ts +3 -2
  94. package/dist/load-hook.js +28 -6
  95. package/dist/optimize.d.ts +2 -1
  96. package/dist/overrides.d.ts +29 -0
  97. package/dist/overrides.js +51 -0
  98. package/dist/plugin-loader.d.ts +23 -0
  99. package/dist/plugin-loader.js +42 -12
  100. package/dist/root-globs.d.ts +9 -0
  101. package/dist/root-globs.js +48 -0
  102. package/dist/rule-routing.js +6 -3
  103. package/dist/rules/CLAUDE.md.spec.d.ts +3 -0
  104. package/dist/rules/CLAUDE.md.spec.js +22 -0
  105. package/dist/rules/groups.d.ts +32 -0
  106. package/dist/rules/groups.js +27 -0
  107. package/dist/rules/index.d.ts +412 -0
  108. package/dist/{core/rule-meta.js → rules/index.js} +329 -200
  109. package/dist/rules/skill-resource-resolves.d.ts +36 -0
  110. package/dist/rules/skill-resource-resolves.js +51 -0
  111. package/dist/run-hook.d.ts +27 -3
  112. package/dist/run-hook.js +48 -12
  113. package/dist/scan-core.d.ts +103 -29
  114. package/dist/scan-core.js +178 -186
  115. package/dist/scan-files.d.ts +0 -8
  116. package/dist/scan-files.js +79 -24
  117. package/dist/scan.d.ts +23 -28
  118. package/dist/scan.js +64 -18
  119. package/dist/score-core.js +3 -9
  120. package/dist/setup-plan.d.ts +13 -27
  121. package/dist/setup-plan.js +46 -71
  122. package/dist/test-coverage.d.ts +3 -8
  123. package/dist/test-coverage.js +6 -6
  124. package/dist/test.d.ts +1 -1
  125. package/dist/vigilesrc.schema.json +1347 -121
  126. package/package.json +2 -2
  127. package/packages/eslint-config/index.mjs +61 -0
  128. package/skills/adopt-spec/SKILL.md +2 -1
  129. package/dist/core/rule-meta.d.ts +0 -82
@@ -13,10 +13,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
13
13
  const spec_js_1 = require("./spec.js");
14
14
  exports.default = (0, spec_js_1.instructionFile)({
15
15
  sections: {
16
- scope: `Working in \`src/core/\`? This is the harness-AGNOSTIC domain (spec, compile, linters, the lint/audit detectors). The root \`CLAUDE.md\` holds the full positioning + rule set — read it first. Two invariants live closest to this code: the core must not import an adapter (\`core ⊄ adapter\`, eslint-enforced) and must not hard-code a Claude Code literal (read it from the injected layout/dialect). This file adds the rule for ADDING or CHANGING a detector.`,
16
+ scope: `Working in \`src/core/\`? This is the harness-AGNOSTIC domain (spec, compile, linters, the lint/audit detectors). The root \`CLAUDE.md\` holds the full positioning + rule set — read it first. Two invariants live closest to this code: the core must not import an adapter (\`core ⊄ adapter\`, eslint-enforced) and must not hard-code a Claude Code literal (read it from the injected layout/dialect). This file adds the rule for ADDING or CHANGING a detector, and where the rule around it lives.`,
17
17
  },
18
18
  rules: {
19
- "detector-meta": (0, spec_js_1.guidance)("A deterministic DETECTOR here is one half of a RULE — and a rule is not done until it is DECLARED. Three things move together (sibling of one-detector-no-drift + rules-docs-in-sync): (1) the pure detector function (shared by `lint` AND `audit`, never reimplemented per surface; read the layout/dialect, never a CC literal); (2) its entry in `src/core/rule-meta.ts` — the `Record<RuleName, RuleMeta>` won't typecheck without it — declaring its DECIDABILITY BUCKET (structural-closed = a type could prevent it / external-decidable = needs the world, error-capable / heuristic-behavioral = warn-or-measure-only), surface, defaultSeverity, the detector name, and any upstreamPrevention; (3) its `docs/rules/<name>.md` (the coverage test binds the registry to the docs by an EXACT set match, so a missing meta or doc fails CI). The bucket is the CEILING, not a preference — a heuristic proxy may NEVER default to `error` (it cries wolf); a structural/external fact MAY, once proven FP-safe. Before writing a new detector, CLASSIFY the defect into a bucket — that decides whether it can ever gate. The full model + the prose behind the buckets is the root `lint-rule-calibration` rule and `research/enforcement-model.md`."),
19
+ "detector-meta": (0, spec_js_1.guidance)("A deterministic DETECTOR here is a domain capability — a pure function shared by `lint` AND `audit`, never reimplemented per surface, reading the injected layout/dialect and never a Claude Code literal. The RULE around it — id, group, default, decidability bucket, cure, how a finding reads — is POLICY and lives in the outer rule layer: a module `src/rules/<id>.ts` (or, until it migrates in #390, its row in `src/rules/index.ts`), plus its `docs/rules/<id>.md`; `src/rules/index.test.ts` binds the set to the docs by an exact set match. Core declares the port (`./rule.ts`) and receives the set as a parameter (`configFor`, `applyRules`): never import `../rules`, never spell a rule id, never name a group — `boundaries`, `local/no-rule-ids` and `local/no-group-names` refuse each. The bucket is the CEILING, not a preference — a heuristic proxy (heuristic-behavioral) may NEVER default to `error` (it cries wolf); a structural-closed or external-decidable fact MAY, once proven FP-safe. Before writing a new detector, CLASSIFY the defect into a bucket — that decides whether it can ever gate. The model behind the buckets is the root `lint-rule-calibration` rule."),
20
20
  },
21
21
  });
22
22
  //# sourceMappingURL=CLAUDE.md.spec.js.map
@@ -0,0 +1,71 @@
1
+ /**
2
+ * ONE ANSWER TO "IS THIS RULE ON?" FOR EVERYTHING A REPORT SHOWS.
3
+ *
4
+ * `lint` asked the resolver per rule; `audit` did not ask at all, so a rule the
5
+ * repo turned off still printed in the audit (`hand-written-hook`, off by
6
+ * default, listed every hook line vigiles did not write). `applyRules` is the
7
+ * one place a scan meets the resolved config: it blanks what an OFF rule found
8
+ * and mints the {@link FilteredScan} brand that every renderer takes — so a
9
+ * renderer handed a raw scan is a tsc error, not an off rule on screen.
10
+ *
11
+ * Generic machinery: it takes the rule set, the groups and the resolver as
12
+ * parameters (`src/rules/` and `../overrides.ts` stay outside core) and names no
13
+ * rule and no group. Which scan field a not-yet-migrated rule's findings sit in
14
+ * is that rule's own declaration (`RuleSpec.legacy.scanField`); a migrated
15
+ * rule's findings come from its own `findings`.
16
+ *
17
+ * UNGRADED GROUPS. A rule in a group with `graded: false` is shown and never
18
+ * scored: its findings go to `ungraded`, one block per group, headed by the
19
+ * group's own `label`/`blurb`. A scorer takes a {@link GradedView}, which has no
20
+ * `ungraded` — so handing one the filtered scan is a tsc error, not a coin
21
+ * flip moving the grade. (A migrated rule in a GRADED group would need its
22
+ * findings to reach the scorers; none exists yet, and `src/rules/index.test.ts`
23
+ * fails the day one does, until vigiles#390 wires it.)
24
+ *
25
+ * ⚠️ SCOPE, stated: only fields that are SHOWN and never scored are blanked
26
+ * today. Blanking a graded field when its rule is off would change a repo's
27
+ * audit score — a product decision, not a refactor — so a rule whose findings
28
+ * feed a ring declares no `scanField` yet.
29
+ */
30
+ import type { Finding, RuleGroups, RuleSpec, ScanInput } from "./rule.js";
31
+ /** The part of a rule resolver this needs: whether a rule can fire anywhere. */
32
+ export interface RuleSwitch {
33
+ mayFire(rule: string): boolean;
34
+ }
35
+ /** The brand's key — module-private, so only this file can mint the brand. */
36
+ declare const FILTERED: unique symbol;
37
+ /** One rule that is on, and everything it found (possibly nothing). */
38
+ export interface RuleResult {
39
+ readonly rule: string;
40
+ readonly findings: readonly Finding[];
41
+ }
42
+ /** An ungraded group's block: its label and the rules of it that are on. */
43
+ export interface UngradedGroup {
44
+ readonly group: string;
45
+ readonly label: string;
46
+ readonly blurb?: string;
47
+ readonly rules: readonly RuleResult[];
48
+ }
49
+ /**
50
+ * A scan with the resolved config applied. Minted ONLY by {@link applyRules}
51
+ * (`local/frame-mint` refuses an `as FilteredScan` anywhere else).
52
+ */
53
+ export type FilteredScan<S extends object> = S & {
54
+ readonly [FILTERED]: true;
55
+ /** Only a renderer reads this (`no-restricted-syntax` in the scorers). */
56
+ readonly ungraded: readonly UngradedGroup[];
57
+ };
58
+ /** What a scorer takes: a scan that carries no ungraded findings. */
59
+ export type GradedView<S extends object> = S & {
60
+ readonly ungraded?: never;
61
+ };
62
+ /**
63
+ * Apply the resolved config to a scan: every rule that cannot fire anywhere
64
+ * has the scan field it declares emptied, and the ungraded groups' rules that
65
+ * are on report into `ungraded`. Pure; the scan is not mutated.
66
+ */
67
+ export declare function applyRules<S extends ScanInput>(rules: readonly RuleSpec[], groups: RuleGroups, resolver: RuleSwitch, scan: S): FilteredScan<S>;
68
+ /** The scan a scorer may read: `ungraded` dropped, everything else as is. */
69
+ export declare function gradedView<S extends ScanInput>(scan: FilteredScan<S>): GradedView<Omit<FilteredScan<S>, "ungraded">>;
70
+ export {};
71
+ //# sourceMappingURL=apply-rules.d.ts.map
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyRules = applyRules;
4
+ exports.gradedView = gradedView;
5
+ /** The brand's key — module-private, so only this file can mint the brand. */
6
+ const FILTERED = Symbol("filtered-scan");
7
+ /** `scan` with `field` emptied, when that field is a list. */
8
+ function blank(scan, field) {
9
+ return Array.isArray(Reflect.get(scan, field))
10
+ ? { ...scan, [field]: [] }
11
+ : scan;
12
+ }
13
+ /** The ungraded groups' blocks: each group that has a rule on, in order. */
14
+ function ungradedOf(rules, groups, scan) {
15
+ return Object.entries(groups)
16
+ .filter(([, g]) => !g.graded)
17
+ .map(([group, g]) => ({
18
+ group,
19
+ label: g.label,
20
+ ...(g.blurb === undefined ? {} : { blurb: g.blurb }),
21
+ rules: rules
22
+ .filter((r) => r.group === group)
23
+ .map((r) => ({ rule: r.id, findings: r.findings?.(scan) ?? [] })),
24
+ }))
25
+ .filter((g) => g.rules.length > 0);
26
+ }
27
+ /**
28
+ * Apply the resolved config to a scan: every rule that cannot fire anywhere
29
+ * has the scan field it declares emptied, and the ungraded groups' rules that
30
+ * are on report into `ungraded`. Pure; the scan is not mutated.
31
+ */
32
+ function applyRules(rules, groups, resolver, scan) {
33
+ const on = rules.filter((r) => resolver.mayFire(r.id));
34
+ const blanked = rules
35
+ .filter((r) => !on.includes(r))
36
+ .reduce((acc, r) => (r.legacy ? blank(acc, r.legacy.scanField) : acc), scan);
37
+ return {
38
+ ...blanked,
39
+ ungraded: ungradedOf(on, groups, scan),
40
+ [FILTERED]: true,
41
+ };
42
+ }
43
+ /** The scan a scorer may read: `ungraded` dropped, everything else as is. */
44
+ function gradedView(scan) {
45
+ const { ungraded: _shown, ...graded } = scan;
46
+ return graded;
47
+ }
48
+ //# sourceMappingURL=apply-rules.js.map
@@ -225,61 +225,4 @@ export declare function leafCommandsNormalized(command: string): NormalizedLeaf[
225
225
  * Parse failure → `[]`.
226
226
  */
227
227
  export declare function leafArgvSource(command: string): string[][];
228
- /**
229
- * Every word of every simple command in `command` that could name a FILE, with
230
- * inline PROGRAM TEXT removed — the primitive behind "does this hook's script
231
- * exist?".
232
- *
233
- * 🔴 WHY A FOURTH EXTRACTOR, stated against the three that already exist,
234
- * because "there is already one that returns words" is exactly the reasoning
235
- * that produced the bug this replaces.
236
- *
237
- * - `leafCommands` drops every word it cannot reduce to a LITERAL, so
238
- * `${CLAUDE_PLUGIN_ROOT}/hooks/x.sh` — the standard spelling of a hook path —
239
- * disappears entirely. Unusable for a file question.
240
- * - `leafCommandsNormalized` basenames the head, so `./hooks/x.sh` becomes
241
- * `x.sh` and no resolver can find it.
242
- * - `leafArgvSource` keeps the spelling but answers a DIFFERENT question:
243
- * "which leaves unconditionally RUN". It drops the right-hand side of `&&`
244
- * by design, so `cd "$ROOT" && node hooks/x.mjs` yields no script. For
245
- * coverage attribution that abstention is correct; for "must this file
246
- * exist?" it is a miss, because a conditionally-run script still has to be
247
- * on disk.
248
- *
249
- * So this walks EVERY simple command, keeps every word at source level, and
250
- * subtracts only the words that are provably not paths.
251
- *
252
- * 🔴 WHAT IT SUBTRACTS, and the defect that motivated it. The hook scanner used
253
- * to run a regex over the raw command STRING. Against the standard portable
254
- * plugin idiom —
255
- *
256
- * node -e "(async()=>{…await import(…join(root,'hooks','always-on.mjs'))…})()"
257
- *
258
- * — it grabbed a character run ending at `.mjs` and reported the hook's script
259
- * as `import(require(node:url).pathToFileURL(require(node:path).join(root,hooks,always-on.mjs`,
260
- * MISSING. `hooks/always-on.mjs` was 1,766 bytes on disk. Nine such findings
261
- * across the 32-repo corpus, contributing to two `F/0` grades. Inside a shell
262
- * parse the argument of `-e` is not a word the shell will ever resolve to a
263
- * file, so it is not returned.
264
- *
265
- * ⚠️ HOW MUCH OF THAT THE FLAG TABLE ACTUALLY DID, measured rather than
266
- * assumed: none of it, on that corpus. Mutating the `-e`/`-c` subtraction OFF
267
- * and re-auditing all three affected repos still yields ZERO false hook-script
268
- * findings, because both real payloads are single words that either contain
269
- * whitespace or do not end in a script extension, and the caller anchors its
270
- * match to a whole word. The table is kept because it is the difference
271
- * between a function whose contract ("words that could name a file") is true
272
- * and one whose contract is merely true-so-far: without it a JavaScript
273
- * program is handed to every caller as a candidate filename, and the next
274
- * caller inherits the bug. Recorded here so nobody reads a corpus number back
275
- * onto the wrong mechanism.
276
- *
277
- * Words beginning with `-` are dropped as flags: a flag is not a path, and the
278
- * one caller anchors its match to a whole word anyway.
279
- *
280
- * Returns `null` — not `[]` — when the text does not parse as shell, so a
281
- * caller can tell "no file operands" from "no analysis", and cannot silently
282
- * treat the second as the first.
283
- */
284
- export declare function commandWords(command: string): string[] | null;
285
228
  //# sourceMappingURL=bash-effects.d.ts.map
@@ -31,7 +31,6 @@ exports.leafCommands = leafCommands;
31
31
  exports.stripWrappers = stripWrappers;
32
32
  exports.leafCommandsNormalized = leafCommandsNormalized;
33
33
  exports.leafArgvSource = leafArgvSource;
34
- exports.commandWords = commandWords;
35
34
  /**
36
35
  * The parser is loaded ON FIRST PARSE, not at module load — `sh()` memoizes it.
37
36
  *
@@ -1164,150 +1163,4 @@ function normalizeCallExpr(node, redirs) {
1164
1163
  chdir: stripped.chdir,
1165
1164
  };
1166
1165
  }
1167
- // ===========================================================================
1168
- // FILE-OPERAND extraction (the reference question, not the effect question)
1169
- // ===========================================================================
1170
- /**
1171
- * Interpreters, and the flags after which the NEXT word is a PROGRAM rather
1172
- * than a path. `node -e "<js>"`, `python -c "<py>"`, `perl -E "<pl>"`.
1173
- *
1174
- * Keyed by the head's basename, so `/usr/local/bin/node` and `node` behave the
1175
- * same.
1176
- */
1177
- const INLINE_PROGRAM_FLAGS = new Map([
1178
- ["node", ["-e", "--eval", "-p", "--print"]],
1179
- ["nodejs", ["-e", "--eval", "-p", "--print"]],
1180
- ["bun", ["-e", "--eval", "-p", "--print"]],
1181
- ["deno", ["-e", "--eval", "-p", "--print"]],
1182
- ["python", ["-c"]],
1183
- ["python2", ["-c"]],
1184
- ["python3", ["-c"]],
1185
- ["ruby", ["-e"]],
1186
- ["perl", ["-e", "-E"]],
1187
- ["php", ["-r"]],
1188
- ]);
1189
- /**
1190
- * Shells, whose `-c` argument is a nested SHELL program. Not program text to be
1191
- * discarded — program text to be PARSED, so `bash -c 'exec "$ROOT/hooks/x.sh"'`
1192
- * still yields its script.
1193
- */
1194
- const SHELL_HEADS = new Set([
1195
- "sh",
1196
- "bash",
1197
- "zsh",
1198
- "dash",
1199
- "ksh",
1200
- "ash",
1201
- "busybox",
1202
- ]);
1203
- /** Guard against a pathological `sh -c 'sh -c "sh -c …"'` nest. */
1204
- const MAX_SHELL_NESTING = 3;
1205
- /**
1206
- * Every word of every simple command in `command` that could name a FILE, with
1207
- * inline PROGRAM TEXT removed — the primitive behind "does this hook's script
1208
- * exist?".
1209
- *
1210
- * 🔴 WHY A FOURTH EXTRACTOR, stated against the three that already exist,
1211
- * because "there is already one that returns words" is exactly the reasoning
1212
- * that produced the bug this replaces.
1213
- *
1214
- * - `leafCommands` drops every word it cannot reduce to a LITERAL, so
1215
- * `${CLAUDE_PLUGIN_ROOT}/hooks/x.sh` — the standard spelling of a hook path —
1216
- * disappears entirely. Unusable for a file question.
1217
- * - `leafCommandsNormalized` basenames the head, so `./hooks/x.sh` becomes
1218
- * `x.sh` and no resolver can find it.
1219
- * - `leafArgvSource` keeps the spelling but answers a DIFFERENT question:
1220
- * "which leaves unconditionally RUN". It drops the right-hand side of `&&`
1221
- * by design, so `cd "$ROOT" && node hooks/x.mjs` yields no script. For
1222
- * coverage attribution that abstention is correct; for "must this file
1223
- * exist?" it is a miss, because a conditionally-run script still has to be
1224
- * on disk.
1225
- *
1226
- * So this walks EVERY simple command, keeps every word at source level, and
1227
- * subtracts only the words that are provably not paths.
1228
- *
1229
- * 🔴 WHAT IT SUBTRACTS, and the defect that motivated it. The hook scanner used
1230
- * to run a regex over the raw command STRING. Against the standard portable
1231
- * plugin idiom —
1232
- *
1233
- * node -e "(async()=>{…await import(…join(root,'hooks','always-on.mjs'))…})()"
1234
- *
1235
- * — it grabbed a character run ending at `.mjs` and reported the hook's script
1236
- * as `import(require(node:url).pathToFileURL(require(node:path).join(root,hooks,always-on.mjs`,
1237
- * MISSING. `hooks/always-on.mjs` was 1,766 bytes on disk. Nine such findings
1238
- * across the 32-repo corpus, contributing to two `F/0` grades. Inside a shell
1239
- * parse the argument of `-e` is not a word the shell will ever resolve to a
1240
- * file, so it is not returned.
1241
- *
1242
- * ⚠️ HOW MUCH OF THAT THE FLAG TABLE ACTUALLY DID, measured rather than
1243
- * assumed: none of it, on that corpus. Mutating the `-e`/`-c` subtraction OFF
1244
- * and re-auditing all three affected repos still yields ZERO false hook-script
1245
- * findings, because both real payloads are single words that either contain
1246
- * whitespace or do not end in a script extension, and the caller anchors its
1247
- * match to a whole word. The table is kept because it is the difference
1248
- * between a function whose contract ("words that could name a file") is true
1249
- * and one whose contract is merely true-so-far: without it a JavaScript
1250
- * program is handed to every caller as a candidate filename, and the next
1251
- * caller inherits the bug. Recorded here so nobody reads a corpus number back
1252
- * onto the wrong mechanism.
1253
- *
1254
- * Words beginning with `-` are dropped as flags: a flag is not a path, and the
1255
- * one caller anchors its match to a whole word anyway.
1256
- *
1257
- * Returns `null` — not `[]` — when the text does not parse as shell, so a
1258
- * caller can tell "no file operands" from "no analysis", and cannot silently
1259
- * treat the second as the first.
1260
- */
1261
- function commandWords(command) {
1262
- return commandWordsAt(command, 0);
1263
- }
1264
- function commandWordsAt(command, depth) {
1265
- let file;
1266
- try {
1267
- file = sh().syntax.NewParser().Parse(command, "cmd.sh");
1268
- }
1269
- catch {
1270
- return null;
1271
- }
1272
- const out = [];
1273
- sh().syntax.Walk(file, (node) => {
1274
- if (sh().syntax.NodeType(node) === "CallExpr" && node.Args?.length)
1275
- fileOperandsOf(node.Args, depth, out);
1276
- return true;
1277
- });
1278
- return out;
1279
- }
1280
- /**
1281
- * The file-operand words of ONE simple command, appended to `out`.
1282
- *
1283
- * Wrappers are resolved through with the same table `leafArgvSource` uses (it
1284
- * keys on the BASENAME head, and wrappers only ever drop words off the FRONT,
1285
- * so a count maps the result back onto the original spellings — not a second
1286
- * copy of the rule). Flags never name a file, so they are dropped; the word
1287
- * AFTER an inline-program flag is dropped with them, and the word after a
1288
- * shell's `-c` is parsed as shell instead.
1289
- */
1290
- function fileOperandsOf(args, depth, out) {
1291
- const raw = args.map((w) => sourceParts(w.Parts) ?? "");
1292
- const probe = [normalizeHead(raw[0] ?? ""), ...raw.slice(1)];
1293
- const argv = raw.slice(probe.length - stripWrappers(probe).argv.length);
1294
- const head = normalizeHead(argv[0] ?? "");
1295
- const programFlags = INLINE_PROGRAM_FLAGS.get(head);
1296
- const nestsShell = SHELL_HEADS.has(head) && depth < MAX_SHELL_NESTING;
1297
- for (let i = 0; i < argv.length; i++) {
1298
- const w = argv[i] ?? "";
1299
- if (w === "")
1300
- continue;
1301
- if (!w.startsWith("-")) {
1302
- out.push(w);
1303
- continue;
1304
- }
1305
- if (nestsShell && w === "-c") {
1306
- out.push(...(commandWordsAt(argv[++i] ?? "", depth + 1) ?? []));
1307
- }
1308
- else if (programFlags?.includes(w)) {
1309
- i++; // the program text — not a word any shell resolves to a file
1310
- }
1311
- }
1312
- }
1313
1166
  //# sourceMappingURL=bash-effects.js.map
@@ -48,16 +48,16 @@ export interface CompileGeneratorSkillResult {
48
48
  * function* () { … })` — to a full SKILL.md (frontmatter + body + integrity
49
49
  * hash), verifying gate references. This is the CLI entry for generator skills.
50
50
  */
51
- export declare function compileGeneratorSkill(source: string, options?: {
52
- basePath?: string;
53
- specFile?: string;
51
+ export declare function compileGeneratorSkill(source: string, options: {
52
+ readonly basePath: string;
53
+ readonly specFile?: string;
54
54
  }): CompileGeneratorSkillResult;
55
55
  /**
56
56
  * Compile a generator's SOURCE text to SKILL.md markdown + verified-ref
57
57
  * errors. `frontmatter` (name/description/…) is prepended verbatim if given.
58
58
  */
59
- export declare function compileGenerator(source: string, options?: {
60
- basePath?: string;
61
- frontmatter?: string;
59
+ export declare function compileGenerator(source: string, options: {
60
+ readonly basePath: string;
61
+ readonly frontmatter?: string;
62
62
  }): CompileGeneratorResult;
63
63
  //# sourceMappingURL=compile-generator.d.ts.map
@@ -263,8 +263,8 @@ function extractMeta(obj) {
263
263
  * function* () { … })` — to a full SKILL.md (frontmatter + body + integrity
264
264
  * hash), verifying gate references. This is the CLI entry for generator skills.
265
265
  */
266
- function compileGeneratorSkill(source, options = {}) {
267
- const basePath = options.basePath ?? process.cwd();
266
+ function compileGeneratorSkill(source, options) {
267
+ const basePath = options.basePath;
268
268
  const specFile = options.specFile ?? "SKILL.md.spec.ts";
269
269
  const sf = typescript_1.default.createSourceFile("s.ts", source, typescript_1.default.ScriptTarget.Latest, true);
270
270
  const call = findGenSkillCall(sf);
@@ -309,8 +309,8 @@ function compileGeneratorSkill(source, options = {}) {
309
309
  * Compile a generator's SOURCE text to SKILL.md markdown + verified-ref
310
310
  * errors. `frontmatter` (name/description/…) is prepended verbatim if given.
311
311
  */
312
- function compileGenerator(source, options = {}) {
313
- const basePath = options.basePath ?? process.cwd();
312
+ function compileGenerator(source, options) {
313
+ const basePath = options.basePath;
314
314
  const sf = typescript_1.default.createSourceFile("skill.ts", source, typescript_1.default.ScriptTarget.Latest, true);
315
315
  const gen = findGenerator(sf);
316
316
  if (!gen?.body) {
@@ -66,6 +66,46 @@ export declare function validateCommandRef(command: string, basePath: string): C
66
66
  export declare function validateSymbolRef(file: string, name: string, basePath: string): Promise<CompileError | null>;
67
67
  export declare function validateDirRef(dirPath: string, basePath: string): CompileError | null;
68
68
  export declare function validateGlobRef(pattern: string, basePath: string): CompileError | null;
69
+ /**
70
+ * Why an `enforce()` reference was NOT verified. Separate members, not one
71
+ * "unknown", because the compile output turns each into a different instruction
72
+ * (make-the-distinction-a-type): a switch someone turned off is fixed by turning
73
+ * it on, an unset rule by setting it, an unreadable config by fixing the probe.
74
+ */
75
+ export type UnverifiedReason =
76
+ /** `enforce(…, { verify: false })` on this rule. */
77
+ "verify-false"
78
+ /** `verifyLinters: false` for the whole compile. */
79
+ | "verification-off"
80
+ /** `linterModes[<linter>] === false`. */
81
+ | "linter-off"
82
+ /** catalog-only: the rule exists, the config was not read. */
83
+ | "config-not-read"
84
+ /** the whole config was read and no block sets the rule. */
85
+ | "not-configured"
86
+ /** the rule exists; whether the config switches it on could not be read. */
87
+ | "enabled-unknown"
88
+ /** the rule does not exist (a compile error as well). */
89
+ | "not-found"
90
+ /** the config switches the rule off (a compile error as well). */
91
+ | "disabled";
92
+ /** What `compile` established about one `enforce()` reference. */
93
+ export type EnforceVerdict = {
94
+ readonly kind: "verified";
95
+ } | {
96
+ readonly kind: "unverified";
97
+ readonly reason: UnverifiedReason;
98
+ };
99
+ /** One `enforce()` reference and its verdict, in spec order. */
100
+ export interface EnforceVerdictEntry {
101
+ readonly ref: string;
102
+ readonly verdict: EnforceVerdict;
103
+ }
104
+ /**
105
+ * The sentence the compile prints for an unverified reference: what happened
106
+ * and the one thing that turns it into "Enforced by".
107
+ */
108
+ export declare function unverifiedAdvice(ref: string, reason: UnverifiedReason): string;
69
109
  export interface CompileClaudeResult {
70
110
  markdown: string;
71
111
  errors: CompileError[];
@@ -86,10 +126,28 @@ export interface CompileClaudeResult {
86
126
  */
87
127
  budgetFindings: CompileError[];
88
128
  linterResults: LinterCheckResult[];
129
+ /**
130
+ * One entry per `enforce()` rule, in spec order: rendered "**Enforced by:**"
131
+ * exactly when `verdict.kind === "verified"`, "**Claimed (unverified):**"
132
+ * otherwise. The CLI's "N linter-verified" count is the verified entries.
133
+ */
134
+ enforceVerdicts: EnforceVerdictEntry[];
89
135
  /** Estimated token count of compiled output (~4 chars/token). */
90
136
  tokens: number;
91
137
  /** All targets from the spec (for multi-target compilation). */
92
138
  targets: string[];
139
+ /**
140
+ * Each target's file: its repo-relative path (a secondary target is named
141
+ * from the primary's directory) and its markdown, rendered for THAT path so
142
+ * every link resolves from where the file sits. The first is the primary,
143
+ * whose markdown is `markdown`.
144
+ */
145
+ outputs: CompiledOutput[];
146
+ }
147
+ /** One file a multi-target compile writes. */
148
+ export interface CompiledOutput {
149
+ readonly path: string;
150
+ readonly markdown: string;
93
151
  }
94
152
  export interface CompileClaudeOptions {
95
153
  /**
@@ -107,7 +165,8 @@ export interface CompileClaudeOptions {
107
165
  maxSectionChars?: number;
108
166
  /** Character budget for the whole rendered Rules section (0 disables). */
109
167
  maxRulesChars?: number;
110
- basePath?: string;
168
+ /** The repo root every reference resolves against. Required: no cwd fallback. */
169
+ readonly basePath: string;
111
170
  specFile?: string;
112
171
  /** Injected harness dialect; its instructionTargets[0] is the default target. */
113
172
  dialect?: HarnessDialect;
@@ -166,13 +225,15 @@ export declare const DEFAULT_MAX_SECTION_CHARS = 15000;
166
225
  * budget, which leaves the other half for everything else the file carries.
167
226
  */
168
227
  export declare const DEFAULT_MAX_RULES_CHARS = 20000;
228
+ /** The compile options that decide how an `enforce()` reference is verified. */
229
+ type LinterVerifyOptions = Pick<CompileClaudeOptions, "verifyLinters" | "linterModes" | "catalogOnly" | "linters">;
169
230
  /**
170
231
  * Compile a ClaudeSpec into markdown.
171
232
  *
172
233
  * Returns the compiled markdown, validation errors, and linter check results.
173
234
  * The markdown is generated even if there are errors (with warnings).
174
235
  */
175
- export declare function compileClaude(spec: ClaudeSpec, options?: CompileClaudeOptions): Promise<CompileClaudeResult>;
236
+ export declare function compileClaude(spec: ClaudeSpec, options: Readonly<CompileClaudeOptions>): Promise<CompileClaudeResult>;
176
237
  export interface CompileSkillResult {
177
238
  /**
178
239
  * The stamped artifact — present ONLY when `errors` is empty. `null` is what
@@ -187,13 +248,14 @@ export interface CompileSkillResult {
187
248
  /**
188
249
  * Compile a SkillSpec into SKILL.md markdown with YAML frontmatter.
189
250
  */
190
- export declare function compileSkill(spec: SkillSpec, options?: {
191
- basePath?: string;
251
+ export declare function compileSkill(spec: SkillSpec, options: Readonly<{
252
+ /** The repo root every reference resolves against. Required: no cwd fallback. */
253
+ readonly basePath: string;
192
254
  specFile?: string;
193
255
  /** The harness dialect — supplies `skillFrontmatterKeys`. Omitting it emits
194
256
  * every key the compiler can render, so existing callers are unchanged. */
195
257
  dialect?: HarnessDialect;
196
- }): Promise<CompileSkillResult>;
258
+ }>): Promise<CompileSkillResult>;
197
259
  export interface CompileAgentResult {
198
260
  /**
199
261
  * The stamped artifact — present ONLY when `errors` is empty. `null` is what
@@ -210,13 +272,14 @@ export interface CompileAgentResult {
210
272
  * Verifies the tool contract and the body's references; the marks the body
211
273
  * carries (`vigiles:symbol`, file/cmd refs) are the same ones `lint` re-checks.
212
274
  */
213
- export declare function compileAgent(spec: AgentSpec, options: {
214
- basePath?: string;
275
+ export declare function compileAgent(spec: AgentSpec, options: Readonly<{
276
+ /** The repo root every reference resolves against. Required: no cwd fallback. */
277
+ basePath: string;
215
278
  specFile?: string;
216
279
  /** The harness dialect to verify the tool contract against (required — the
217
280
  * core defines no default dialect; the adapter/composition root injects it). */
218
281
  dialect: HarnessDialect;
219
- }): Promise<CompileAgentResult>;
282
+ } & LinterVerifyOptions>): Promise<CompileAgentResult>;
220
283
  export interface CompileRailwayOptions {
221
284
  /** Names of compiled agents, to resolve `delegate` targets. Skipped if omitted. */
222
285
  knownAgents?: readonly string[];
@@ -261,4 +324,5 @@ export interface AdoptResult {
261
324
  * Returns the diff so users can see what was manually changed.
262
325
  */
263
326
  export declare function adoptDiff(filePath: string, spec: ClaudeSpec | SkillSpec | AgentSpec, basePath: string, dialect: HarnessDialect): Promise<AdoptResult>;
327
+ export {};
264
328
  //# sourceMappingURL=compile.d.ts.map