@monte3l/groundwork 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +23 -0
  2. package/bin/m3l-groundwork.mjs +10 -0
  3. package/dist/assets.d.ts +20 -0
  4. package/dist/assets.js +79 -0
  5. package/dist/caps.d.ts +25 -0
  6. package/dist/caps.js +69 -0
  7. package/dist/conflicts.d.ts +12 -0
  8. package/dist/conflicts.js +77 -0
  9. package/dist/emit.d.ts +7 -0
  10. package/dist/emit.js +42 -0
  11. package/dist/git.d.ts +3 -0
  12. package/dist/git.js +9 -0
  13. package/dist/harness/conformance.d.ts +20 -0
  14. package/dist/harness/conformance.js +18 -0
  15. package/dist/harness/frontmatter.d.ts +38 -0
  16. package/dist/harness/frontmatter.js +204 -0
  17. package/dist/harness/grade.d.ts +4 -0
  18. package/dist/harness/grade.js +105 -0
  19. package/dist/harness/rules.d.ts +55 -0
  20. package/dist/harness/rules.js +580 -0
  21. package/dist/harness/types.d.ts +32 -0
  22. package/dist/harness/types.js +9 -0
  23. package/dist/inventory.d.ts +63 -0
  24. package/dist/inventory.js +66 -0
  25. package/dist/jsonc.d.ts +14 -0
  26. package/dist/jsonc.js +83 -0
  27. package/dist/main.d.ts +24 -0
  28. package/dist/main.js +297 -0
  29. package/dist/merge-json.d.ts +74 -0
  30. package/dist/merge-json.js +135 -0
  31. package/dist/mode.d.ts +19 -0
  32. package/dist/mode.js +53 -0
  33. package/dist/packs.d.ts +61 -0
  34. package/dist/packs.js +186 -0
  35. package/dist/plugin.d.ts +23 -0
  36. package/dist/plugin.js +79 -0
  37. package/dist/report.d.ts +4 -0
  38. package/dist/report.js +323 -0
  39. package/dist/survey/fs-walk.d.ts +14 -0
  40. package/dist/survey/fs-walk.js +60 -0
  41. package/dist/survey/survey-docs.d.ts +4 -0
  42. package/dist/survey/survey-docs.js +69 -0
  43. package/dist/survey/survey-harness.d.ts +4 -0
  44. package/dist/survey/survey-harness.js +121 -0
  45. package/dist/survey/survey-shape.d.ts +4 -0
  46. package/dist/survey/survey-shape.js +182 -0
  47. package/dist/survey/survey-toolchain.d.ts +4 -0
  48. package/dist/survey/survey-toolchain.js +217 -0
  49. package/dist/survey/survey.d.ts +5 -0
  50. package/dist/survey/survey.js +21 -0
  51. package/dist/survey/types.d.ts +117 -0
  52. package/dist/survey/types.js +8 -0
  53. package/dist/tokens.d.ts +13 -0
  54. package/dist/tokens.js +13 -0
  55. package/dist/toolchain/conformance.d.ts +20 -0
  56. package/dist/toolchain/conformance.js +30 -0
  57. package/dist/toolchain/grade.d.ts +4 -0
  58. package/dist/toolchain/grade.js +244 -0
  59. package/dist/toolchain/rules.d.ts +118 -0
  60. package/dist/toolchain/rules.js +706 -0
  61. package/dist/toolchain/tsconfig-chain.d.ts +36 -0
  62. package/dist/toolchain/tsconfig-chain.js +116 -0
  63. package/dist/toolchain/types.d.ts +27 -0
  64. package/dist/toolchain/types.js +9 -0
  65. package/package.json +59 -0
  66. package/plugin/skills/customize/SKILL.md +305 -0
  67. package/plugin/src/domain-map.ts +134 -0
  68. package/plugin/src/index.ts +4 -0
  69. package/plugin/src/kind-facet-map.ts +174 -0
  70. package/plugin/src/pack-map.ts +65 -0
  71. package/templates/core/.claude/agents/Explore.md +43 -0
  72. package/templates/core/.claude/agents/code-implementer.md +258 -0
  73. package/templates/core/.claude/agents/code-reviewer.md +163 -0
  74. package/templates/core/.claude/agents/silent-failure-hunter.md +191 -0
  75. package/templates/core/.claude/agents/test-author.md +211 -0
  76. package/templates/core/.claude/hooks/guard-branch-isolation.mjs +123 -0
  77. package/templates/core/.claude/hooks/guard-double-background.mjs +113 -0
  78. package/templates/core/.claude/hooks/guard-git-push-signed.mjs +90 -0
  79. package/templates/core/.claude/hooks/guard-hub-src-writes.mjs +88 -0
  80. package/templates/core/.claude/hooks/guard-js-extension.mjs +66 -0
  81. package/templates/core/.claude/hooks/guard-no-commonjs.mjs +105 -0
  82. package/templates/core/.claude/hooks/guard-protected-paths.mjs +45 -0
  83. package/templates/core/.claude/hooks/guard-secret-writes.mjs +183 -0
  84. package/templates/core/.claude/hooks/inject-decision-gate.mjs +119 -0
  85. package/templates/core/.claude/hooks/post-edit-verify.mjs +150 -0
  86. package/templates/core/.claude/rules/agent-dispatch.md +121 -0
  87. package/templates/core/.claude/rules/refactoring.md +52 -0
  88. package/templates/core/.claude/rules/src.md +114 -0
  89. package/templates/core/.claude/rules/tests.md +129 -0
  90. package/templates/core/.claude/settings.json +111 -0
  91. package/templates/core/.claude/skills/creating-prs/SKILL.md +132 -0
  92. package/templates/core/.claude/skills/finishing-work/SKILL.md +117 -0
  93. package/templates/core/.claude/skills/harness-guidance/SKILL.md +140 -0
  94. package/templates/core/.claude/skills/harness-guidance/references/official-sources.md +58 -0
  95. package/templates/core/.claude/skills/starting-work/SKILL.md +94 -0
  96. package/templates/core/.claude/skills/triaging-ci/SKILL.md +111 -0
  97. package/templates/core/.claude/skills/typescript-guidance/SKILL.md +143 -0
  98. package/templates/core/.claude/skills/typescript-guidance/references/typescript-sources.md +102 -0
  99. package/templates/core/.claude/skills/writing-commits/SKILL.md +248 -0
  100. package/templates/core/.github/workflows/ci.yml +123 -0
  101. package/templates/core/.github/workflows/dependency-review.yml +26 -0
  102. package/templates/core/.github/workflows/security-audit.yml +54 -0
  103. package/templates/core/.node-version +1 -0
  104. package/templates/core/.prettierignore +5 -0
  105. package/templates/core/.prettierrc.json +4 -0
  106. package/templates/core/CLAUDE.md +127 -0
  107. package/templates/core/README.md +24 -0
  108. package/templates/core/_gitignore +19 -0
  109. package/templates/core/_npmrc +1 -0
  110. package/templates/core/bin/check-exports.mjs +92 -0
  111. package/templates/core/bin/check-harness.mjs +27 -0
  112. package/templates/core/bin/check-node-version.mjs +51 -0
  113. package/templates/core/bin/check-toolchain.mjs +20 -0
  114. package/templates/core/bin/lib/agent-roster.mjs +8 -0
  115. package/templates/core/bin/lib/frontmatter.mjs +210 -0
  116. package/templates/core/bin/lib/harness-rules.mjs +916 -0
  117. package/templates/core/bin/lib/protected-paths.mjs +23 -0
  118. package/templates/core/bin/lib/report.mjs +56 -0
  119. package/templates/core/bin/lib/signed-range.mjs +178 -0
  120. package/templates/core/bin/lib/toolchain-rules.mjs +1264 -0
  121. package/templates/core/bin/lib/verify-steps.mjs +131 -0
  122. package/templates/core/bin/lib/verify-steps.packs.json +1 -0
  123. package/templates/core/bin/lint-commit.mjs +50 -0
  124. package/templates/core/bin/strip-claude-trailers.mjs +25 -0
  125. package/templates/core/bin/verify.mjs +64 -0
  126. package/templates/core/commitlint.config.js +11 -0
  127. package/templates/core/docs/research/harness-refresh.md +27 -0
  128. package/templates/core/docs/research/typescript-refresh.md +32 -0
  129. package/templates/core/eslint.config.js +105 -0
  130. package/templates/core/knip.json +6 -0
  131. package/templates/core/lefthook.yml +39 -0
  132. package/templates/core/package.json +58 -0
  133. package/templates/core/pnpm-workspace.yaml +13 -0
  134. package/templates/core/src/index.ts +12 -0
  135. package/templates/core/tests/index.test.ts +8 -0
  136. package/templates/core/tsconfig.base.json +36 -0
  137. package/templates/core/tsconfig.build.json +10 -0
  138. package/templates/core/tsconfig.json +11 -0
  139. package/templates/core/vitest.config.ts +32 -0
  140. package/templates/packs/README.md +81 -0
  141. package/templates/packs/harness-extras/files/.claude/agents/type-design-analyzer.md +188 -0
  142. package/templates/packs/harness-extras/files/.claude/hooks/guard-readonly-bash.mjs +324 -0
  143. package/templates/packs/harness-extras/files/.claude/hooks/reinject-compact-handoff.mjs +197 -0
  144. package/templates/packs/harness-extras/files/.claude/hooks/write-compact-handoff.mjs +180 -0
  145. package/templates/packs/harness-extras/files/bin/check-file-budget.mjs +407 -0
  146. package/templates/packs/harness-extras/files/bin/file-budget-baseline.json +1 -0
  147. package/templates/packs/harness-extras/pack.json +65 -0
  148. package/templates/packs/statusline/files/.claude/hooks/statusline-layout.mjs +365 -0
  149. package/templates/packs/statusline/files/.claude/hooks/statusline.mjs +996 -0
  150. package/templates/packs/statusline/files/.claude/hooks/subagent-statusline.mjs +203 -0
  151. package/templates/packs/statusline/pack.json +31 -0
