paperlint 3.1.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 (284) 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/bin/paperlint.test.mjs +45 -0
  6. package/dist/adapters/node/files.io.d.ts +2 -0
  7. package/dist/adapters/node/files.io.d.ts.map +1 -1
  8. package/dist/adapters/node/files.io.js +3 -2
  9. package/dist/adapters/node/files.io.js.map +1 -1
  10. package/dist/adapters/pdfjs/fill.d.ts +15 -4
  11. package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
  12. package/dist/adapters/pdfjs/fill.js +31 -21
  13. package/dist/adapters/pdfjs/fill.js.map +1 -1
  14. package/dist/adapters/references/index.d.ts.map +1 -1
  15. package/dist/adapters/references/index.js +14 -5
  16. package/dist/adapters/references/index.js.map +1 -1
  17. package/dist/adapters/references/pool.d.ts +8 -0
  18. package/dist/adapters/references/pool.d.ts.map +1 -0
  19. package/dist/adapters/references/pool.js +19 -0
  20. package/dist/adapters/references/pool.js.map +1 -0
  21. package/dist/build.d.ts +10 -2
  22. package/dist/build.d.ts.map +1 -1
  23. package/dist/build.js +7 -8
  24. package/dist/build.js.map +1 -1
  25. package/dist/cli.d.ts +27 -1
  26. package/dist/cli.d.ts.map +1 -1
  27. package/dist/cli.js +71 -55
  28. package/dist/cli.js.map +1 -1
  29. package/dist/doctor.d.ts +27 -1
  30. package/dist/doctor.d.ts.map +1 -1
  31. package/dist/doctor.js +4 -3
  32. package/dist/doctor.js.map +1 -1
  33. package/dist/domain/result.d.ts +0 -2
  34. package/dist/domain/result.d.ts.map +1 -1
  35. package/dist/domain/result.js +0 -4
  36. package/dist/domain/result.js.map +1 -1
  37. package/dist/hooks-settings.d.ts +8 -5
  38. package/dist/hooks-settings.d.ts.map +1 -1
  39. package/dist/hooks-settings.js +32 -32
  40. package/dist/hooks-settings.js.map +1 -1
  41. package/dist/init.d.ts +8 -0
  42. package/dist/init.d.ts.map +1 -1
  43. package/dist/init.js +3 -3
  44. package/dist/init.js.map +1 -1
  45. package/dist/latex-log.d.ts +0 -2
  46. package/dist/latex-log.d.ts.map +1 -1
  47. package/dist/latex-log.js +7 -13
  48. package/dist/latex-log.js.map +1 -1
  49. package/dist/link-skills.d.ts +15 -2
  50. package/dist/link-skills.d.ts.map +1 -1
  51. package/dist/link-skills.js +36 -20
  52. package/dist/link-skills.js.map +1 -1
  53. package/dist/paper-files.d.ts.map +1 -1
  54. package/dist/paper-files.js +6 -4
  55. package/dist/paper-files.js.map +1 -1
  56. package/dist/pdf-facts.d.ts +11 -0
  57. package/dist/pdf-facts.d.ts.map +1 -1
  58. package/dist/pdf-facts.js +16 -18
  59. package/dist/pdf-facts.js.map +1 -1
  60. package/dist/pdf-geometry.js +1 -1
  61. package/dist/pdf-geometry.js.map +1 -1
  62. package/dist/presets.d.ts +2 -0
  63. package/dist/presets.d.ts.map +1 -1
  64. package/dist/presets.js +3 -1
  65. package/dist/presets.js.map +1 -1
  66. package/dist/references.js +2 -2
  67. package/dist/references.js.map +1 -1
  68. package/dist/tex-requirements.d.ts +8 -0
  69. package/dist/tex-requirements.d.ts.map +1 -1
  70. package/dist/tex-requirements.js +6 -5
  71. package/dist/tex-requirements.js.map +1 -1
  72. package/dist/toolchain.d.ts +3 -2
  73. package/dist/toolchain.d.ts.map +1 -1
  74. package/dist/toolchain.js +4 -5
  75. package/dist/toolchain.js.map +1 -1
  76. package/dist/venue-rules.d.ts.map +1 -1
  77. package/dist/venue-rules.js +11 -9
  78. package/dist/venue-rules.js.map +1 -1
  79. package/docs/prior-art/README.md +1 -1
  80. package/docs/prior-art/test-tooling.md +7 -30
  81. package/eslint-rules/install-path-literals.harness.mjs +17 -1
  82. package/eslint-rules/latex-language.harness.mjs +3 -3
  83. package/eslint-rules/latex-language.mjs +12 -9
  84. package/eslint-rules/latex-language.test.mjs +200 -0
  85. package/eslint-rules/paper-research-question.harness.mjs +73 -7
  86. package/eslint-rules/paper-stages.harness.mjs +9 -14
  87. package/eslint-rules/paper-stages.test.mjs +135 -0
  88. package/eslint-rules/paper-typography.mjs +9 -2
  89. package/eslint-rules/paper-typography.test.mjs +146 -1
  90. package/eslint-rules/pdf-last-page-balance.harness.mjs +63 -7
  91. package/eslint-rules/review-frontmatter.mjs +2 -1
  92. package/eslint-rules/review-frontmatter.test.mjs +37 -1
  93. package/eslint-rules/temp-root-realpath.harness.mjs +49 -0
  94. package/eslint-rules/tex-build.mjs +13 -13
  95. package/eslint.config.mjs +6 -2
  96. package/hooks/hooks.harness.mjs +6 -11
  97. package/hooks/paper-edit-guard.hook.mjs +2 -2
  98. package/hooks/paper-edit-guard.test.mjs +61 -0
  99. package/hooks/paper-skills-nudge.hook.mjs +2 -2
  100. package/hooks/paper-skills-nudge.test.mjs +38 -0
  101. package/hooks/paper-status-gates.test.mjs +42 -0
  102. package/lib/agent-cli-version.harness.mjs +7 -0
  103. package/lib/check.d.mts +17 -0
  104. package/lib/check.mjs +50 -0
  105. package/lib/check.test.ts +67 -0
  106. package/lib/markdown.mjs +16 -69
  107. package/lib/markdown.test.mjs +246 -0
  108. package/lib/paper-config.harness.mjs +28 -9
  109. package/lib/paper-config.mjs +7 -2
  110. package/lib/skill-checks.mjs +18 -46
  111. package/lib/skill-checks.test.mjs +297 -0
  112. package/lib/skill-corpus.mjs +11 -13
  113. package/lib/skill-corpus.test.mjs +54 -0
  114. package/lib/skill-trigger-cases.harness.mjs +57 -4
  115. package/package.json +6 -5
  116. package/scripts/check.harness.mjs +4 -10
  117. package/scripts/check.mjs +45 -42
  118. package/scripts/check.test.mjs +104 -0
  119. package/scripts/coverage-config.test.ts +406 -0
  120. package/scripts/eslint-report-guard.test.mjs +48 -0
  121. package/scripts/harness-api.frozen.json +0 -2
  122. package/scripts/layer-legacy-frozen.d.mts +9 -1
  123. package/scripts/layer-legacy-frozen.mjs +28 -11
  124. package/scripts/layer-legacy-frozen.test.ts +143 -1
  125. package/scripts/rules-are-content-only.harness.mjs +3 -11
  126. package/scripts/rules-are-content-only.mjs +29 -19
  127. package/scripts/rules-are-content-only.test.mjs +61 -0
  128. package/scripts/rules-see-files.mjs +11 -9
  129. package/scripts/rules-see-files.test.mjs +71 -0
  130. package/scripts/test.mjs +44 -0
  131. package/scripts/test.test.mjs +52 -0
  132. package/skills/grade-paper-writing/__snapshots__/prose-lint.test.mjs.snap +57 -0
  133. package/skills/grade-paper-writing/prose-lint.mjs +31 -12
  134. package/skills/grade-paper-writing/prose-lint.test.mjs +153 -0
  135. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +3 -3
  136. package/skills/paper-pipeline/scripts/README.md +13 -132
  137. package/skills/paper-pipeline/scripts/announce.test.mjs +102 -0
  138. package/skills/paper-pipeline/scripts/artifact-coverage.test.mjs +86 -0
  139. package/skills/paper-pipeline/scripts/consumer.d.mts +8 -1
  140. package/skills/paper-pipeline/scripts/consumer.harness.mjs +3 -3
  141. package/skills/paper-pipeline/scripts/consumer.mjs +13 -25
  142. package/skills/paper-pipeline/scripts/consumer.test.mjs +113 -0
  143. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +9 -4
  144. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +380 -0
  145. package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +71 -0
  146. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +4 -4
  147. package/skills/paper-pipeline/scripts/generated-code.mjs +3 -2
  148. package/skills/paper-pipeline/scripts/generated-code.test.mjs +62 -0
  149. package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +302 -0
  150. package/skills/paper-pipeline/scripts/ledger.mjs +1 -1
  151. package/skills/paper-pipeline/scripts/{ledger.selftest.mjs → ledger.test.mjs} +44 -76
  152. package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +474 -0
  153. package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -63
  154. package/skills/paper-pipeline/scripts/pipeline-check.mjs +37 -21
  155. package/skills/paper-pipeline/scripts/pipeline-edges.mjs +2 -2
  156. package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +73 -0
  157. package/skills/paper-pipeline/scripts/population-map.harness.mjs +4 -47
  158. package/skills/paper-pipeline/scripts/population-map.mjs +12 -2
  159. package/skills/paper-pipeline/scripts/population-map.test.mjs +154 -0
  160. package/skills/paper-pipeline/scripts/provenance.harness.mjs +26 -0
  161. package/skills/paper-pipeline/scripts/round-diff.harness.mjs +1 -2
  162. package/skills/paper-pipeline/scripts/round-diff.mjs +13 -6
  163. package/skills/paper-pipeline/scripts/round-diff.test.mjs +159 -0
  164. package/skills/paper-pipeline/scripts/run-mechanical.mjs +30 -6
  165. package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +468 -0
  166. package/skills/paper-pipeline/scripts/status.mjs +2 -2
  167. package/skills/paper-pipeline/scripts/status.test.mjs +232 -0
  168. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +2 -2
  169. package/skills/render-paper/extract-pdf-facts.harness.mjs +56 -7
  170. package/skills/tighten-paper/__snapshots__/structure.test.mjs.snap +133 -0
  171. package/skills/tighten-paper/structure.test.mjs +116 -0
  172. package/skills/verify-citations/SKILL.md +5 -5
  173. package/skills/verify-citations/SKILL.md.spec.ts +4 -4
  174. package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +235 -0
  175. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +274 -0
  176. package/skills/verify-citations/scripts/bib-authors.cli.test.mjs +178 -0
  177. package/skills/verify-citations/scripts/bib-authors.mjs +2 -1
  178. package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
  179. package/skills/verify-citations/scripts/verify-cites.mjs +96 -96
  180. package/skills/verify-citations/scripts/verify-cites.net.test.mjs +287 -0
  181. package/skills/verify-citations/scripts/verify-cites.test.mjs +149 -5
  182. package/src/adapters/banal/failure.test.ts +70 -1
  183. package/src/adapters/banal/index.test.ts +16 -0
  184. package/src/adapters/banal/output.test.ts +5 -0
  185. package/src/adapters/curl/download.test.ts +43 -1
  186. package/src/adapters/memory/index.test.ts +25 -0
  187. package/src/adapters/node/files.io.ts +3 -2
  188. package/src/adapters/node/files.test.ts +13 -1
  189. package/src/adapters/node/process.test.ts +38 -0
  190. package/src/adapters/pdfjs/fill.test.ts +34 -0
  191. package/src/adapters/pdfjs/fill.ts +44 -25
  192. package/src/adapters/references/index.test.ts +228 -0
  193. package/src/adapters/references/index.ts +21 -6
  194. package/src/adapters/references/pool.test.ts +31 -0
  195. package/src/adapters/references/pool.ts +24 -0
  196. package/src/build-engine.harness.mjs +3 -7
  197. package/src/build-engine.test.ts +137 -0
  198. package/src/build.harness.mjs +5 -9
  199. package/src/build.test.ts +362 -0
  200. package/src/build.ts +13 -9
  201. package/src/cli.harness.mjs +10 -17
  202. package/src/cli.test.ts +584 -0
  203. package/src/cli.ts +112 -66
  204. package/src/doctor.harness.mjs +3 -8
  205. package/src/doctor.test.ts +64 -0
  206. package/src/doctor.ts +12 -4
  207. package/src/domain/result.test.ts +4 -0
  208. package/src/domain/result.ts +0 -5
  209. package/src/engine.harness.mjs +27 -7
  210. package/src/facts-file.test.ts +110 -0
  211. package/src/hooks-settings.harness.mjs +43 -13
  212. package/src/hooks-settings.ts +41 -35
  213. package/src/init.test.ts +308 -5
  214. package/src/init.ts +3 -3
  215. package/src/latex-log.harness.mjs +32 -7
  216. package/src/latex-log.ts +6 -13
  217. package/src/latex-loop.harness.mjs +8 -7
  218. package/src/link-skills.harness.mjs +3 -8
  219. package/src/link-skills.test.ts +105 -0
  220. package/src/link-skills.ts +75 -21
  221. package/src/new-paper.harness.mjs +3 -7
  222. package/src/new-paper.test.ts +31 -1
  223. package/src/paper-files.test.ts +12 -0
  224. package/src/paper-files.ts +9 -5
  225. package/src/paper-settings.test.ts +20 -0
  226. package/src/pdf-facts.harness.mjs +3 -6
  227. package/src/pdf-facts.test.ts +144 -0
  228. package/src/pdf-facts.ts +17 -19
  229. package/src/pdf-geometry.harness.mjs +3 -7
  230. package/src/pdf-geometry.ts +1 -1
  231. package/src/presets.test.ts +2 -0
  232. package/src/presets.ts +7 -1
  233. package/src/references.test.ts +97 -0
  234. package/src/references.ts +2 -2
  235. package/src/rules-config.harness.mjs +10 -8
  236. package/src/structure.harness.mjs +3 -7
  237. package/src/tex-requirements.harness.mjs +34 -7
  238. package/src/tex-requirements.ts +12 -6
  239. package/src/toolchain.harness.mjs +3 -7
  240. package/src/toolchain.test.ts +280 -0
  241. package/src/toolchain.ts +8 -7
  242. package/src/venue-rules.test.ts +60 -0
  243. package/src/venue-rules.ts +13 -11
  244. package/action.mutations.mjs +0 -162
  245. package/eslint-rules/install-path-literals.mutations.mjs +0 -62
  246. package/eslint-rules/latex-language.mutations.mjs +0 -196
  247. package/eslint-rules/paper-research-question.mutations.mjs +0 -127
  248. package/eslint-rules/paper-stages.mutations.mjs +0 -81
  249. package/eslint-rules/papers.mutations.mjs +0 -186
  250. package/eslint-rules/temp-root-realpath.mutations.mjs +0 -99
  251. package/eslint-rules/tex-build.mutations.mjs +0 -258
  252. package/hooks/hooks.mutations.mjs +0 -337
  253. package/lib/agent-cli-version.mutations.mjs +0 -109
  254. package/lib/mutation-driver.harness.mjs +0 -227
  255. package/lib/mutation-driver.mjs +0 -397
  256. package/lib/mutation-driver.mutations.mjs +0 -68
  257. package/lib/paper-config.mutations.mjs +0 -143
  258. package/lib/skill-trigger-cases.mutations.mjs +0 -65
  259. package/scripts/check.mutations.mjs +0 -110
  260. package/scripts/exclusive.mjs +0 -138
  261. package/scripts/mutation-batteries-frozen.harness.mjs +0 -204
  262. package/scripts/mutation-batteries-frozen.mjs +0 -238
  263. package/scripts/mutation-batteries.frozen.json +0 -108
  264. package/scripts/rules-are-content-only.mutations.mjs +0 -81
  265. package/scripts/rules-see-files.mutations.mjs +0 -131
  266. package/scripts/run-mutations.mjs +0 -100
  267. package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +0 -218
  268. package/skills/paper-pipeline/scripts/consumer.mutations.mjs +0 -190
  269. package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +0 -54
  270. package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +0 -254
  271. package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +0 -54
  272. package/skills/paper-pipeline/scripts/population-map.mutations.mjs +0 -65
  273. package/skills/paper-pipeline/scripts/population-map.selftest.mjs +0 -122
  274. package/skills/paper-pipeline/scripts/provenance.mutations.mjs +0 -59
  275. package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +0 -276
  276. package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +0 -65
  277. package/skills/skill-contract.mutations.mjs +0 -179
  278. package/src/build.mutations.mjs +0 -196
  279. package/src/cli.mutations.mjs +0 -414
  280. package/src/doctor.mutations.mjs +0 -175
  281. package/src/hooks-settings.mutations.mjs +0 -99
  282. package/src/link-skills.mutations.mjs +0 -140
  283. package/src/new-paper.mutations.mjs +0 -65
  284. package/src/structure.mutations.mjs +0 -83
