paperlint 3.0.0 → 3.1.1

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 (319) hide show
  1. package/.github/workflows/ci.yml +28 -41
  2. package/.github/workflows/dependabot-automerge.yml +1 -1
  3. package/CLAUDE.md +51 -56
  4. package/CONTRIBUTING.md +30 -18
  5. package/README.md +179 -180
  6. package/bin/paperlint.test.mjs +45 -0
  7. package/dist/adapters/node/files.io.d.ts +2 -0
  8. package/dist/adapters/node/files.io.d.ts.map +1 -1
  9. package/dist/adapters/node/files.io.js +3 -2
  10. package/dist/adapters/node/files.io.js.map +1 -1
  11. package/dist/adapters/pdfjs/fill.d.ts +15 -4
  12. package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
  13. package/dist/adapters/pdfjs/fill.js +31 -21
  14. package/dist/adapters/pdfjs/fill.js.map +1 -1
  15. package/dist/adapters/references/index.d.ts.map +1 -1
  16. package/dist/adapters/references/index.js +14 -5
  17. package/dist/adapters/references/index.js.map +1 -1
  18. package/dist/adapters/references/pool.d.ts +8 -0
  19. package/dist/adapters/references/pool.d.ts.map +1 -0
  20. package/dist/adapters/references/pool.js +19 -0
  21. package/dist/adapters/references/pool.js.map +1 -0
  22. package/dist/build-engine.d.ts.map +1 -1
  23. package/dist/build-engine.js +6 -2
  24. package/dist/build-engine.js.map +1 -1
  25. package/dist/build.d.ts +10 -2
  26. package/dist/build.d.ts.map +1 -1
  27. package/dist/build.js +29 -11
  28. package/dist/build.js.map +1 -1
  29. package/dist/cli.d.ts +93 -6
  30. package/dist/cli.d.ts.map +1 -1
  31. package/dist/cli.js +296 -80
  32. package/dist/cli.js.map +1 -1
  33. package/dist/doctor.d.ts +27 -1
  34. package/dist/doctor.d.ts.map +1 -1
  35. package/dist/doctor.js +4 -3
  36. package/dist/doctor.js.map +1 -1
  37. package/dist/domain/result.d.ts +0 -2
  38. package/dist/domain/result.d.ts.map +1 -1
  39. package/dist/domain/result.js +0 -4
  40. package/dist/domain/result.js.map +1 -1
  41. package/dist/engine.d.ts +9 -0
  42. package/dist/engine.d.ts.map +1 -1
  43. package/dist/engine.js +27 -0
  44. package/dist/engine.js.map +1 -1
  45. package/dist/hooks-settings.d.ts +8 -5
  46. package/dist/hooks-settings.d.ts.map +1 -1
  47. package/dist/hooks-settings.js +32 -32
  48. package/dist/hooks-settings.js.map +1 -1
  49. package/dist/init.d.ts +8 -0
  50. package/dist/init.d.ts.map +1 -1
  51. package/dist/init.js +4 -4
  52. package/dist/init.js.map +1 -1
  53. package/dist/latex-log.d.ts +0 -2
  54. package/dist/latex-log.d.ts.map +1 -1
  55. package/dist/latex-log.js +7 -13
  56. package/dist/latex-log.js.map +1 -1
  57. package/dist/link-skills.d.ts +15 -2
  58. package/dist/link-skills.d.ts.map +1 -1
  59. package/dist/link-skills.js +36 -20
  60. package/dist/link-skills.js.map +1 -1
  61. package/dist/new-paper.d.ts +8 -1
  62. package/dist/new-paper.d.ts.map +1 -1
  63. package/dist/new-paper.js +50 -15
  64. package/dist/new-paper.js.map +1 -1
  65. package/dist/paper-files.d.ts +30 -8
  66. package/dist/paper-files.d.ts.map +1 -1
  67. package/dist/paper-files.js +59 -30
  68. package/dist/paper-files.js.map +1 -1
  69. package/dist/pdf-facts.d.ts +11 -0
  70. package/dist/pdf-facts.d.ts.map +1 -1
  71. package/dist/pdf-facts.js +16 -18
  72. package/dist/pdf-facts.js.map +1 -1
  73. package/dist/pdf-geometry.js +1 -1
  74. package/dist/pdf-geometry.js.map +1 -1
  75. package/dist/presets.d.ts +2 -0
  76. package/dist/presets.d.ts.map +1 -1
  77. package/dist/presets.js +3 -1
  78. package/dist/presets.js.map +1 -1
  79. package/dist/references.js +2 -2
  80. package/dist/references.js.map +1 -1
  81. package/dist/structure.js +1 -1
  82. package/dist/structure.js.map +1 -1
  83. package/dist/tex-requirements.d.ts +8 -0
  84. package/dist/tex-requirements.d.ts.map +1 -1
  85. package/dist/tex-requirements.js +6 -5
  86. package/dist/tex-requirements.js.map +1 -1
  87. package/dist/toolchain.d.ts +8 -3
  88. package/dist/toolchain.d.ts.map +1 -1
  89. package/dist/toolchain.js +26 -10
  90. package/dist/toolchain.js.map +1 -1
  91. package/dist/types.d.ts +6 -0
  92. package/dist/types.d.ts.map +1 -1
  93. package/dist/venue-rules.d.ts.map +1 -1
  94. package/dist/venue-rules.js +12 -10
  95. package/dist/venue-rules.js.map +1 -1
  96. package/docs/configuration.md +48 -4
  97. package/docs/prior-art/README.md +1 -1
  98. package/docs/prior-art/test-tooling.md +7 -30
  99. package/docs/rules.md +29 -25
  100. package/docs/skills.md +80 -0
  101. package/eslint-rules/install-path-literals.harness.mjs +17 -1
  102. package/eslint-rules/latex-language.harness.mjs +3 -3
  103. package/eslint-rules/latex-language.mjs +12 -9
  104. package/eslint-rules/latex-language.test.mjs +200 -0
  105. package/eslint-rules/paper-research-question.harness.mjs +73 -7
  106. package/eslint-rules/paper-stages.harness.mjs +9 -14
  107. package/eslint-rules/paper-stages.test.mjs +135 -0
  108. package/eslint-rules/paper-typography.mjs +56 -9
  109. package/eslint-rules/paper-typography.test.mjs +186 -1
  110. package/eslint-rules/pdf-last-page-balance.harness.mjs +63 -7
  111. package/eslint-rules/review-frontmatter.mjs +2 -1
  112. package/eslint-rules/review-frontmatter.schema.json +1 -1
  113. package/eslint-rules/review-frontmatter.test.mjs +37 -1
  114. package/eslint-rules/temp-root-realpath.harness.mjs +49 -0
  115. package/eslint-rules/tex-build.mjs +13 -13
  116. package/eslint.config.mjs +6 -2
  117. package/fixtures/toolchain-mirror/catalog.txt +1 -0
  118. package/fixtures/toolchain-mirror/install-tl +4 -1
  119. package/fixtures/toolchain-mirror/stub-fmtutil-sys +4 -0
  120. package/fixtures/toolchain-mirror/stub-tlmgr +15 -0
  121. package/hooks/hooks.harness.mjs +6 -11
  122. package/hooks/paper-edit-guard.hook.mjs +2 -2
  123. package/hooks/paper-edit-guard.test.mjs +61 -0
  124. package/hooks/paper-skills-nudge.hook.mjs +2 -2
  125. package/hooks/paper-skills-nudge.test.mjs +38 -0
  126. package/hooks/paper-status-gates.test.mjs +42 -0
  127. package/lib/agent-cli-version.harness.mjs +7 -0
  128. package/lib/check.d.mts +17 -0
  129. package/lib/check.mjs +50 -0
  130. package/lib/check.test.ts +67 -0
  131. package/lib/markdown.mjs +16 -69
  132. package/lib/markdown.test.mjs +246 -0
  133. package/lib/paper-config.harness.mjs +28 -9
  134. package/lib/paper-config.mjs +7 -2
  135. package/lib/skill-checks.mjs +18 -46
  136. package/lib/skill-checks.test.mjs +297 -0
  137. package/lib/skill-corpus.mjs +11 -13
  138. package/lib/skill-corpus.test.mjs +54 -0
  139. package/lib/skill-trigger-cases.harness.mjs +57 -4
  140. package/package.json +6 -5
  141. package/scripts/check.harness.mjs +4 -10
  142. package/scripts/check.mjs +45 -42
  143. package/scripts/check.test.mjs +104 -0
  144. package/scripts/coverage-config.test.ts +406 -0
  145. package/scripts/eslint-report-guard.test.mjs +48 -0
  146. package/scripts/harness-api.frozen.json +0 -2
  147. package/scripts/layer-legacy-frozen.d.mts +9 -1
  148. package/scripts/layer-legacy-frozen.mjs +28 -11
  149. package/scripts/layer-legacy-frozen.test.ts +143 -1
  150. package/scripts/rules-are-content-only.harness.mjs +3 -11
  151. package/scripts/rules-are-content-only.mjs +29 -19
  152. package/scripts/rules-are-content-only.test.mjs +61 -0
  153. package/scripts/rules-see-files.mjs +11 -9
  154. package/scripts/rules-see-files.test.mjs +71 -0
  155. package/scripts/test.mjs +44 -0
  156. package/scripts/test.test.mjs +52 -0
  157. package/skills/grade-paper-writing/__snapshots__/prose-lint.test.mjs.snap +57 -0
  158. package/skills/grade-paper-writing/prose-lint.mjs +31 -12
  159. package/skills/grade-paper-writing/prose-lint.test.mjs +153 -0
  160. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +3 -3
  161. package/skills/paper-pipeline/scripts/README.md +13 -132
  162. package/skills/paper-pipeline/scripts/announce.test.mjs +102 -0
  163. package/skills/paper-pipeline/scripts/artifact-coverage.test.mjs +86 -0
  164. package/skills/paper-pipeline/scripts/consumer.d.mts +8 -1
  165. package/skills/paper-pipeline/scripts/consumer.harness.mjs +3 -3
  166. package/skills/paper-pipeline/scripts/consumer.mjs +13 -25
  167. package/skills/paper-pipeline/scripts/consumer.test.mjs +113 -0
  168. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +9 -4
  169. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +380 -0
  170. package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +71 -0
  171. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +4 -4
  172. package/skills/paper-pipeline/scripts/generated-code.mjs +3 -2
  173. package/skills/paper-pipeline/scripts/generated-code.test.mjs +62 -0
  174. package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +302 -0
  175. package/skills/paper-pipeline/scripts/ledger.mjs +1 -1
  176. package/skills/paper-pipeline/scripts/{ledger.selftest.mjs → ledger.test.mjs} +44 -76
  177. package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +474 -0
  178. package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -63
  179. package/skills/paper-pipeline/scripts/pipeline-check.mjs +37 -21
  180. package/skills/paper-pipeline/scripts/pipeline-edges.mjs +2 -2
  181. package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +73 -0
  182. package/skills/paper-pipeline/scripts/population-map.harness.mjs +4 -47
  183. package/skills/paper-pipeline/scripts/population-map.mjs +12 -2
  184. package/skills/paper-pipeline/scripts/population-map.test.mjs +154 -0
  185. package/skills/paper-pipeline/scripts/provenance.harness.mjs +26 -0
  186. package/skills/paper-pipeline/scripts/round-diff.harness.mjs +1 -2
  187. package/skills/paper-pipeline/scripts/round-diff.mjs +13 -6
  188. package/skills/paper-pipeline/scripts/round-diff.test.mjs +159 -0
  189. package/skills/paper-pipeline/scripts/run-mechanical.mjs +30 -6
  190. package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +468 -0
  191. package/skills/paper-pipeline/scripts/status.mjs +2 -2
  192. package/skills/paper-pipeline/scripts/status.test.mjs +232 -0
  193. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +2 -2
  194. package/skills/render-paper/extract-pdf-facts.harness.mjs +56 -7
  195. package/skills/tighten-paper/__snapshots__/structure.test.mjs.snap +133 -0
  196. package/skills/tighten-paper/structure.test.mjs +116 -0
  197. package/skills/verify-citations/SKILL.md +5 -5
  198. package/skills/verify-citations/SKILL.md.spec.ts +4 -4
  199. package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +235 -0
  200. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +274 -0
  201. package/skills/verify-citations/scripts/bib-authors.cli.test.mjs +178 -0
  202. package/skills/verify-citations/scripts/bib-authors.mjs +2 -1
  203. package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
  204. package/skills/verify-citations/scripts/verify-cites.mjs +96 -96
  205. package/skills/verify-citations/scripts/verify-cites.net.test.mjs +287 -0
  206. package/skills/verify-citations/scripts/verify-cites.test.mjs +149 -5
  207. package/src/adapters/banal/failure.test.ts +70 -1
  208. package/src/adapters/banal/index.test.ts +16 -0
  209. package/src/adapters/banal/output.test.ts +5 -0
  210. package/src/adapters/curl/download.test.ts +43 -1
  211. package/src/adapters/memory/index.test.ts +25 -0
  212. package/src/adapters/node/files.io.ts +3 -2
  213. package/src/adapters/node/files.test.ts +13 -1
  214. package/src/adapters/node/process.test.ts +38 -0
  215. package/src/adapters/pdfjs/fill.test.ts +34 -0
  216. package/src/adapters/pdfjs/fill.ts +44 -25
  217. package/src/adapters/references/index.test.ts +228 -0
  218. package/src/adapters/references/index.ts +21 -6
  219. package/src/adapters/references/pool.test.ts +31 -0
  220. package/src/adapters/references/pool.ts +24 -0
  221. package/src/build-engine.harness.mjs +3 -7
  222. package/src/build-engine.test.ts +137 -0
  223. package/src/build-engine.ts +6 -1
  224. package/src/build.harness.mjs +51 -11
  225. package/src/build.test.ts +362 -0
  226. package/src/build.ts +40 -14
  227. package/src/cli.harness.mjs +12 -19
  228. package/src/cli.test.ts +584 -0
  229. package/src/cli.ts +409 -94
  230. package/src/doctor.harness.mjs +3 -8
  231. package/src/doctor.test.ts +64 -0
  232. package/src/doctor.ts +12 -4
  233. package/src/domain/result.test.ts +4 -0
  234. package/src/domain/result.ts +0 -5
  235. package/src/engine.harness.mjs +77 -7
  236. package/src/engine.ts +35 -0
  237. package/src/facts-file.test.ts +110 -0
  238. package/src/hooks-settings.harness.mjs +43 -13
  239. package/src/hooks-settings.ts +41 -35
  240. package/src/init.test.ts +308 -5
  241. package/src/init.ts +4 -4
  242. package/src/latex-log.harness.mjs +32 -7
  243. package/src/latex-log.ts +6 -13
  244. package/src/latex-loop.harness.mjs +8 -7
  245. package/src/link-skills.harness.mjs +3 -8
  246. package/src/link-skills.test.ts +105 -0
  247. package/src/link-skills.ts +75 -21
  248. package/src/linted-set.test.ts +118 -0
  249. package/src/new-paper.harness.mjs +5 -7
  250. package/src/new-paper.test.ts +203 -2
  251. package/src/new-paper.ts +84 -17
  252. package/src/paper-files.test.ts +122 -5
  253. package/src/paper-files.ts +99 -39
  254. package/src/paper-settings-commands.test.ts +41 -5
  255. package/src/paper-settings.test.ts +20 -0
  256. package/src/parse-args.test.ts +48 -0
  257. package/src/pdf-facts.harness.mjs +3 -6
  258. package/src/pdf-facts.test.ts +144 -0
  259. package/src/pdf-facts.ts +17 -19
  260. package/src/pdf-geometry.harness.mjs +3 -7
  261. package/src/pdf-geometry.ts +1 -1
  262. package/src/presets.test.ts +2 -0
  263. package/src/presets.ts +7 -1
  264. package/src/references.test.ts +97 -0
  265. package/src/references.ts +2 -2
  266. package/src/rules-config.harness.mjs +10 -8
  267. package/src/rules-off.test.ts +111 -0
  268. package/src/structure.harness.mjs +5 -9
  269. package/src/structure.ts +1 -1
  270. package/src/tex-requirements.harness.mjs +34 -7
  271. package/src/tex-requirements.ts +12 -6
  272. package/src/toolchain.harness.mjs +44 -13
  273. package/src/toolchain.test.ts +280 -0
  274. package/src/toolchain.ts +37 -12
  275. package/src/types.ts +6 -0
  276. package/src/venue-rules.test.ts +63 -0
  277. package/src/venue-rules.ts +14 -12
  278. package/templates/paper/paperlint.json +1 -1
  279. package/action.mutations.mjs +0 -162
  280. package/eslint-rules/install-path-literals.mutations.mjs +0 -62
  281. package/eslint-rules/latex-language.mutations.mjs +0 -196
  282. package/eslint-rules/paper-research-question.mutations.mjs +0 -127
  283. package/eslint-rules/paper-stages.mutations.mjs +0 -81
  284. package/eslint-rules/papers.mutations.mjs +0 -186
  285. package/eslint-rules/temp-root-realpath.mutations.mjs +0 -99
  286. package/eslint-rules/tex-build.mutations.mjs +0 -258
  287. package/hooks/hooks.mutations.mjs +0 -337
  288. package/lib/agent-cli-version.mutations.mjs +0 -109
  289. package/lib/mutation-driver.harness.mjs +0 -227
  290. package/lib/mutation-driver.mjs +0 -397
  291. package/lib/mutation-driver.mutations.mjs +0 -68
  292. package/lib/paper-config.mutations.mjs +0 -143
  293. package/lib/skill-trigger-cases.mutations.mjs +0 -65
  294. package/scripts/check.mutations.mjs +0 -110
  295. package/scripts/exclusive.mjs +0 -138
  296. package/scripts/mutation-batteries-frozen.harness.mjs +0 -204
  297. package/scripts/mutation-batteries-frozen.mjs +0 -238
  298. package/scripts/mutation-batteries.frozen.json +0 -108
  299. package/scripts/rules-are-content-only.mutations.mjs +0 -81
  300. package/scripts/rules-see-files.mutations.mjs +0 -131
  301. package/scripts/run-mutations.mjs +0 -100
  302. package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +0 -218
  303. package/skills/paper-pipeline/scripts/consumer.mutations.mjs +0 -190
  304. package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +0 -54
  305. package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +0 -254
  306. package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +0 -54
  307. package/skills/paper-pipeline/scripts/population-map.mutations.mjs +0 -65
  308. package/skills/paper-pipeline/scripts/population-map.selftest.mjs +0 -122
  309. package/skills/paper-pipeline/scripts/provenance.mutations.mjs +0 -59
  310. package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +0 -276
  311. package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +0 -65
  312. package/skills/skill-contract.mutations.mjs +0 -179
  313. package/src/build.mutations.mjs +0 -196
  314. package/src/cli.mutations.mjs +0 -414
  315. package/src/doctor.mutations.mjs +0 -175
  316. package/src/hooks-settings.mutations.mjs +0 -99
  317. package/src/link-skills.mutations.mjs +0 -140
  318. package/src/new-paper.mutations.mjs +0 -79
  319. package/src/structure.mutations.mjs +0 -83
