paperlint 2.1.0 → 3.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 (219) hide show
  1. package/CLAUDE.md +6 -1
  2. package/README.md +243 -235
  3. package/action.yml +4 -6
  4. package/dist/adapters/references/index.d.ts +13 -0
  5. package/dist/adapters/references/index.d.ts.map +1 -0
  6. package/dist/adapters/references/index.js +55 -0
  7. package/dist/adapters/references/index.js.map +1 -0
  8. package/dist/adapters/references/reach.io.d.ts +3 -0
  9. package/dist/adapters/references/reach.io.d.ts.map +1 -0
  10. package/dist/adapters/references/reach.io.js +14 -0
  11. package/dist/adapters/references/reach.io.js.map +1 -0
  12. package/dist/build-engine.d.ts.map +1 -1
  13. package/dist/build-engine.js +6 -2
  14. package/dist/build-engine.js.map +1 -1
  15. package/dist/build.d.ts +15 -0
  16. package/dist/build.d.ts.map +1 -1
  17. package/dist/build.js +60 -7
  18. package/dist/build.js.map +1 -1
  19. package/dist/cli.d.ts +94 -30
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +500 -288
  22. package/dist/cli.js.map +1 -1
  23. package/dist/doctor.d.ts.map +1 -1
  24. package/dist/doctor.js +40 -72
  25. package/dist/doctor.js.map +1 -1
  26. package/dist/engine.d.ts +9 -0
  27. package/dist/engine.d.ts.map +1 -1
  28. package/dist/engine.js +27 -0
  29. package/dist/engine.js.map +1 -1
  30. package/dist/facts-file.d.ts +2 -2
  31. package/dist/facts-file.js +2 -2
  32. package/dist/hooks-settings.d.ts +0 -21
  33. package/dist/hooks-settings.d.ts.map +1 -1
  34. package/dist/hooks-settings.js +17 -135
  35. package/dist/hooks-settings.js.map +1 -1
  36. package/dist/init.d.ts +39 -29
  37. package/dist/init.d.ts.map +1 -1
  38. package/dist/init.js +144 -173
  39. package/dist/init.js.map +1 -1
  40. package/dist/link-skills.d.ts +1 -1
  41. package/dist/link-skills.d.ts.map +1 -1
  42. package/dist/link-skills.js +11 -29
  43. package/dist/link-skills.js.map +1 -1
  44. package/dist/new-paper.d.ts +8 -1
  45. package/dist/new-paper.d.ts.map +1 -1
  46. package/dist/new-paper.js +49 -25
  47. package/dist/new-paper.js.map +1 -1
  48. package/dist/paper-files.d.ts +85 -0
  49. package/dist/paper-files.d.ts.map +1 -0
  50. package/dist/paper-files.js +96 -0
  51. package/dist/paper-files.js.map +1 -0
  52. package/dist/paper-settings.d.ts +13 -41
  53. package/dist/paper-settings.d.ts.map +1 -1
  54. package/dist/paper-settings.js +71 -71
  55. package/dist/paper-settings.js.map +1 -1
  56. package/dist/ports/check-references.d.ts +31 -0
  57. package/dist/ports/check-references.d.ts.map +1 -0
  58. package/dist/ports/check-references.js +7 -0
  59. package/dist/ports/check-references.js.map +1 -0
  60. package/dist/presets.d.ts +1 -1
  61. package/dist/presets.d.ts.map +1 -1
  62. package/dist/presets.js +4 -6
  63. package/dist/presets.js.map +1 -1
  64. package/dist/reference-rules.d.ts +47 -0
  65. package/dist/reference-rules.d.ts.map +1 -0
  66. package/dist/reference-rules.js +156 -0
  67. package/dist/reference-rules.js.map +1 -0
  68. package/dist/references.d.ts +36 -0
  69. package/dist/references.d.ts.map +1 -0
  70. package/dist/references.js +91 -0
  71. package/dist/references.js.map +1 -0
  72. package/dist/rules-config.d.ts +5 -3
  73. package/dist/rules-config.d.ts.map +1 -1
  74. package/dist/rules-config.js +15 -6
  75. package/dist/rules-config.js.map +1 -1
  76. package/dist/structure.js +3 -3
  77. package/dist/structure.js.map +1 -1
  78. package/dist/tex-requirements.js +2 -2
  79. package/dist/tex-requirements.js.map +1 -1
  80. package/dist/toolchain.d.ts +5 -1
  81. package/dist/toolchain.d.ts.map +1 -1
  82. package/dist/toolchain.js +22 -5
  83. package/dist/toolchain.js.map +1 -1
  84. package/dist/types.d.ts +26 -18
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/venue-rules.d.ts.map +1 -1
  87. package/dist/venue-rules.js +12 -10
  88. package/dist/venue-rules.js.map +1 -1
  89. package/docs/configuration.md +163 -104
  90. package/docs/e2e.md +1 -1
  91. package/docs/install.md +25 -40
  92. package/docs/optional-rules.md +5 -4
  93. package/docs/rules.md +65 -24
  94. package/docs/skills.md +80 -0
  95. package/eslint-rules/bib-reachable-entry.mjs +75 -0
  96. package/eslint-rules/latex-language.mjs +29 -1
  97. package/eslint-rules/paper-stages.harness.mjs +1 -117
  98. package/eslint-rules/paper-stages.mjs +0 -120
  99. package/eslint-rules/paper-stages.mutations.mjs +0 -76
  100. package/eslint-rules/paper-typography.mjs +306 -191
  101. package/eslint-rules/paper-typography.test.mjs +337 -0
  102. package/eslint-rules/papers.harness.mjs +8 -12
  103. package/eslint-rules/papers.mjs +17 -31
  104. package/eslint-rules/pdf-last-page-balance.mjs +4 -7
  105. package/eslint-rules/review-frontmatter.mjs +118 -0
  106. package/eslint-rules/review-frontmatter.schema.json +36 -0
  107. package/eslint-rules/review-frontmatter.test.mjs +118 -0
  108. package/eslint-rules/sibling-frontmatter.mjs +19 -0
  109. package/eslint-rules/sibling-frontmatter.schema.json +14 -0
  110. package/eslint.config.mjs +0 -15
  111. package/fixtures/real-markdown-paper/baseline.json +6 -7
  112. package/fixtures/toolchain-mirror/catalog.txt +1 -0
  113. package/fixtures/toolchain-mirror/install-tl +4 -1
  114. package/fixtures/toolchain-mirror/stub-fmtutil-sys +4 -0
  115. package/fixtures/toolchain-mirror/stub-tlmgr +15 -0
  116. package/hooks/hooks.harness.mjs +39 -98
  117. package/hooks/hooks.mutations.mjs +6 -6
  118. package/hooks/paper-edit-guard.hook.d.mts +2 -3
  119. package/hooks/paper-edit-guard.hook.mjs +31 -42
  120. package/hooks/paper-skills-nudge.hook.mjs +21 -19
  121. package/hooks/paper-status-gates.hook.mjs +21 -19
  122. package/hooks/paper-status-gates.sh +8 -13
  123. package/lib/paper-config.d.mts +15 -25
  124. package/lib/paper-config.harness.mjs +18 -35
  125. package/lib/paper-config.mjs +83 -105
  126. package/lib/paper-config.mutations.mjs +2 -2
  127. package/lib/skill-checks.mjs +1 -1
  128. package/lib/skill-trigger-cases.harness.mjs +9 -8
  129. package/lib/skill-trigger-cases.mjs +7 -15
  130. package/package.json +1 -1
  131. package/scripts/eslint-report-guard.mjs +1 -1
  132. package/scripts/harness-api.frozen.json +0 -3
  133. package/scripts/mutation-batteries.frozen.json +5 -14
  134. package/skills/analyze-sibling-paper/SKILL.md +16 -12
  135. package/skills/analyze-sibling-paper/SKILL.md.spec.ts +15 -11
  136. package/skills/cold-read-diff/SKILL.md +20 -1
  137. package/skills/cold-read-diff/SKILL.md.spec.ts +19 -0
  138. package/skills/grade-paper-writing/SKILL.md +20 -1
  139. package/skills/grade-paper-writing/SKILL.md.spec.ts +19 -0
  140. package/skills/paper-pipeline/scripts/consumer.d.mts +0 -1
  141. package/skills/paper-pipeline/scripts/consumer.harness.mjs +8 -12
  142. package/skills/paper-pipeline/scripts/consumer.mjs +19 -33
  143. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +6 -4
  144. package/skills/paper-pipeline/scripts/run-mechanical.mjs +2 -11
  145. package/skills/plan-paper-timeline/SKILL.md +2 -2
  146. package/skills/plan-paper-timeline/SKILL.md.spec.ts +1 -1
  147. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +1 -1
  148. package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +2 -2
  149. package/skills/render-paper/check-render.sh +2 -3
  150. package/skills/render-paper/render-paper.harness.mjs +2 -2
  151. package/skills/verify-citations/SKILL.md +10 -1
  152. package/skills/verify-citations/SKILL.md.spec.ts +9 -0
  153. package/skills/verify-citations/scripts/bib-authors.mjs +48 -26
  154. package/src/adapters/references/index.ts +102 -0
  155. package/src/adapters/references/reach.io.ts +12 -0
  156. package/src/build-engine.ts +6 -1
  157. package/src/build.harness.mjs +54 -6
  158. package/src/build.ts +89 -11
  159. package/src/cli.harness.mjs +149 -294
  160. package/src/cli.mutations.mjs +35 -44
  161. package/src/cli.ts +637 -328
  162. package/src/doctor.harness.mjs +41 -84
  163. package/src/doctor.mutations.mjs +9 -9
  164. package/src/doctor.ts +44 -89
  165. package/src/engine.harness.mjs +50 -0
  166. package/src/engine.ts +35 -0
  167. package/src/facts-file.test.ts +0 -7
  168. package/src/facts-file.ts +2 -2
  169. package/src/hooks-settings.harness.mjs +7 -92
  170. package/src/hooks-settings.mutations.mjs +2 -17
  171. package/src/hooks-settings.ts +18 -162
  172. package/src/init.test.ts +206 -0
  173. package/src/init.ts +187 -231
  174. package/src/link-skills.harness.mjs +1 -38
  175. package/src/link-skills.mutations.mjs +9 -5
  176. package/src/link-skills.ts +10 -34
  177. package/src/lint-fix.test.ts +120 -0
  178. package/src/linted-set.test.ts +118 -0
  179. package/src/new-paper.harness.mjs +2 -0
  180. package/src/new-paper.mutations.mjs +0 -14
  181. package/src/new-paper.test.ts +173 -14
  182. package/src/new-paper.ts +82 -30
  183. package/src/paper-files.test.ts +395 -0
  184. package/src/paper-files.ts +200 -0
  185. package/src/paper-settings-commands.test.ts +121 -122
  186. package/src/paper-settings.test.ts +93 -64
  187. package/src/paper-settings.ts +99 -97
  188. package/src/parse-args.test.ts +48 -0
  189. package/src/ports/check-references.ts +28 -0
  190. package/src/presets.ts +5 -15
  191. package/src/reference-rules.ts +229 -0
  192. package/src/references.test.ts +209 -0
  193. package/src/references.ts +138 -0
  194. package/src/rules-config.harness.mjs +24 -8
  195. package/src/rules-config.ts +19 -7
  196. package/src/rules-off.test.ts +111 -0
  197. package/src/structure.harness.mjs +4 -2
  198. package/src/structure.ts +3 -3
  199. package/src/tex-requirements.ts +2 -2
  200. package/src/toolchain.harness.mjs +41 -6
  201. package/src/toolchain.ts +29 -5
  202. package/src/types.ts +36 -18
  203. package/src/venue-rules.test.ts +23 -11
  204. package/src/venue-rules.ts +12 -14
  205. package/templates/paper/paperlint.json +1 -1
  206. package/eslint-rules/doc-fields.harness.mjs +0 -336
  207. package/eslint-rules/doc-fields.mjs +0 -186
  208. package/eslint-rules/doc-fields.mutations.mjs +0 -96
  209. package/eslint-rules/paper-typography.harness.mjs +0 -291
  210. package/eslint-rules/paper-typography.mutations.mjs +0 -131
  211. package/eslint-rules/review-findings-cause.harness.mjs +0 -228
  212. package/eslint-rules/review-findings-cause.mjs +0 -135
  213. package/eslint-rules/review-findings-cause.mutations.mjs +0 -72
  214. package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +0 -16
  215. package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +0 -17
  216. package/fixtures/review-findings-cause/clean.md +0 -17
  217. package/fixtures/review-findings-cause/defect.md +0 -14
  218. package/fixtures/review-findings-cause/old-debt.md +0 -14
  219. package/fixtures/review-findings-cause/quiet-in-fence.md +0 -16