@@ -1,81 +0,0 @@
1
- /**
2
- * Battery for `paper/stages` and `paper/source` — four mutations, chosen so that each one
3
- * removes ITS OWN load-bearing property, rather than just breaking the file.
4
- *
5
- * 🔴 Why the battery matters here specifically. For both rules the success state is SILENCE, and
6
- * the harness catches them on fixtures that it lays out itself. "It passed" and "it cannot fail"
7
- * look identical from the outside; the battery is the only thing that tells them apart.
8
- *
9
- * The first two mutations hit the TWO DIRECTIONS of `paper/stages`, and this is not symmetry for
10
- * its own sake: without the second direction the rule turns itself off by deleting the
11
- * frontmatter — no declarations, so no mismatches, so green. The `declared-without-bytes`
12
- * mutation and the `bytes-without-declaration` mutation must die on DIFFERENT asserts, or only
13
- * one half is proven.
14
- */
15
- import { fileURLToPath } from "node:url";
16
- import { dirname, join, resolve } from "node:path";
17
- import { runMutations } from "../lib/mutation-driver.mjs";
18
-
19
- const HERE = dirname(fileURLToPath(import.meta.url));
20
- const ROOT = resolve(HERE, "..");
21
- const RULE = join(HERE, "paper-stages.mjs");
22
- const HARNESS = join(HERE, "paper-stages.harness.mjs");
23
-
24
- process.exit(
25
- runMutations({
26
- root: ROOT,
27
- runner: "node",
28
- cases: [
29
- {
30
- name: "the DECLARED→BYTES direction stops checking",
31
- harness: HARNESS,
32
- expect: "wrong byte count is reported",
33
- disables:
34
- "the check of declared `bytes` against the file on disk — a stage could declare any " +
35
- "number, and the pdf underneath it could be anything",
36
- edits: [[RULE, "if (got !== Number(rec.bytes)) {", "if (false) {"]],
37
- },
38
- {
39
- name: "the BYTES→DECLARED direction stops checking",
40
- harness: HARNESS,
41
- expect: "a frozen version nobody declared is reported",
42
- disables:
43
- "the second half — the one without which the rule is muted by deleting the " +
44
- "frontmatter: no declarations ⇒ no mismatches ⇒ green",
45
- edits: [
46
- [
47
- RULE,
48
- "if (records.some((r) => r.stage === f.stage && r.date === f.date))\n continue;",
49
- "if (true)\n continue;",
50
- ],
51
- ],
52
- },
53
- {
54
- name: "`paper/source` stops checking the source's size",
55
- harness: HARNESS,
56
- expect: "a byte mismatch on the source is reported",
57
- disables:
58
- "the IDENTITY check on a frozen source: `existsSync` answers 'a file exists', " +
59
- "and only the size answers 'this is that exact file'",
60
- edits: [
61
- [RULE, "if (Number.isFinite(want) && got !== want)", "if (false)"],
62
- ],
63
- },
64
- {
65
- name: "acknowledging a loss starts EXEMPTING instead of recording",
66
- harness: HARNESS,
67
- expect: "an acknowledged loss is still reported, not silenced",
68
- disables:
69
- "the decision that `sourceLost` is a RECORD, not an indulgence: the rule must keep " +
70
- "speaking, because the state is still defective, merely unfixable today",
71
- edits: [
72
- [
73
- RULE,
74
- "if (rec?.sourceLost === true) {",
75
- "if (rec?.sourceLost === true && false) {",
76
- ],
77
- ],
78
- },
79
- ],
80
- }),
81
- );
@@ -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);