@@ -1,186 +0,0 @@
1
- /**
2
- * Mutation battery for `eslint-rules/papers.mjs`.
3
- * Run: `node eslint-rules/papers.mutations.mjs` (not `vigiles test` — this is not a harness).
4
- *
5
- * 🔴 EVERY MUTATION CARRIES A "THE PATCH LANDED" ASSERTION: the file is re-read FROM DISK, the
6
- * mutant must be in it, and after the rollback the contents must return byte for byte. In this
7
- * project a green mutation has repeatedly meant not "the defence holds" but "the patch never
8
- * landed" / "the test is looking somewhere else".
9
- *
10
- * WHAT A GREEN HARNESS UNDER A MUTATION MEANS: a finding about the TEST, not a conclusion
11
- * about the defence. Rank the causes in this order — the test greps PROSE ABOUT a value
12
- * instead of the value · the test looks in the wrong place · the test is the wrong test · the
13
- * patch did not land · the set is too narrow · the run was already red BEFORE the mutation.
14
- * The last one is closed by the baseline run below.
15
- */
16
- import { readFileSync, writeFileSync } from "node:fs";
17
- import { execFileSync } from "node:child_process";
18
-
19
- const RULES = "eslint-rules/papers.mjs";
20
- const HARNESS = "eslint-rules/papers.harness.mjs";
21
-
22
- // Coverage mode — see `scripts/run-mutations.mjs` guard 2 and the block in
23
- // `lib/mutation-driver.mjs`. Says what this battery can kill, without touching a file.
24
- if (process.env.MUTATIONS_REPORT_COVERAGE) {
25
- console.log(`MUTATION-COVERS\t${HARNESS}`);
26
- process.exit(0);
27
- }
28
-
29
- const PRISTINE = readFileSync(RULES, "utf8");
30
-
31
- const M = [
32
- [
33
- "existsSync/the central refusal",
34
- "stop checking that the root is on disk — a wrong root stops being an error and becomes a " +
35
- "run that lints zero files and exits 0, which reads exactly like a clean pass",
36
- " if (!existsSync(resolve(baseDir, root)))",
37
- " if (false)",
38
- ],
39
- [
40
- "baseDir/ignored in favour of cwd",
41
- "resolve against `process.cwd()` instead of the given base — passes every run started from " +
42
- "the repository root and fails in a worktree or from an editor started elsewhere",
43
- " if (!existsSync(resolve(baseDir, root)))",
44
- " if (!existsSync(resolve(process.cwd(), root)))",
45
- ],
46
- [
47
- "default/absence",
48
- "drop the default — a consumer that declares nothing gets `undefined` instead of `papers`",
49
- " const root = declared === undefined ? DEFAULT_PAPERS_ROOT : declared;",
50
- " const root = declared;",
51
- ],
52
- [
53
- "declared === undefined/null read as absence",
54
- 'go back to `??` — an explicit `"papersDir": null` is read as «nothing was declared» and ' +
55
- "silently falls back to the default, i.e. a typed keystroke treated as an absence",
56
- " const root = declared === undefined ? DEFAULT_PAPERS_ROOT : declared;",
57
- " const root = declared ?? DEFAULT_PAPERS_ROOT;",
58
- ],
59
- [
60
- "type guard/empty string",
61
- "accept any string, including the empty one — every glob becomes `/*/paper.tex`, rooted at " +
62
- "the filesystem, matching nothing, silently",
63
- ' if (typeof root !== "string" || root.length === 0)',
64
- ' if (typeof root !== "string")',
65
- ],
66
- [
67
- "return shape/absolute path",
68
- "return the RESOLVED root — ESLint interprets `files:` globs relative to the config, so an " +
69
- "absolute path there is a different pattern, and the breakage shows up as silence",
70
- " return root;\n}",
71
- " return resolve(baseDir, root);\n}",
72
- ],
73
- [
74
- "message/which value was declared",
75
- "drop the declared value from the error — the reader is told something is missing but not " +
76
- "which of several candidate paths the tool was looking at",
77
- 'the papers root "${root}" does not exist under ${baseDir}.',
78
- "the papers root does not exist.",
79
- ],
80
- [
81
- "message/default vs declared",
82
- "collapse the two failures into one message — «you declared the wrong thing» and «you " +
83
- "declared nothing and the default does not fit» have different fixes",
84
- " (declared === undefined",
85
- " (false",
86
- ],
87
- [
88
- "paperFiles/one glob set stops interpolating the root",
89
- "hard-code the `tex` set — the block keeps matching whatever layout the author had in mind",
90
- " tex: [`${root}/*/paper.tex`],",
91
- " tex: [`docs/papers/*/paper.tex`],",
92
- ],
93
- [
94
- "paperFiles/a set disappears",
95
- "drop `status` — a consumer's block named it, so that block now lints nothing at all",
96
- " status: [`${root}/*/PIPELINE-STATUS.md`],",
97
- "",
98
- ],
99
- ];
100
-
101
- const runHarness = () => {
102
- try {
103
- execFileSync("npx", ["vigiles", "test", HARNESS], {
104
- encoding: "utf8",
105
- stdio: ["ignore", "pipe", "pipe"],
106
- });
107
- return { failed: false, out: "" };
108
- } catch (e) {
109
- return { failed: true, out: (e.stdout ?? "") + (e.stderr ?? "") };
110
- }
111
- };
112
-
113
- // 🔴 A BASELINE RUN BEFORE ANY MUTATION. A red harness makes EVERY mutation look "killed", and
114
- // the battery would print a confident green verdict about a defence that is not there.
115
- {
116
- const base = runHarness();
117
- if (base.failed) {
118
- console.log(
119
- "❌ THE HARNESS IS RED BEFORE ANY MUTATION — the battery cannot tell a killed mutation " +
120
- "from that:\n" +
121
- base.out.slice(-1500),
122
- );
123
- process.exit(1);
124
- }
125
- }
126
-
127
- let ok = 0;
128
- let bad = 0;
129
- const rows = [];
130
- for (const [label, what, from, to] of M) {
131
- const hits = PRISTINE.split(from).length - 1;
132
- if (hits !== 1) {
133
- console.log(
134
- `❌ ${label}: TARGET ${hits === 0 ? "NOT FOUND" : `NOT UNIQUE (${hits})`} — ${from.slice(0, 70)}`,
135
- );
136
- bad++;
137
- continue;
138
- }
139
- writeFileSync(RULES, PRISTINE.replace(from, to));
140
- const on = readFileSync(RULES, "utf8");
141
- if (!on.includes(to) || on.includes(from)) {
142
- console.log(
143
- `❌ ${label}: THE MUTATION DID NOT LAND (checked by re-reading the file)`,
144
- );
145
- bad++;
146
- writeFileSync(RULES, PRISTINE);
147
- continue;
148
- }
149
- const { failed, out } = runHarness();
150
- writeFileSync(RULES, PRISTINE);
151
- if (readFileSync(RULES, "utf8") !== PRISTINE) {
152
- console.log(`❌ ${label}: THE ROLLBACK FAILED`);
153
- bad++;
154
- continue;
155
- }
156
- // The verdict carries the HARNESS LINE and the assertion text: "killed" without saying by
157
- // what is half an answer. The battery must show that each mutation died on ITS OWN assertion.
158
- const at = (out.match(/papers\.harness\.mjs:(\d+)/) ?? [])[1];
159
- const msg =
160
- (out.match(/AssertionError[^:]*: ([^\n]+)/) ?? [])[1] ??
161
- // Not every mutation dies on an assertion: removing the `try/catch` crashes the linter
162
- // itself, and then "what it died on" is a thrown error. Without this branch the column
163
- // would come out EMPTY — "killed by something unknown", which is the half-answer this
164
- // battery forbids.
165
- (out.match(/((?:Error|ENOENT)[^\n]*)/) ?? [])[1] ??
166
- (out.match(
167
- /([^\n]*(?:must|the wrong|drifted|was lost|did not fire)[^\n]*)/,
168
- ) ?? [])[1] ??
169
- "";
170
- const where = (at ? `harness:${at} · ` : "") + msg;
171
- if (failed) {
172
- ok++;
173
- rows.push([label, what, where.trim().slice(0, 130)]);
174
- } else {
175
- console.log(
176
- `🔴 ${label}: THE HARNESS IS GREEN UNDER THE MUTATION — a finding about the TEST, not a conclusion about the defence`,
177
- );
178
- bad++;
179
- }
180
- }
181
- console.log("\n| property | mutation | what the harness died on |");
182
- console.log("|---|---|---|");
183
- for (const [a, b, c] of rows)
184
- console.log(`| \`${a}\` | ${b} | ${c.replace(/\|/g, "\\|")} |`);
185
- console.log(`\n${ok} mutation(s) killed, ${bad} problem(s)`);
186
- process.exit(bad ? 1 : 0);
@@ -1,99 +0,0 @@
1
- /**
2
- * Battery for `local/temp-root-realpath`. Five mutations, five DIFFERENT asserts: the rule
3
- * must fire, must exempt a resolved root, must not scream at a nested one, must understand
4
- * the namespaced spelling — and, separately, case 6 must catch a REGRESSION IN THE CORPUS,
5
- * not just in a made-up string.
6
- *
7
- * 🔴 The last mutation is the only thing that makes issue #9's fix guarded. The defect
8
- * cannot be reproduced on Linux: removing the `realpathSync` wrapper leaves all 52 harnesses
9
- * green. If removing it turns NOTHING red, then the twenty-four fixes rest entirely on the
10
- * next author's memory.
11
- */
12
- import { fileURLToPath } from "node:url";
13
- import { dirname, join, resolve } from "node:path";
14
- import { runMutations } from "../lib/mutation-driver.mjs";
15
-
16
- const HERE = dirname(fileURLToPath(import.meta.url));
17
- const ROOT = resolve(HERE, "..");
18
- const RULE = join(HERE, "temp-root-realpath.mjs");
19
- const HARNESS = join(HERE, "temp-root-realpath.harness.mjs");
20
- const CORPUS = join(
21
- ROOT,
22
- "skills",
23
- "paper-pipeline",
24
- "scripts",
25
- "consumer.harness.mjs",
26
- );
27
-
28
- process.exit(
29
- runMutations({
30
- root: ROOT,
31
- runner: "node",
32
- cases: [
33
- {
34
- name: "the rule stops reporting",
35
- harness: HARNESS,
36
- expect: "one finding was expected",
37
- disables: "the verdict itself — an unresolved root passes silently",
38
- edits: [
39
- [
40
- RULE,
41
- 'context.report({ node, messageId: "unresolved" });',
42
- "void node;",
43
- ],
44
- ],
45
- },
46
- {
47
- name: "the realpathSync exemption is removed",
48
- harness: HARNESS,
49
- expect: "the rule must stay silent",
50
- disables:
51
- "the exemption — an already-fixed root starts going red, and for an error-level rule a false positive is worse than a miss",
52
- edits: [
53
- [
54
- RULE,
55
- "if (isRealpathCall(parent) && parent.arguments[0] === node) return;",
56
- "void parent;",
57
- ],
58
- ],
59
- },
60
- {
61
- name: "the 'tmpdir() is in the arguments' condition is removed",
62
- harness: HARNESS,
63
- expect: "a nested root inherits its spelling from the parent",
64
- disables:
65
- "narrowing the predicate — the rule starts demanding a resolve on nested roots too",
66
- edits: [
67
- [RULE, "if (!mentionsTmpdir(node.arguments)) return;", "void node;"],
68
- ],
69
- },
70
- {
71
- name: "the namespaced spelling stops being recognized",
72
- harness: HARNESS,
73
- expect: "the namespaced spelling must be caught",
74
- disables: "parsing of `fs.mkdtempSync` — the defect hides behind a dot",
75
- edits: [
76
- [
77
- RULE,
78
- 'if (\n c.type === "MemberExpression" &&\n !c.computed &&\n c.property.type === "Identifier"\n )\n return c.property.name;',
79
- 'if (false) return "";',
80
- ],
81
- ],
82
- },
83
- {
84
- name: "🔴 REGRESSION IN THE CORPUS: the wrapper is removed from one real root",
85
- harness: HARNESS,
86
- expect: "the corpus must be clean of unresolved roots",
87
- disables:
88
- "the guard over the #9 fix itself — on Linux removing the wrapper does not turn a single behavioral test red",
89
- edits: [
90
- [
91
- CORPUS,
92
- 'const TMP = realpathSync(mkdtempSync(join(tmpdir(), "consumer-harness-")));',
93
- 'const TMP = mkdtempSync(join(tmpdir(), "consumer-harness-"));',
94
- ],
95
- ],
96
- },
97
- ],
98
- }),
99
- );
@@ -1,258 +0,0 @@
1
- /**
2
- * Mutation battery for `eslint-rules/tex-build.mjs`.
3
- * Run: `node eslint-rules/tex-build.mutations.mjs` (not `vigiles test` — this is not a harness).
4
- *
5
- * 🔴 EVERY MUTATION CARRIES A "THE PATCH LANDED" ASSERTION: the file is re-read FROM DISK, the
6
- * mutant must be in it, and after the rollback the contents must return byte for byte. In the
7
- * repository this came from, a green mutation has FOUR TIMES meant not "the defence holds" but
8
- * "the patch never landed" / "the test is looking somewhere else".
9
- *
10
- * WHAT A GREEN HARNESS UNDER A MUTATION MEANS: it is a finding about the TEST, not a conclusion
11
- * about the defence. Rank the causes in this order — the test greps PROSE ABOUT a value instead
12
- * of the value · the test looks in the wrong place · the test is the wrong test · the patch did
13
- * not land · the set is too narrow · the run was already red BEFORE the mutation. The last one
14
- * is closed by the baseline run below.
15
- */
16
- import { readFileSync, writeFileSync } from "node:fs";
17
- import { execFileSync } from "node:child_process";
18
-
19
- const RULES = "eslint-rules/tex-build.mjs";
20
- const HARNESS = "eslint-rules/tex-build.harness.mjs";
21
-
22
- // Coverage mode — see `scripts/run-mutations.mjs` guard 2 and the block in
23
- // `lib/mutation-driver.mjs`. Says what this battery can kill, without touching a file.
24
- if (process.env.MUTATIONS_REPORT_COVERAGE) {
25
- console.log(`MUTATION-COVERS\t${HARNESS}`);
26
- process.exit(0);
27
- }
28
-
29
- const PRISTINE = readFileSync(RULES, "utf8");
30
-
31
- const M = [
32
- [
33
- "vocabulary/camera-ready",
34
- "remove the «at camera-ready» branch — the very one the check was added for on 2026-08-24",
35
- " /\\b(?:at (?:the )?camera[-\\s]?ready|upon acceptance|on acceptance|will be (?:released|published|made (?:publicly )?available|public|open[-\\s]?sourced))\\b/i;",
36
- " /\\b(?:upon acceptance|on acceptance|will be (?:released|published|made (?:publicly )?available|public|open[-\\s]?sourced))\\b/i; /* MUT */",
37
- ],
38
- [
39
- "vocabulary/will be …",
40
- "keep only camera-ready — the «will be released/published/public» family dies",
41
- " /\\b(?:at (?:the )?camera[-\\s]?ready|upon acceptance|on acceptance|will be (?:released|published|made (?:publicly )?available|public|open[-\\s]?sourced))\\b/i;",
42
- " /\\b(?:at (?:the )?camera[-\\s]?ready)\\b/i; /* MUT */",
43
- ],
44
- [
45
- "review/exemption",
46
- "stop exempting a review build — a promise made during peer review becomes a finding",
47
- " if (REVIEW_MODE_RE.test(text)) return;",
48
- " if (false) return; /* MUT */",
49
- ],
50
- [
51
- "review/anchor against comments",
52
- "drop `^[^%\\n]*` — a COMMENTED-OUT `\\documentclass[review]` starts exempting the build " +
53
- "(a live case in the source corpus: a paper carrying its previous class a few lines above the real one)",
54
- " /^[^%\\n]*(?:\\\\documentclass\\[[^\\]]*\\breview\\b|printacmref=false)/m;",
55
- " /(?:\\\\documentclass\\[[^\\]]*\\breview\\b|printacmref=false)/m; /* MUT */",
56
- ],
57
- [
58
- "review/printacmref",
59
- "remove the second sign of the mode — a build with `printacmref=false` stops being exempt",
60
- " /^[^%\\n]*(?:\\\\documentclass\\[[^\\]]*\\breview\\b|printacmref=false)/m;",
61
- " /^[^%\\n]*(?:\\\\documentclass\\[[^\\]]*\\breview\\b)/m; /* MUT */",
62
- ],
63
- [
64
- "comments/blanking",
65
- "stop cutting LaTeX comments — an author note `% camera-ready blocker` becomes a finding",
66
- ' .map((l) => l.replace(/(^|[^\\\\])%.*$/, "$1"));\n lines.forEach((line, i) => {',
67
- " .map((l) => l); /* MUT */\n lines.forEach((line, i) => {",
68
- ],
69
- [
70
- "comments/escaped percent",
71
- "treat `\\%` as the start of a comment — the rest of the line behind the percent goes invisible",
72
- ' .map((l) => l.replace(/(^|[^\\\\])%.*$/, "$1"));\n lines.forEach((line, i) => {',
73
- ' .map((l) => l.replace(/%.*$/, "")); /* MUT */\n lines.forEach((line, i) => {',
74
- ],
75
- [
76
- "address/line",
77
- "report every finding on line 1 — the address, which is what the move to a lint rule bought, is lost",
78
- " loc: {\n start: { line: i + 1, column },",
79
- " loc: {\n start: { line: 1, column: 1 }, /* MUT */",
80
- ],
81
- [
82
- "address/column",
83
- "report the start of the line instead of the start of the match",
84
- " if (!m) return;\n const column = m.index + 1;",
85
- " if (!m) return;\n const column = 1; /* MUT */",
86
- ],
87
- [
88
- "capture/finding text",
89
- "print the whole line in the message instead of the captured substring — «how many» would still match, «what» would not",
90
- " data: { text: m[0] },",
91
- " data: { text: line.trim() }, /* MUT */",
92
- ],
93
- // ── tex/acm-frontmatter-override ───────────────────────────────────────────
94
- [
95
- "frontmatter/acmart guard",
96
- "drop the `acmart` guard — `\\pagestyle{plain}` in an ordinary `article` becomes a finding, " +
97
- "which is the false positive that gets a rule switched off on its first day",
98
- " if (!ACMART_RE.test(code)) return;",
99
- " if (false) return; /* MUT */",
100
- ],
101
- [
102
- "frontmatter/nonacm exemption",
103
- "stop exempting `nonacm` — the rule punishes the very escape hatch its own message recommends",
104
- " if (NONACM_RE.test(code)) return;",
105
- " if (false) return; /* MUT */",
106
- ],
107
- [
108
- "frontmatter/setcopyright",
109
- "remove `\\setcopyright{none}` from the set — the first of the three lines of the " +
110
- "desk-rejected preamble stops being caught",
111
- " re: /\\\\setcopyright\\s*\\{\\s*none\\s*\\}/g,",
112
- " re: /\\\\setcopyright\\s*\\{\\s*NEVER\\s*\\}/g, /* MUT */",
113
- ],
114
- [
115
- "frontmatter/permission footnote",
116
- "remove the `\\renewcommand\\footnotetextcopyrightpermission` branch — the permission block " +
117
- "on page 1 can be blanked silently",
118
- " re: /\\\\renewcommand\\s*\\*?\\s*\\{?\\s*\\\\footnotetextcopyrightpermission/g,",
119
- " re: /\\\\renewcommand\\s*\\*?\\s*\\{?\\s*\\\\NEVERfootnotetext/g, /* MUT */",
120
- ],
121
- [
122
- "frontmatter/pagestyle empty",
123
- "narrow the page-style branch to `plain` — `\\pagestyle{empty}` strips the same furniture and " +
124
- "goes unnoticed",
125
- " re: /\\\\(?:this)?pagestyle\\s*\\{\\s*(?:plain|empty)\\s*\\}/g,",
126
- " re: /\\\\(?:this)?pagestyle\\s*\\{\\s*(?:plain)\\s*\\}/g, /* MUT */",
127
- ],
128
- [
129
- "frontmatter/thispagestyle",
130
- "drop `(?:this)?` — `\\thispagestyle{empty}`, which targets page 1 SPECIFICALLY, stops being caught",
131
- " re: /\\\\(?:this)?pagestyle\\s*\\{\\s*(?:plain|empty)\\s*\\}/g,",
132
- " re: /\\\\pagestyle\\s*\\{\\s*(?:plain|empty)\\s*\\}/g, /* MUT */",
133
- ],
134
- [
135
- "frontmatter/comment blanking",
136
- "stop cutting LaTeX comments — the accepted sibling paper's COMMENTED record of its old " +
137
- "preamble becomes three findings, and the arming `\\documentclass` may come from a comment too",
138
- ' .map((l) => l.replace(/(^|[^\\\\])%.*$/, "$1"));\n const code = lines.join("\\n");',
139
- ' .map((l) => l);\n const code = lines.join("\\n"); /* MUT */',
140
- ],
141
- // ⚠️ `frontmatter/lastIndex reset` STOOD HERE AND WAS REMOVED, not silenced. It mutated away
142
- // an explicit `re.lastIndex = 0` and the harness stayed GREEN — which by this file's own
143
- // ranking is a finding about the TEST or about the CODE, never a pass. It was the code: a
144
- // `/g` `exec` that returns `null` resets `lastIndex` itself, and the loop always drains the
145
- // regex, so the line was a guard nothing could kill. The line is gone; the property is held
146
- // by the harness assertion that lints one input twice in a single process.
147
- [
148
- "frontmatter/line",
149
- "report every finding on line 1 — the address of the offending preamble line is lost",
150
- " start: { line: i + 1, column },",
151
- " start: { line: 1, column: 1 }, /* MUT */",
152
- ],
153
- [
154
- "frontmatter/all matches per line",
155
- "take only the FIRST match of each pattern per line — a compacted preamble is under-counted",
156
- " while ((m = re.exec(line)) !== null) {",
157
- " if ((m = re.exec(line)) !== null) { /* MUT */",
158
- ],
159
- [
160
- "frontmatter/column",
161
- "report the start of the line instead of the start of the match (second rule's copy)",
162
- " const column = m.index + 1;",
163
- " const column = 1; /* MUT */",
164
- ],
165
- [
166
- "disk read/quiet instead of throwing",
167
- "remove the `try/catch` around the read — the rule THROWS on a stdin run where there is no file",
168
- ' try {\n text = readFileSync(context.filename, "utf8");\n } catch {\n return;\n }\n if (REVIEW_MODE_RE.test(text)) return;',
169
- ' text = readFileSync(context.filename, "utf8"); /* MUT */\n if (REVIEW_MODE_RE.test(text)) return;',
170
- ],
171
- ];
172
-
173
- const runHarness = () => {
174
- try {
175
- execFileSync("npx", ["vigiles", "test", HARNESS], {
176
- encoding: "utf8",
177
- stdio: ["ignore", "pipe", "pipe"],
178
- });
179
- return { failed: false, out: "" };
180
- } catch (e) {
181
- return { failed: true, out: (e.stdout ?? "") + (e.stderr ?? "") };
182
- }
183
- };
184
-
185
- // 🔴 A BASELINE RUN BEFORE ANY MUTATION. A red harness makes EVERY mutation look "killed", and
186
- // the battery would print a confident green verdict about a defence that is not there.
187
- {
188
- const base = runHarness();
189
- if (base.failed) {
190
- console.log(
191
- "❌ THE HARNESS IS RED BEFORE ANY MUTATION — the battery cannot tell a killed mutation " +
192
- "from that:\n" +
193
- base.out.slice(-1500),
194
- );
195
- process.exit(1);
196
- }
197
- }
198
-
199
- let ok = 0;
200
- let bad = 0;
201
- const rows = [];
202
- for (const [label, what, from, to] of M) {
203
- const hits = PRISTINE.split(from).length - 1;
204
- if (hits !== 1) {
205
- console.log(
206
- `❌ ${label}: TARGET ${hits === 0 ? "NOT FOUND" : `NOT UNIQUE (${hits})`} — ${from.slice(0, 70)}`,
207
- );
208
- bad++;
209
- continue;
210
- }
211
- writeFileSync(RULES, PRISTINE.replace(from, to));
212
- const on = readFileSync(RULES, "utf8");
213
- if (!on.includes(to) || on.includes(from)) {
214
- console.log(
215
- `❌ ${label}: THE MUTATION DID NOT LAND (checked by re-reading the file)`,
216
- );
217
- bad++;
218
- writeFileSync(RULES, PRISTINE);
219
- continue;
220
- }
221
- const { failed, out } = runHarness();
222
- writeFileSync(RULES, PRISTINE);
223
- if (readFileSync(RULES, "utf8") !== PRISTINE) {
224
- console.log(`❌ ${label}: THE ROLLBACK FAILED`);
225
- bad++;
226
- continue;
227
- }
228
- // The verdict carries the HARNESS LINE and the assertion text: "killed" without saying by
229
- // what is half an answer. The battery must show that each mutation died on ITS OWN assertion.
230
- const at = (out.match(/tex-build\.harness\.mjs:(\d+)/) ?? [])[1];
231
- const msg =
232
- (out.match(/AssertionError[^:]*: ([^\n]+)/) ?? [])[1] ??
233
- // Not every mutation dies on an assertion: removing the `try/catch` crashes the linter
234
- // itself, and then "what it died on" is a thrown error. Without this branch the column
235
- // would come out EMPTY — "killed by something unknown", which is the half-answer this
236
- // battery forbids.
237
- (out.match(/((?:Error|ENOENT)[^\n]*)/) ?? [])[1] ??
238
- (out.match(
239
- /([^\n]*(?:must|the wrong|drifted|was lost|did not fire)[^\n]*)/,
240
- ) ?? [])[1] ??
241
- "";
242
- const where = (at ? `harness:${at} · ` : "") + msg;
243
- if (failed) {
244
- ok++;
245
- rows.push([label, what, where.trim().slice(0, 130)]);
246
- } else {
247
- console.log(
248
- `🔴 ${label}: THE HARNESS IS GREEN UNDER THE MUTATION — a finding about the TEST, not a conclusion about the defence`,
249
- );
250
- bad++;
251
- }
252
- }
253
- console.log("\n| property | mutation | what the harness died on |");
254
- console.log("|---|---|---|");
255
- for (const [a, b, c] of rows)
256
- console.log(`| \`${a}\` | ${b} | ${c.replace(/\|/g, "\\|")} |`);
257
- console.log(`\n${ok} mutation(s) killed, ${bad} problem(s)`);
258
- process.exit(bad ? 1 : 0);