@@ -36,7 +36,6 @@ const {
36
36
  shippedWiring,
37
37
  wireHooks,
38
38
  wiredCounts,
39
- pluginEnabledHere,
40
39
  doctorHooks,
41
40
  readSettings,
42
41
  MANAGED_BY,
@@ -83,36 +82,26 @@ try {
83
82
  // ── which spelling runs which hook ─────────────────────────────────────────────────────
84
83
  const ours = `node "\${CLAUDE_PROJECT_DIR}/${MANAGED_BY}" hook paper-edit-guard`;
85
84
  const cases = [
86
- [ours, { name: "paper-edit-guard", ours: true, legacy: false }],
85
+ [ours, { name: "paper-edit-guard", ours: true }],
87
86
  [
88
87
  `node "$CLAUDE_PROJECT_DIR/${MANAGED_BY}" hook paper-status-gates`,
89
- { name: "paper-status-gates", ours: true, legacy: false },
90
- ],
91
- // Guards: what an older install wrote is recognised as ours-but-stale. Spelled literally, not
92
- // built from a constant: these are what 1.x and 2.0.0 actually put in users' settings files.
93
- [
94
- `node "$CLAUDE_PROJECT_DIR/node_modules/research-paper-pipeline/bin/rpp.mjs" hook paper-edit-guard`,
95
- { name: "paper-edit-guard", ours: false, legacy: true },
96
- ],
97
- [
98
- `node "\${CLAUDE_PROJECT_DIR}/node_modules/paperlint/bin/rpp.mjs" hook paper-status-gates`,
99
- { name: "paper-status-gates", ours: false, legacy: true },
88
+ { name: "paper-status-gates", ours: true },
100
89
  ],
101
90
  [
102
91
  `node "$CLAUDE_PROJECT_DIR/node_modules/vigiles/dist/cli.js" hook-runtime run-program "$CLAUDE_PROJECT_DIR/node_modules/paperlint/hooks/paper-edit-guard.hook.mjs"`,
103
- { name: "paper-edit-guard", ours: false, legacy: false },
92
+ { name: "paper-edit-guard", ours: false },
104
93
  ],
105
94
  [
106
95
  `npx paperlint hook paper-skills-nudge`,
107
- { name: "paper-skills-nudge", ours: false, legacy: false },
96
+ { name: "paper-skills-nudge", ours: false },
108
97
  ],
109
98
  [
110
99
  `npx paperlint hook paper-skills-nudge`,
111
- { name: "paper-skills-nudge", ours: false, legacy: false },
100
+ { name: "paper-skills-nudge", ours: false },
112
101
  ],
113
102
  [
114
103
  `node /abs/proj/node_modules/paperlint/bin/paperlint.mjs hook paper-edit-guard`,
115
- { name: "paper-edit-guard", ours: false, legacy: false },
104
+ { name: "paper-edit-guard", ours: false },
116
105
  ],
117
106
  [`node my-own-lint.mjs`, null],
118
107
  [`node node_modules/paperlint/bin/paperlint.mjs lint`, null],
@@ -158,46 +147,6 @@ try {
158
147
  );
159
148
  }
160
149
 
161
- // ── what an older install wrote is migrated, the user's own command kept ─────────────────
162
- // Two real histories, spelled literally: 1.x wrote the old package directory, 2.0.0 wrote the
163
- // new directory with the old entry file name. Neither file exists after an upgrade.
164
- for (const [release, stale] of [
165
- ["1.x", "node_modules/research-paper-pipeline/bin/rpp.mjs"],
166
- ["2.0.0", "node_modules/paperlint/bin/rpp.mjs"],
167
- ]) {
168
- const legacyWired = JSON.parse(
169
- JSON.stringify(merge({}, wiring.compiled, MANAGED_BY)).replaceAll(
170
- MANAGED_BY,
171
- stale,
172
- ),
173
- );
174
- legacyWired.hooks.PostToolUse[0].hooks.push({
175
- type: "command",
176
- command: "node my-own-lint.mjs",
177
- });
178
- const dir = project(`legacy-${release}`, legacyWired);
179
- const before = doctorHooks(dir, wiring).join("\n");
180
- check(
181
- `🔴 doctor names hook commands ${release} left behind — the missing file, and the fix`,
182
- before.includes(`run ${stale}, which this version does not install`) &&
183
- /npx paperlint init` replaces them/.test(before),
184
- );
185
- const r = wireHooks(dir, merge, wiring);
186
- const s = JSON.parse(text(dir));
187
- const counts = [...wiredCounts(s, wiring.names).values()];
188
- check(
189
- `🔴 init replaces what ${release} wrote: every hook wired once, none left at the old path`,
190
- r.status === "written" &&
191
- r.replaced === wiring.names.length &&
192
- counts.every((c) => c.ours === 1 && c.legacy === 0 && c.other === 0) &&
193
- !text(dir).includes(stale),
194
- );
195
- check(
196
- `and the user's own command in the same matcher survives the ${release} migration`,
197
- text(dir).includes("node my-own-lint.mjs"),
198
- );
199
- }
200
-
201
150
  // ── the user's own hooks and keys survive ──────────────────────────────────────────────
202
151
  {
203
152
  const dir = project("user", {
@@ -250,7 +199,7 @@ try {
250
199
  {
251
200
  type: "command",
252
201
  command:
253
- 'node "$CLAUDE_PROJECT_DIR/node_modules/vigiles/dist/cli.js" hook-runtime run-program "$CLAUDE_PROJECT_DIR/node_modules/research-paper-pipeline/hooks/paper-edit-guard.hook.mjs"',
202
+ 'node "$CLAUDE_PROJECT_DIR/node_modules/vigiles/dist/cli.js" hook-runtime run-program "$CLAUDE_PROJECT_DIR/node_modules/paperlint/hooks/paper-edit-guard.hook.mjs"',
254
203
  },
255
204
  ],
256
205
  },
@@ -296,25 +245,6 @@ try {
296
245
  );
297
246
  }
298
247
 
299
- // ── the plugin, enabled by the project ─────────────────────────────────────────────────
300
- check(
301
- "the project's enabledPlugins entry for this package is found",
302
- pluginEnabledHere({
303
- enabledPlugins: {
304
- "research-paper-pipeline@research-paper-pipeline": true,
305
- "other@x": true,
306
- },
307
- }).join() === "research-paper-pipeline@research-paper-pipeline",
308
- );
309
- check(
310
- "a disabled entry does not count",
311
- pluginEnabledHere({
312
- enabledPlugins: {
313
- "research-paper-pipeline@research-paper-pipeline": false,
314
- },
315
- }).length === 0,
316
- );
317
-
318
248
  // ── doctor's section ───────────────────────────────────────────────────────────────────
319
249
  const doc = (settings) => {
320
250
  const dir = project(`doc-${String(n)}`, settings);
@@ -361,21 +291,6 @@ try {
361
291
  handPartial,
362
292
  ) && /writes nothing then/.test(handPartial),
363
293
  );
364
- const pluginText = doc({
365
- ...wiredOnce,
366
- enabledPlugins: { "research-paper-pipeline@research-paper-pipeline": true },
367
- });
368
- check(
369
- "the project also enabling the plugin → told to uninstall it, with the command",
370
- /ALSO enables the plugin/.test(pluginText) &&
371
- /\/plugin uninstall research-paper-pipeline@research-paper-pipeline/.test(
372
- pluginText,
373
- ),
374
- );
375
- check(
376
- "🔴 and it says it cannot see a USER-scope plugin, rather than implying there is none",
377
- /USER scope is not visible from here/.test(doc({})),
378
- );
379
294
  check(
380
295
  "an unparsable file is named in doctor, not crashed on",
381
296
  /does not parse/.test(
@@ -24,12 +24,12 @@ process.exit(
24
24
  harness: HARNESS,
25
25
  // The spelling truth table is the first owner: `npx paperlint hook …` must read as NOT ours,
26
26
  // and that row is where it goes red.
27
- expect: '→ {"name":"paper-edit-guard","ours":false,"legacy":true}',
27
+ expect: '→ {"name":"paper-skills-nudge","ours":false}',
28
28
  disables:
29
29
  "the duplicate guard for every bin spelling but ours. A hook wired by hand as " +
30
30
  "`npx paperlint hook …` or through an absolute path to the bin reads as ours, init merges its own " +
31
31
  "copy beside it, and the hook runs twice per event",
32
- edits: [[SRC, " ours: t === MANAGED_BY,", " ours: true,"]],
32
+ edits: [[SRC, "ours: t === MANAGED_BY,", "ours: true,"]],
33
33
  },
34
34
  {
35
35
  name: "another spelling no longer stops the write",
@@ -94,21 +94,6 @@ process.exit(
94
94
  "same thing again — a loop with no exit",
95
95
  edits: [[SRC, " const remedy = handWired", " const remedy = false"]],
96
96
  },
97
- {
98
- name: "a disabled plugin counts as enabled",
99
- harness: HARNESS,
100
- expect: "a disabled entry does not count",
101
- disables:
102
- "the plugin warning's precision. A project that already switched the plugin off would " +
103
- "be told to uninstall it",
104
- edits: [
105
- [
106
- SRC,
107
- ' ([id, on]) => on === true && id.split("@")[0] === LEGACY_PACKAGE_NAME,',
108
- ' ([id]) => id.split("@")[0] === LEGACY_PACKAGE_NAME,',
109
- ],
110
- ],
111
- },
112
97
  ],
113
98
  }),
114
99
  );
@@ -28,7 +28,6 @@ import { basename, dirname, join, posix } from "node:path";
28
28
  import { fileURLToPath } from "node:url";
29
29
  import {
30
30
  BIN_FILE,
31
- LEGACY_PACKAGE_NAME,
32
31
  PACKAGE_NAME,
33
32
  } from "../skills/paper-pipeline/scripts/consumer.mjs";
34
33
 
@@ -47,9 +46,6 @@ export const WIRING_FILE = fileURLToPath(
47
46
  */
48
47
  export const MANAGED_BY = `node_modules/${PACKAGE_NAME}/${BIN_FILE}`;
49
48
 
50
- /** The names the package's directory under `node_modules` has had. */
51
- const PKG_DIRS: readonly string[] = [PACKAGE_NAME, LEGACY_PACKAGE_NAME];
52
-
53
49
  export interface HookCommand {
54
50
  readonly type: string;
55
51
  readonly command: string;
@@ -80,7 +76,6 @@ function bare(token: string): string {
80
76
  interface Run {
81
77
  readonly name: string;
82
78
  readonly ours: boolean;
83
- readonly legacy: boolean;
84
79
  readonly path: string;
85
80
  }
86
81
 
@@ -90,19 +85,11 @@ interface Run {
90
85
  * Recognised spellings — each is ONE lexeme of a shell command, which is what a command is:
91
86
  * `node <…>/node_modules/paperlint/bin/paperlint.mjs hook <name>`
92
87
  * ours when the path is exactly MANAGED_BY
93
- * `node node_modules/<pkg>/bin/<any other file> hook <name>`, <pkg> this name or the old one
94
- * `legacy`: the project-relative form `init` writes, pointing at a file
95
- * this version does not install — what an older `init` left behind
96
- * (1.x: `research-paper-pipeline/bin/rpp.mjs`; 2.0.0: `paperlint/bin/rpp.mjs`)
97
- * `npx paperlint hook <name>`, `node_modules/.bin/paperlint hook <name>` (or under the old package name),
98
- * an absolute path into the package's `bin/`
88
+ * `npx paperlint hook <name>`, `node_modules/.bin/paperlint hook <name>`, an absolute path into
89
+ * the package's `bin/`
99
90
  * another spelling
100
- * `vigiles … run-program <…>/node_modules/<pkg>/hooks/<name>.hook.mjs`
91
+ * `vigiles … run-program <…>/node_modules/paperlint/hooks/<name>.hook.mjs`
101
92
  * another spelling
102
- * The last is how the one real consumer wired all three by hand before `init` could.
103
- *
104
- * `legacy` is decided by the SHAPE `init` writes, not by a list of past file names: any file in
105
- * our `bin/` other than BIN_FILE is one that no longer ships, so a future rename is covered too.
106
93
  */
107
94
  function parseRun(command: string): Run | null {
108
95
  const tokens = command.trim().split(/\s+/).map(bare);
@@ -113,54 +100,37 @@ function parseRun(command: string): Run | null {
113
100
  return null;
114
101
  }
115
102
 
116
- /** The path segments after `node_modules/<pkg>/`, and whether the token starts there. */
117
- function insidePackage(parts: readonly string[]): {
118
- readonly inside: readonly string[];
119
- readonly relative: boolean;
120
- } {
103
+ /** The path segments after `node_modules/paperlint/`. */
104
+ function insidePackage(parts: readonly string[]): readonly string[] {
121
105
  const at = parts.findIndex(
122
- (p, j) => p === "node_modules" && PKG_DIRS.includes(parts[j + 1] ?? ""),
106
+ (p, j) => p === "node_modules" && parts[j + 1] === PACKAGE_NAME,
123
107
  );
124
- return {
125
- inside: at === -1 ? [] : parts.slice(at + 2),
126
- relative: at === 0,
127
- };
108
+ return at === -1 ? [] : parts.slice(at + 2);
128
109
  }
129
110
 
130
111
  /** The run token `i` starts, if it names a paperlint hook. */
131
112
  function runAt(tokens: readonly string[], i: number): Run | null {
132
113
  const t = posix.normalize(tokens[i] ?? "");
133
- const { inside, relative } = insidePackage(t.split("/"));
114
+ const inside = insidePackage(t.split("/"));
134
115
  const inBin = inside[0] === "bin" && inside.length === 2;
135
116
  const name = tokens[i + 2];
136
117
  if (
137
- (inBin || PKG_DIRS.includes(basename(t))) &&
118
+ (inBin || basename(t) === PACKAGE_NAME) &&
138
119
  tokens[i + 1] === "hook" &&
139
120
  name
140
121
  )
141
- return {
142
- name,
143
- ours: t === MANAGED_BY,
144
- legacy: inBin && relative && t !== MANAGED_BY,
145
- path: t,
146
- };
122
+ return { name, ours: t === MANAGED_BY, path: t };
147
123
  if (inside[0] === "hooks" && inside[1]?.endsWith(".hook.mjs"))
148
- return {
149
- name: basename(inside[1], ".hook.mjs"),
150
- ours: false,
151
- legacy: false,
152
- path: t,
153
- };
124
+ return { name: basename(inside[1], ".hook.mjs"), ours: false, path: t };
154
125
  return null;
155
126
  }
156
127
 
157
128
  export function hookRun(command: string): {
158
129
  readonly name: string;
159
130
  readonly ours: boolean;
160
- readonly legacy: boolean;
161
131
  } | null {
162
132
  const run = parseRun(command);
163
- return run && { name: run.name, ours: run.ours, legacy: run.legacy };
133
+ return run && { name: run.name, ours: run.ours };
164
134
  }
165
135
 
166
136
  /** Every command in a settings object, with the event it hangs off. */
@@ -205,69 +175,18 @@ export function shippedWiring(file: string = WIRING_FILE): Wiring {
205
175
  export function wiredCounts(
206
176
  settings: Settings,
207
177
  names: readonly string[],
208
- ): Map<string, { ours: number; legacy: number; other: number }> {
209
- const counts = new Map(
210
- names.map((n) => [n, { ours: 0, legacy: 0, other: 0 }]),
211
- );
178
+ ): Map<string, { ours: number; other: number }> {
179
+ const counts = new Map(names.map((n) => [n, { ours: 0, other: 0 }]));
212
180
  for (const { command } of commandsIn(settings)) {
213
181
  const run = hookRun(command);
214
182
  const slot = run ? counts.get(run.name) : undefined;
215
183
  if (run && slot)
216
184
  if (run.ours) slot.ours++;
217
- else if (run.legacy) slot.legacy++;
218
185
  else slot.other++;
219
186
  }
220
187
  return counts;
221
188
  }
222
189
 
223
- /**
224
- * The settings with every command an install under the old package name wrote removed, and the
225
- * matcher blocks and events left empty by that removal dropped. The user's own commands stay.
226
- */
227
- export function withoutLegacy(settings: Settings): {
228
- readonly settings: Settings;
229
- readonly removed: number;
230
- } {
231
- const hooks = settings["hooks"];
232
- if (!hooks || typeof hooks !== "object") return { settings, removed: 0 };
233
- let removed = 0;
234
- const next: Record<string, HookEntry[]> = {};
235
- for (const [event, entries] of Object.entries(hooks as HooksMap)) {
236
- const kept: HookEntry[] = [];
237
- for (const entry of Array.isArray(entries) ? entries : []) {
238
- const list: readonly HookCommand[] = Array.isArray(entry?.hooks)
239
- ? entry.hooks
240
- : [];
241
- const own = list.filter((h) => {
242
- const legacy =
243
- typeof h?.command === "string" && hookRun(h.command)?.legacy === true;
244
- if (legacy) removed++;
245
- return !legacy;
246
- });
247
- if (own.length > 0 || list.length === 0)
248
- kept.push({ ...entry, hooks: own });
249
- }
250
- if (kept.length > 0) next[event] = kept;
251
- }
252
- return removed === 0
253
- ? { settings, removed }
254
- : { settings: { ...settings, hooks: next }, removed };
255
- }
256
-
257
- /**
258
- * Plugin ids the PROJECT enables for this package. A user-scope plugin lives in the user's own
259
- * settings, which this cannot see — callers must say so rather than print "not enabled".
260
- */
261
- export function pluginEnabledHere(settings: Settings): string[] {
262
- const enabled = settings["enabledPlugins"];
263
- if (!enabled || typeof enabled !== "object") return [];
264
- return Object.entries(enabled as Record<string, unknown>)
265
- .filter(
266
- ([id, on]) => on === true && id.split("@")[0] === LEGACY_PACKAGE_NAME,
267
- )
268
- .map(([id]) => id);
269
- }
270
-
271
190
  export type SettingsRead =
272
191
  | { readonly status: "absent"; readonly path: string; readonly settings: {} }
273
192
  | {
@@ -301,9 +220,6 @@ export type WireResult =
301
220
  readonly status: "written" | "present";
302
221
  readonly path: string;
303
222
  readonly names: readonly string[];
304
- readonly plugin: readonly string[];
305
- /** Commands an install under the old package name wrote, removed before the merge. */
306
- readonly replaced: number;
307
223
  }
308
224
  | {
309
225
  readonly status: "foreign";
@@ -316,7 +232,6 @@ export type WireResult =
316
232
  }[];
317
233
  /** Shipped hooks that no command runs in ANY spelling — left unwired as well. */
318
234
  readonly missing: readonly string[];
319
- readonly plugin: readonly string[];
320
235
  }
321
236
  | {
322
237
  readonly status: "unparsable";
@@ -342,11 +257,7 @@ export function wireHooks(
342
257
  ): WireResult {
343
258
  const read = readSettings(root);
344
259
  if (read.status === "unparsable") return read;
345
- const { path } = read;
346
- // Commands from an install under the old package name point at a directory that is gone after
347
- // the upgrade. They are ours, so they are replaced rather than counted as someone else's.
348
- const { settings, removed } = withoutLegacy(read.settings);
349
- const plugin = pluginEnabledHere(settings);
260
+ const { path, settings } = read;
350
261
  const found = commandsIn(settings)
351
262
  .map(({ command }) => ({ command, run: hookRun(command) }))
352
263
  .filter(({ run }) => run && !run.ours && wiring.names.includes(run.name))
@@ -356,63 +267,20 @@ export function wireHooks(
356
267
  const missing = wiring.names.filter(
357
268
  (n) => (counts.get(n)?.ours ?? 0) + (counts.get(n)?.other ?? 0) === 0,
358
269
  );
359
- return {
360
- status: "foreign",
361
- path,
362
- names: wiring.names,
363
- found,
364
- missing,
365
- plugin,
366
- };
270
+ return { status: "foreign", path, names: wiring.names, found, missing };
367
271
  }
368
272
 
369
273
  const next = merge(settings, wiring.compiled, MANAGED_BY);
370
274
  if (JSON.stringify(next) === JSON.stringify(read.settings))
371
- return {
372
- status: "present",
373
- path,
374
- names: wiring.names,
375
- plugin,
376
- replaced: 0,
377
- };
275
+ return { status: "present", path, names: wiring.names };
378
276
  mkdirSync(dirname(path), { recursive: true });
379
277
  writeFileSync(path, JSON.stringify(next, null, 2) + "\n", "utf8");
380
- return {
381
- status: "written",
382
- path,
383
- names: wiring.names,
384
- plugin,
385
- replaced: removed,
386
- };
278
+ return { status: "written", path, names: wiring.names };
387
279
  }
388
280
 
389
281
  /** The consequence every report of a written hook must carry, in one place. */
390
282
  export const FRESH_CLONE_NOTE = `the commands point into node_modules/${PACKAGE_NAME}/ — in a fresh clone they cannot run until \`npm install\` has run there`;
391
283
 
392
- /** The instruction for a project that also enables the plugin (it only ever existed under the old name). */
393
- export const UNINSTALL_PLUGIN = `/plugin uninstall ${LEGACY_PACKAGE_NAME}@${LEGACY_PACKAGE_NAME}`;
394
-
395
- /**
396
- * The doctor lines for hook commands an older install left behind: each names the file it runs,
397
- * which this version does not install, so the hook silently does nothing.
398
- */
399
- function legacyLines(settings: Settings): string[] {
400
- const stale = new Map<string, number>();
401
- for (const { command } of commandsIn(settings)) {
402
- const run = parseRun(command);
403
- if (run?.legacy) stale.set(run.path, (stale.get(run.path) ?? 0) + 1);
404
- }
405
- return stale.size === 0
406
- ? []
407
- : [
408
- ...[...stale].map(
409
- ([path, n]) =>
410
- ` ⚠ ${String(n)} hook command(s) run ${path}, which this version does not install — they no longer run.`,
411
- ),
412
- ` \`npx ${PACKAGE_NAME} init\` replaces them`,
413
- ];
414
- }
415
-
416
284
  /**
417
285
  * `paperlint doctor`'s section. ADVISORY — it never fails the run: the hooks are an in-editor guard,
418
286
  * `--no-hooks` is a legitimate choice, and a doctor that exits non-zero on a choice gets muted.
@@ -461,17 +329,5 @@ export function doctorHooks(
461
329
  ` ✓ wired — ${wiring.names.join(", ")}, once each`,
462
330
  ` ${FRESH_CLONE_NOTE}`,
463
331
  );
464
- out.push(...legacyLines(read.settings));
465
- const plugin = pluginEnabledHere(read.settings);
466
- if (plugin.length > 0) {
467
- out.push(
468
- ` ⚠ this project ALSO enables the plugin (${plugin.join(", ")}) — every hook would run twice.`,
469
- ` the plugin no longer carries the hooks: ${UNINSTALL_PLUGIN}`,
470
- ` and remove it from "enabledPlugins" in ${SETTINGS_PATH}`,
471
- );
472
- }
473
- out.push(
474
- ` · a plugin installed at USER scope is not visible from here — \`/plugin\` inside Claude Code shows it`,
475
- );
476
332
  return out;
477
333
  }