@@ -0,0 +1,706 @@
1
+ const NONE = { checked: 0, failures: [] };
2
+ const lower = (value) => typeof value === "string" ? value.toLowerCase() : undefined;
3
+ /**
4
+ * Options TypeScript 6.0 deprecated. `removedIn` is the major that drops them
5
+ * (`outFile` and `moduleResolution: classic` already went in 6.0).
6
+ * https://www.typescriptlang.org/docs/handbook/release-notes/typescript-6-0.html
7
+ */
8
+ export const LEGACY_OPTIONS = [
9
+ {
10
+ option: "baseUrl",
11
+ label: "baseUrl",
12
+ deprecatedIn: 6,
13
+ removedIn: 7,
14
+ matches: (value) => value !== undefined,
15
+ },
16
+ {
17
+ option: "outFile",
18
+ label: "outFile",
19
+ deprecatedIn: 6,
20
+ removedIn: 6,
21
+ matches: (value) => value !== undefined,
22
+ },
23
+ {
24
+ option: "moduleResolution",
25
+ label: "moduleResolution: node",
26
+ deprecatedIn: 6,
27
+ removedIn: 7,
28
+ matches: (value) => ["node", "node10"].includes(lower(value) ?? ""),
29
+ },
30
+ {
31
+ option: "moduleResolution",
32
+ label: "moduleResolution: classic",
33
+ deprecatedIn: 6,
34
+ removedIn: 6,
35
+ matches: (value) => lower(value) === "classic",
36
+ },
37
+ {
38
+ option: "target",
39
+ label: "target: es5",
40
+ deprecatedIn: 6,
41
+ removedIn: 7,
42
+ matches: (value) => lower(value) === "es5",
43
+ },
44
+ {
45
+ option: "downlevelIteration",
46
+ label: "downlevelIteration",
47
+ deprecatedIn: 6,
48
+ removedIn: 7,
49
+ matches: (value) => value === true,
50
+ },
51
+ {
52
+ option: "module",
53
+ label: "module: amd|umd|system|none",
54
+ deprecatedIn: 6,
55
+ removedIn: 7,
56
+ matches: (value) => ["amd", "umd", "system", "systemjs", "none"].includes(lower(value) ?? ""),
57
+ },
58
+ {
59
+ option: "esModuleInterop",
60
+ label: "esModuleInterop: false",
61
+ deprecatedIn: 6,
62
+ removedIn: 7,
63
+ matches: (value) => value === false,
64
+ },
65
+ {
66
+ option: "allowSyntheticDefaultImports",
67
+ label: "allowSyntheticDefaultImports: false",
68
+ deprecatedIn: 6,
69
+ removedIn: 7,
70
+ matches: (value) => value === false,
71
+ },
72
+ {
73
+ option: "alwaysStrict",
74
+ label: "alwaysStrict: false",
75
+ deprecatedIn: 6,
76
+ removedIn: 7,
77
+ matches: (value) => value === false,
78
+ },
79
+ ];
80
+ /**
81
+ * The strict-family flags the baseline enables, plus `skipLibCheck`. Three
82
+ * omissions are deliberate and must not be "completed": `isolatedDeclarations`
83
+ * (the build project sets it alone; the tooling project includes tests/, which
84
+ * it does not tolerate), `noUnusedLocals`/`noUnusedParameters`
85
+ * (`@typescript-eslint/no-unused-vars` covers both with an `^_` escape hatch
86
+ * tsc's flags lack), and `forceConsistentCasingInFileNames` (defaults on).
87
+ * `allowUnreachableCode` is graded separately -- it must be `false`.
88
+ */
89
+ export const STRICT_FLAGS = [
90
+ "strict",
91
+ "noUncheckedIndexedAccess",
92
+ "noImplicitOverride",
93
+ "exactOptionalPropertyTypes",
94
+ "verbatimModuleSyntax",
95
+ "isolatedModules",
96
+ "noFallthroughCasesInSwitch",
97
+ "noImplicitReturns",
98
+ "noPropertyAccessFromIndexSignature",
99
+ "noUncheckedSideEffectImports",
100
+ "skipLibCheck",
101
+ ];
102
+ const MODERN_MODULES = new Set([
103
+ "nodenext",
104
+ "node16",
105
+ "node18",
106
+ "node20",
107
+ "preserve",
108
+ ]);
109
+ const MODERN_RESOLUTIONS = new Set(["nodenext", "node16", "bundler"]);
110
+ const MIN_TARGET_YEAR = 2022;
111
+ const PIN_PACKAGES = [
112
+ "typescript",
113
+ "eslint",
114
+ "typescript-eslint",
115
+ "@types/node",
116
+ ];
117
+ const PNPM_BUILTINS = new Set([
118
+ "exec",
119
+ "dlx",
120
+ "install",
121
+ "i",
122
+ "add",
123
+ "remove",
124
+ "update",
125
+ "audit",
126
+ "pack",
127
+ "publish",
128
+ "store",
129
+ "config",
130
+ "env",
131
+ "create",
132
+ ]);
133
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
134
+ // --- helpers shared by rules -----------------------------------------------
135
+ /** The first number in a range: `^6.0.3` gives 6. `undefined` when there is none (`workspace:*`, `catalog:`). */
136
+ function majorOf(spec) {
137
+ const match = /(\d+)/.exec(spec ?? "");
138
+ return match?.[1] === undefined ? undefined : Number(match[1]);
139
+ }
140
+ function depSpec(snapshot, name) {
141
+ const pkg = snapshot.packageJson;
142
+ if (pkg === undefined)
143
+ return undefined;
144
+ for (const key of ["devDependencies", "dependencies"]) {
145
+ const section = pkg[key];
146
+ if (isRecord(section)) {
147
+ const spec = section[name];
148
+ if (typeof spec === "string")
149
+ return spec;
150
+ }
151
+ }
152
+ return undefined;
153
+ }
154
+ const isUnpinned = (spec) => /^\s*(\*|x|latest|next)?\s*$/i.test(spec);
155
+ /**
156
+ * The chain a project's strict-flag judgement is made over: `tsconfig.json`,
157
+ * else the first chain no other chain extends (a leaf, not a base), else the
158
+ * first.
159
+ */
160
+ function primaryChain(snapshot) {
161
+ const named = snapshot.chains.find((chain) => chain.entry === "tsconfig.json");
162
+ if (named !== undefined)
163
+ return named;
164
+ const leaf = snapshot.chains.find((chain) => !snapshot.chains.some((other) => other !== chain && other.files.some((f) => f.rel === chain.entry)));
165
+ return leaf ?? snapshot.chains[0];
166
+ }
167
+ function ownFiles(snapshot) {
168
+ return [...snapshot.tsconfigFiles.values()].filter((file) => file.error === undefined);
169
+ }
170
+ /** The first year an ES target denotes; `Infinity` for `esnext`; `undefined` when unrecognised. */
171
+ function targetYear(target) {
172
+ const value = lower(target);
173
+ if (value === "esnext")
174
+ return Infinity;
175
+ const match = /^es(\d+)$/.exec(value ?? "");
176
+ if (match?.[1] === undefined)
177
+ return undefined;
178
+ const n = Number(match[1]);
179
+ return n < 100 ? n + 2009 : n;
180
+ }
181
+ /** The `.json` file a `tsc -b|-p <file>` script names, if it names one. */
182
+ function scriptTsconfig(script) {
183
+ if (script === undefined)
184
+ return undefined;
185
+ const match = /\btsc\b[^&|;\n]*?\s(?:-b|--build|-p|--project)\s+(?:--\S+\s+)*([^\s&|;]+\.json)/.exec(script);
186
+ return match?.[1]?.replace(/^\.\//, "");
187
+ }
188
+ const gateLaneParity = {
189
+ id: "gate-lane-parity",
190
+ level: "structural",
191
+ category: "gates",
192
+ check: (s) => {
193
+ const { groups } = s.gates;
194
+ if (groups.length === 0)
195
+ return NONE;
196
+ const failures = [];
197
+ let checked = 0;
198
+ for (const lane of s.lanes) {
199
+ // A lane that runs verify.mjs with no static group (a matrix, or a bare
200
+ // full run) cannot be judged by reading it.
201
+ if (lane.dynamic)
202
+ continue;
203
+ for (const group of groups) {
204
+ checked += 1;
205
+ if (!lane.groups.includes(group)) {
206
+ failures.push({
207
+ subject: lane.surface,
208
+ message: `has no \`verify.mjs --group ${group}\` invocation, so that group's steps are never gated here`,
209
+ });
210
+ }
211
+ }
212
+ for (const named of new Set(lane.groups)) {
213
+ if (groups.includes(named))
214
+ continue;
215
+ checked += 1;
216
+ failures.push({
217
+ subject: lane.surface,
218
+ message: `runs \`verify.mjs --group ${named}\`, but ${named} is not in GROUPS (${groups.join(", ")})`,
219
+ });
220
+ }
221
+ for (const step of new Set(lane.steps)) {
222
+ checked += 1;
223
+ failures.push({
224
+ subject: lane.surface,
225
+ message: `runs \`verify.mjs --step ${step}\` by id; name a group instead so a new step joins this lane without editing it`,
226
+ });
227
+ }
228
+ }
229
+ return { checked, failures };
230
+ },
231
+ };
232
+ /** The lower and upper major an `engines.node` range implies; `undefined` for `||` ranges. */
233
+ function enginesBounds(range) {
234
+ if (range.includes("||"))
235
+ return undefined;
236
+ const low = /(?:^|\s)(?:>=|\^|~|=)?\s*v?(\d+)/.exec(range);
237
+ const high = /<\s*(\d+)/.exec(range);
238
+ return {
239
+ min: low?.[1] === undefined ? undefined : Number(low[1]),
240
+ max: high?.[1] === undefined ? undefined : Number(high[1]),
241
+ };
242
+ }
243
+ // --- structural rules ------------------------------------------------------
244
+ const tsconfigParses = {
245
+ id: "tsconfig-parses",
246
+ level: "structural",
247
+ category: "tsconfig",
248
+ check: (s) => ({
249
+ checked: s.tsconfigFiles.size,
250
+ failures: [...s.tsconfigFiles.values()]
251
+ .filter((file) => file.error !== undefined)
252
+ .map((file) => ({
253
+ subject: file.rel,
254
+ message: `does not parse as JSONC: ${file.error ?? ""}`,
255
+ })),
256
+ }),
257
+ };
258
+ const tsconfigExtendsResolves = {
259
+ id: "tsconfig-extends-resolves",
260
+ level: "structural",
261
+ category: "tsconfig",
262
+ check: (s) => {
263
+ // A bare specifier that resolves nowhere is not a failure: before
264
+ // `pnpm install` there is no node_modules to look in.
265
+ const judged = s.tsconfigLinks.filter((link) => link.kind === "relative" || link.resolved);
266
+ return {
267
+ checked: judged.length,
268
+ failures: judged
269
+ .filter((link) => !link.resolved)
270
+ .map((link) => ({
271
+ subject: link.from,
272
+ message: `extends "${link.specifier}", which resolves to no file`,
273
+ })),
274
+ };
275
+ },
276
+ };
277
+ const tsconfigEmitCoherence = {
278
+ id: "tsconfig-emit-coherence",
279
+ level: "structural",
280
+ category: "tsconfig",
281
+ check: (s) => {
282
+ const buildName = scriptTsconfig(s.scripts["build"]);
283
+ const build = s.chains.find((chain) => chain.entry === buildName);
284
+ if (build === undefined || !build.complete)
285
+ return NONE;
286
+ const failures = [];
287
+ let checked = 2;
288
+ if (build.options["outDir"] === undefined) {
289
+ failures.push({
290
+ subject: build.entry,
291
+ message: "is compiled by the `build` script but sets no outDir, so tsc emits .js next to the sources",
292
+ });
293
+ }
294
+ if (build.options["noEmit"] === true) {
295
+ failures.push({
296
+ subject: build.entry,
297
+ message: "is compiled by the `build` script but sets noEmit: true, so `build` emits nothing",
298
+ });
299
+ }
300
+ const tooling = s.chains.find((chain) => chain.entry === "tsconfig.json");
301
+ if (tooling !== undefined && tooling.complete && tooling !== build) {
302
+ checked += 1;
303
+ if (tooling.options["noEmit"] !== true) {
304
+ failures.push({
305
+ subject: tooling.entry,
306
+ message: `sits beside ${build.entry} as the tooling project but does not set noEmit: true, so typecheck would emit`,
307
+ });
308
+ }
309
+ }
310
+ return { checked, failures };
311
+ },
312
+ };
313
+ const gateWiring = {
314
+ id: "gate-wiring",
315
+ level: "structural",
316
+ category: "gates",
317
+ check: (s) => {
318
+ const { stepsFile, groups, steps, packs } = s.gates;
319
+ const failures = [];
320
+ let checked = 0;
321
+ const scripts = s.packageJson === undefined ? undefined : s.scripts;
322
+ for (const step of steps) {
323
+ const [tool, first, second] = step.cmd;
324
+ if (tool === "pnpm" && scripts !== undefined && first !== undefined) {
325
+ const script = first === "run" ? second : first;
326
+ if (script !== undefined && !PNPM_BUILTINS.has(script)) {
327
+ checked += 1;
328
+ if (scripts[script] === undefined) {
329
+ failures.push({
330
+ subject: `${stepsFile ?? ""} step "${step.id}"`,
331
+ message: `runs \`pnpm ${script}\`, but package.json has no "${script}" script`,
332
+ });
333
+ }
334
+ }
335
+ }
336
+ if (tool === "node" && first !== undefined && !first.startsWith("-")) {
337
+ checked += 1;
338
+ if (!s.projectFiles.has(first)) {
339
+ failures.push({
340
+ subject: `${stepsFile ?? ""} step "${step.id}"`,
341
+ message: `runs \`node ${first}\`, which does not exist`,
342
+ });
343
+ }
344
+ }
345
+ if (groups.length > 0 && step.group !== undefined) {
346
+ checked += 1;
347
+ if (!groups.includes(step.group)) {
348
+ failures.push({
349
+ subject: `${stepsFile ?? ""} step "${step.id}"`,
350
+ message: `names group "${step.group}", which is not in GROUPS (${groups.join(", ")})`,
351
+ });
352
+ }
353
+ }
354
+ }
355
+ if (steps.length > 0) {
356
+ for (const group of groups) {
357
+ checked += 1;
358
+ if (!steps.some((step) => step.group === group)) {
359
+ failures.push({
360
+ subject: `${stepsFile ?? ""} group "${group}"`,
361
+ message: "has no step, so its lefthook lane and CI job gate nothing",
362
+ });
363
+ }
364
+ }
365
+ }
366
+ if (packs !== undefined) {
367
+ checked += 1;
368
+ if (packs.error !== undefined) {
369
+ failures.push({
370
+ subject: packs.path,
371
+ message: `does not parse as JSON: ${packs.error}`,
372
+ });
373
+ }
374
+ }
375
+ return { checked, failures };
376
+ },
377
+ };
378
+ const nodePinCoherence = {
379
+ id: "node-pin-coherence",
380
+ level: "structural",
381
+ category: "gates",
382
+ check: (s) => {
383
+ const pkg = s.packageJson;
384
+ const engines = pkg !== undefined && isRecord(pkg["engines"])
385
+ ? pkg["engines"]["node"]
386
+ : undefined;
387
+ if (s.nodeVersion === undefined || typeof engines !== "string")
388
+ return NONE;
389
+ const pin = /^v?(\d+)/.exec(s.nodeVersion);
390
+ const bounds = enginesBounds(engines);
391
+ if (pin?.[1] === undefined || bounds === undefined)
392
+ return NONE;
393
+ const major = Number(pin[1]);
394
+ const failures = [];
395
+ if (bounds.min !== undefined && major < bounds.min) {
396
+ failures.push({
397
+ subject: ".node-version",
398
+ message: `pins Node ${major}, below the ${bounds.min} that package.json engines.node ("${engines}") requires`,
399
+ });
400
+ }
401
+ if (bounds.max !== undefined && major >= bounds.max) {
402
+ failures.push({
403
+ subject: ".node-version",
404
+ message: `pins Node ${major}, at or above the ${bounds.max} that package.json engines.node ("${engines}") excludes`,
405
+ });
406
+ }
407
+ return { checked: 1, failures };
408
+ },
409
+ };
410
+ // --- rubric rules ----------------------------------------------------------
411
+ const strictFlags = {
412
+ id: "strict-flags",
413
+ level: "rubric",
414
+ category: "tsconfig",
415
+ check: (s) => {
416
+ const chain = primaryChain(s);
417
+ if (chain === undefined || !chain.complete)
418
+ return NONE;
419
+ const failures = [];
420
+ const expectValue = (flag, wanted) => {
421
+ const value = chain.options[flag];
422
+ if (value === wanted)
423
+ return;
424
+ failures.push({
425
+ subject: chain.entry,
426
+ message: value === undefined
427
+ ? `${flag} is not set (want ${wanted})`
428
+ : `${flag} is ${JSON.stringify(value)} (want ${wanted})`,
429
+ });
430
+ };
431
+ for (const flag of STRICT_FLAGS)
432
+ expectValue(flag, true);
433
+ expectValue("allowUnreachableCode", false);
434
+ return { checked: STRICT_FLAGS.length + 1, failures };
435
+ },
436
+ };
437
+ const tsconfigOptionLifecycle = {
438
+ id: "tsconfig-option-lifecycle",
439
+ level: "rubric",
440
+ category: "tsconfig",
441
+ check: (s) => {
442
+ const major = majorOf(depSpec(s, "typescript"));
443
+ const files = ownFiles(s);
444
+ const failures = [];
445
+ for (const file of files) {
446
+ const hits = [];
447
+ if (major !== undefined) {
448
+ for (const row of LEGACY_OPTIONS) {
449
+ if (!row.matches(file.options[row.option]))
450
+ continue;
451
+ if (major >= row.removedIn)
452
+ hits.push(`${row.label} (removed)`);
453
+ else if (major >= row.deprecatedIn)
454
+ hits.push(`${row.label} (deprecated)`);
455
+ else if (major + 1 >= row.removedIn) {
456
+ hits.push(`${row.label} (removed in TypeScript ${row.removedIn})`);
457
+ }
458
+ }
459
+ }
460
+ if (file.options["ignoreDeprecations"] !== undefined) {
461
+ hits.push("ignoreDeprecations (a migration aid, not a long-term setting)");
462
+ }
463
+ if (hits.length > 0) {
464
+ failures.push({
465
+ subject: file.rel,
466
+ message: `sets ${hits.join(", ")}`,
467
+ });
468
+ }
469
+ }
470
+ return { checked: files.length, failures };
471
+ },
472
+ };
473
+ const moduleTargetModern = {
474
+ id: "module-target-modern",
475
+ level: "rubric",
476
+ category: "modules",
477
+ check: (s) => {
478
+ const chain = primaryChain(s);
479
+ if (chain === undefined || !chain.complete)
480
+ return NONE;
481
+ const module = chain.options["module"];
482
+ const moduleResolution = chain.options["moduleResolution"];
483
+ const target = chain.options["target"];
484
+ const failures = [];
485
+ let checked = 0;
486
+ if (module !== undefined) {
487
+ checked += 1;
488
+ if (!MODERN_MODULES.has(lower(module) ?? "")) {
489
+ failures.push({
490
+ subject: chain.entry,
491
+ message: `module is ${JSON.stringify(module)}; use nodenext (Node) or preserve (bundler)`,
492
+ });
493
+ }
494
+ }
495
+ if (moduleResolution !== undefined) {
496
+ checked += 1;
497
+ if (!MODERN_RESOLUTIONS.has(lower(moduleResolution) ?? "")) {
498
+ failures.push({
499
+ subject: chain.entry,
500
+ message: `moduleResolution is ${JSON.stringify(moduleResolution)}; use nodenext or bundler`,
501
+ });
502
+ }
503
+ }
504
+ const year = targetYear(target);
505
+ if (year !== undefined) {
506
+ checked += 1;
507
+ if (year < MIN_TARGET_YEAR) {
508
+ failures.push({
509
+ subject: chain.entry,
510
+ message: `target is ${JSON.stringify(target)}; es2022 or later is the floor`,
511
+ });
512
+ }
513
+ }
514
+ return { checked, failures };
515
+ },
516
+ };
517
+ const eslintFlatConfig = {
518
+ id: "eslint-flat-config",
519
+ level: "rubric",
520
+ category: "eslint",
521
+ check: (s) => {
522
+ const { flatFile, legacy } = s.eslint;
523
+ if (flatFile !== undefined)
524
+ return { checked: 1, failures: [] };
525
+ if (legacy.length === 0)
526
+ return NONE;
527
+ return {
528
+ checked: 1,
529
+ failures: legacy.map((name) => ({
530
+ subject: name,
531
+ message: "is a legacy eslintrc config; ESLint 10 reads only eslint.config.*",
532
+ })),
533
+ };
534
+ },
535
+ };
536
+ const eslintTypedLinting = {
537
+ id: "eslint-typed-linting",
538
+ level: "rubric",
539
+ category: "eslint",
540
+ check: (s) => {
541
+ const { flatFile, source } = s.eslint;
542
+ if (flatFile === undefined || source === undefined)
543
+ return NONE;
544
+ const direct = /\bfrom\s+["']typescript-eslint["']|require\(\s*["']typescript-eslint["']\s*\)/.test(source);
545
+ if (!direct) {
546
+ // A preset composed through a shared config package cannot be judged
547
+ // from this file alone -- say nothing rather than claim it is untyped.
548
+ if (/["'](?:@[^/"']+\/)?eslint-config[^"']*["']/.test(source))
549
+ return NONE;
550
+ return {
551
+ checked: 1,
552
+ failures: [
553
+ {
554
+ subject: flatFile,
555
+ message: "does not use typescript-eslint, so no TypeScript-aware rules run",
556
+ },
557
+ ],
558
+ };
559
+ }
560
+ const failures = [];
561
+ if (!/\b(?:recommended|strict)TypeChecked\b/.test(source)) {
562
+ failures.push({
563
+ subject: flatFile,
564
+ message: "uses typescript-eslint without a type-checked preset (recommendedTypeChecked or strictTypeChecked)",
565
+ });
566
+ }
567
+ if (!/\bprojectService\s*:/.test(source)) {
568
+ failures.push({
569
+ subject: flatFile,
570
+ message: "does not set parserOptions.projectService, so type-aware rules cannot read type information",
571
+ });
572
+ }
573
+ return { checked: 2, failures };
574
+ },
575
+ };
576
+ const eslintCoversEmittedCode = {
577
+ id: "eslint-covers-emitted-code",
578
+ level: "rubric",
579
+ category: "eslint",
580
+ check: (s) => {
581
+ const { flatFile, source } = s.eslint;
582
+ if (flatFile === undefined || source === undefined)
583
+ return NONE;
584
+ const failures = [];
585
+ let checked = 1;
586
+ const ignored = [...source.matchAll(/\bignores\s*:\s*\[([\s\S]*?)\]/g)]
587
+ .flatMap((block) => [...(block[1] ?? "").matchAll(/["']([^"']+)["']/g)])
588
+ .map((literal) => literal[1] ?? "");
589
+ for (const glob of ignored.filter((g) => /^(\*\*\/)?src(\/|$)/.test(g))) {
590
+ failures.push({
591
+ subject: flatFile,
592
+ message: `ignores "${glob}", so the project's own source is never linted`,
593
+ });
594
+ }
595
+ const files = [...s.projectFiles];
596
+ const scopes = [
597
+ ["bin/", /\.(?:mjs|js)$/, /["']bin\/[^"']*["']/],
598
+ [".claude/hooks/", /\.(?:mjs|js)$/, /["']\.claude\/hooks\/[^"']*["']/],
599
+ ];
600
+ for (const [prefix, extension, block] of scopes) {
601
+ if (!files.some((p) => p.startsWith(prefix) && extension.test(p)))
602
+ continue;
603
+ checked += 1;
604
+ if (!block.test(source)) {
605
+ failures.push({
606
+ subject: flatFile,
607
+ message: `has no config block for ${prefix}**, whose scripts are code this project runs`,
608
+ });
609
+ }
610
+ }
611
+ return { checked, failures };
612
+ },
613
+ };
614
+ const coverageGate = {
615
+ id: "coverage-gate",
616
+ level: "rubric",
617
+ category: "testing",
618
+ check: (s) => {
619
+ const { file, source } = s.vitest;
620
+ if (file === undefined || source === undefined)
621
+ return NONE;
622
+ const failures = [];
623
+ if (!/\bthresholds\s*:/.test(source)) {
624
+ failures.push({
625
+ subject: file,
626
+ message: "sets no coverage thresholds, so coverage can fall without failing anything",
627
+ });
628
+ }
629
+ if (!/\bperFile\s*:\s*true\b/.test(source)) {
630
+ failures.push({
631
+ subject: file,
632
+ message: "does not set coverage.thresholds.perFile: true, so one well-covered file hides an untested one",
633
+ });
634
+ }
635
+ return { checked: 2, failures };
636
+ },
637
+ };
638
+ const toolchainPinShape = {
639
+ id: "toolchain-pin-shape",
640
+ level: "rubric",
641
+ category: "deps",
642
+ check: (s) => {
643
+ const pkg = s.packageJson;
644
+ if (pkg === undefined)
645
+ return NONE;
646
+ const failures = [];
647
+ let checked = 0;
648
+ const packages = [...PIN_PACKAGES];
649
+ if (s.vitest.file !== undefined || depSpec(s, "vitest") !== undefined) {
650
+ packages.push("vitest");
651
+ }
652
+ for (const name of packages) {
653
+ checked += 1;
654
+ const spec = depSpec(s, name);
655
+ if (spec === undefined) {
656
+ failures.push({
657
+ subject: name,
658
+ message: "is not declared in package.json",
659
+ });
660
+ }
661
+ else if (isUnpinned(spec)) {
662
+ failures.push({
663
+ subject: name,
664
+ message: `is "${spec}", not a real version range, so installs are not reproducible`,
665
+ });
666
+ }
667
+ }
668
+ checked += 1;
669
+ if (typeof pkg["packageManager"] !== "string") {
670
+ failures.push({
671
+ subject: "package.json",
672
+ message: "declares no packageManager, so the package-manager version is unpinned",
673
+ });
674
+ }
675
+ const types = majorOf(depSpec(s, "@types/node"));
676
+ const pin = /^v?(\d+)/.exec(s.nodeVersion ?? "");
677
+ if (types !== undefined && pin?.[1] !== undefined) {
678
+ checked += 1;
679
+ if (types !== Number(pin[1])) {
680
+ failures.push({
681
+ subject: "@types/node",
682
+ message: `is major ${types}, but .node-version pins Node ${pin[1]}, so types describe a different runtime`,
683
+ });
684
+ }
685
+ }
686
+ return { checked, failures };
687
+ },
688
+ };
689
+ /** Every rule, structural first. Order is the order findings are reported in. */
690
+ export const RULES = [
691
+ tsconfigParses,
692
+ tsconfigExtendsResolves,
693
+ tsconfigEmitCoherence,
694
+ gateWiring,
695
+ gateLaneParity,
696
+ nodePinCoherence,
697
+ strictFlags,
698
+ tsconfigOptionLifecycle,
699
+ moduleTargetModern,
700
+ eslintFlatConfig,
701
+ eslintTypedLinting,
702
+ eslintCoversEmittedCode,
703
+ coverageGate,
704
+ toolchainPinShape,
705
+ ];
706
+ //# sourceMappingURL=rules.js.map