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,108 +0,0 @@
1
- {
2
- "issue": "https://github.com/zernie/paperlint/issues/52",
3
- "why": "Hand-written *.mutations.mjs batteries are being removed (#52). This list may only shrink: no new batteries, no new cases in existing ones. Delete a battery or a case, then delete its entry or lower its count here. Checked by scripts/mutation-batteries-frozen.mjs, part of npm run check.",
4
- "batteries": {
5
- "action.mutations.mjs": {
6
- "cases": 6
7
- },
8
- "eslint-rules/install-path-literals.mutations.mjs": {
9
- "cases": 3
10
- },
11
- "eslint-rules/latex-language.mutations.mjs": {
12
- "cases": 12
13
- },
14
- "eslint-rules/paper-research-question.mutations.mjs": {
15
- "cases": 6
16
- },
17
- "eslint-rules/paper-stages.mutations.mjs": {
18
- "cases": 4
19
- },
20
- "eslint-rules/papers.mutations.mjs": {
21
- "cases": 10
22
- },
23
- "eslint-rules/temp-root-realpath.mutations.mjs": {
24
- "cases": 5
25
- },
26
- "eslint-rules/tex-build.mutations.mjs": {
27
- "cases": 21
28
- },
29
- "fixtures/real-markdown-paper/real-markdown-paper.mutations.mjs": {
30
- "cases": 3
31
- },
32
- "hooks/hooks.mutations.mjs": {
33
- "cases": 23
34
- },
35
- "lib/agent-cli-version.mutations.mjs": {
36
- "cases": 6
37
- },
38
- "lib/mutation-driver.mutations.mjs": {
39
- "cases": 3
40
- },
41
- "lib/paper-config.mutations.mjs": {
42
- "cases": 7
43
- },
44
- "lib/skill-trigger-cases.mutations.mjs": {
45
- "cases": 3
46
- },
47
- "scripts/check.mutations.mjs": {
48
- "cases": 5
49
- },
50
- "scripts/rules-are-content-only.mutations.mjs": {
51
- "cases": 3
52
- },
53
- "scripts/rules-see-files.mutations.mjs": {
54
- "cases": 2
55
- },
56
- "skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs": {
57
- "cases": 22
58
- },
59
- "skills/paper-pipeline/scripts/consumer.mutations.mjs": {
60
- "cases": 14
61
- },
62
- "skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs": {
63
- "cases": 1
64
- },
65
- "skills/paper-pipeline/scripts/generated-code.mutations.mjs": {
66
- "cases": 25
67
- },
68
- "skills/paper-pipeline/scripts/pipeline-check.mutations.mjs": {
69
- "cases": 1
70
- },
71
- "skills/paper-pipeline/scripts/population-map.mutations.mjs": {
72
- "cases": 2
73
- },
74
- "skills/paper-pipeline/scripts/provenance.mutations.mjs": {
75
- "cases": 2
76
- },
77
- "skills/paper-pipeline/scripts/round-diff.mutations.mjs": {
78
- "cases": 28
79
- },
80
- "skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs": {
81
- "cases": 2
82
- },
83
- "skills/skill-contract.mutations.mjs": {
84
- "cases": 22
85
- },
86
- "src/build.mutations.mjs": {
87
- "cases": 11
88
- },
89
- "src/cli.mutations.mjs": {
90
- "cases": 25
91
- },
92
- "src/doctor.mutations.mjs": {
93
- "cases": 10
94
- },
95
- "src/hooks-settings.mutations.mjs": {
96
- "cases": 6
97
- },
98
- "src/link-skills.mutations.mjs": {
99
- "cases": 6
100
- },
101
- "src/new-paper.mutations.mjs": {
102
- "cases": 3
103
- },
104
- "src/structure.mutations.mjs": {
105
- "cases": 4
106
- }
107
- }
108
- }
@@ -1,81 +0,0 @@
1
- /**
2
- * Battery for the "rule does not touch git" gate, `scripts/rules-are-content-only.mjs`.
3
- *
4
- * 🔴 This battery needs to be sharper than the rule. Its success state is an EMPTY LIST, which is
5
- * exactly the state it teaches not to trust. A check seen only silent is indistinguishable from
6
- * one that never looks.
7
- *
8
- * BOTH DIRECTIONS, and they must die on DIFFERENT assertions:
9
- * - `undercatch` — stops finding anything. The "quiet on clean" half stays green;
10
- * the "fires" half must fail.
11
- * - `overcatch` — calls any spawner a finding. The "fires" half stays green
12
- * (it still sees `git`), the "quiet on clean" half must fail. MEASURED: it breaks on
13
- * LIVE CORPUS (6 findings in the package's own rules) before a planted fixture with
14
- * `texcount` — and that is stronger, not weaker: real data.
15
- * A battery where both mutations die on one assertion proves ONE half exists.
16
- *
17
- * ⚠️ The second mutation reproduces a REAL false positive: the first version of the check
18
- * was keyed on the `child_process` import and in the first hour called `paper-texcount.mjs`
19
- * a finding in the consumer — it calls `texcount` to count words. On an `error` gate this is
20
- * worse than a miss: a gate you cannot pass gets turned off. The mutation holds this line.
21
- */
22
- import { fileURLToPath } from "node:url";
23
- import { dirname, join, resolve } from "node:path";
24
- import { runMutations } from "../lib/mutation-driver.mjs";
25
-
26
- const HERE = dirname(fileURLToPath(import.meta.url));
27
- const ROOT = resolve(HERE, "..");
28
- const GUARD = join(HERE, "rules-are-content-only.mjs");
29
- const HARNESS = join(HERE, "rules-are-content-only.harness.mjs");
30
-
31
- process.exit(
32
- runMutations({
33
- root: ROOT,
34
- runner: "node",
35
- cases: [
36
- {
37
- name: "undercatch — check stops finding",
38
- harness: HARNESS,
39
- expect: "FIRES on execFileSync with a git argv",
40
- disables:
41
- "the verdict itself: the check becomes that very confident green zero it was written " +
42
- "to spot — it scans, counts files and renders no judgment",
43
- edits: [
44
- [
45
- GUARD,
46
- "if (SPAWNERS.has(String(name)) && isGit(n.arguments?.[0]?.value))",
47
- "if (false && SPAWNERS.has(String(name)) && isGit(n.arguments?.[0]?.value))",
48
- ],
49
- ],
50
- },
51
- {
52
- name: "overcatch — any spawner call becomes a finding",
53
- harness: HARNESS,
54
- // Fails on LIVE CORPUS, not on the texcount fixture, and this is measured: under this mutation
55
- // the package's own rules yield 6 findings, meaning the "quiet on clean" half breaks
56
- // before we reach the planted case. Live signal is stronger than fixture — it is real data — so
57
- // it is named, not the one I expected to see.
58
- expect: "quiet on the real corpus",
59
- disables:
60
- "the boundary of the predicate 'calls git' vs 'spawns a process' — the one that was " +
61
- "set after the texcount false positive in the consumer",
62
- edits: [
63
- [
64
- GUARD,
65
- "if (SPAWNERS.has(String(name)) && isGit(n.arguments?.[0]?.value))",
66
- "if (SPAWNERS.has(String(name)))",
67
- ],
68
- ],
69
- },
70
- {
71
- name: "guard for empty scan stops failing",
72
- harness: HARNESS,
73
- expect: "scanned something",
74
- disables:
75
- "the answer to 'did you even look': zero found sources become silent success again, " +
76
- "indistinguishable from a clean run",
77
- edits: [[GUARD, 'f.endsWith(".mjs") &&', "false &&"]],
78
- },
79
- ],
80
- }),
81
- );
@@ -1,131 +0,0 @@
1
- /**
2
- * Mutation battery for the guard against a green zero, `scripts/rules-see-files.mjs`.
3
- * Run: `node scripts/rules-see-files.mutations.mjs` (not `vigiles test` — not a harness).
4
- *
5
- * 🔴 WHY THIS GUARD NEEDS A MUTATION BATTERY MORE THAN ANYTHING ELSE HERE. Its success state
6
- * is an empty list, which is the exact state it was built to distrust. A guard that has only
7
- * been observed quiet is indistinguishable from a guard that does nothing — that is the whole
8
- * premise of the thing being guarded, applied to the guard itself.
9
- *
10
- * BOTH DIRECTIONS, one mutation each, and they must die on DIFFERENT assertions:
11
- * - `under-report` makes the guard incapable of ever calling a rule blind. The quiet half of
12
- * its harness stays green; the FIRING half must go red.
13
- * - `over-report` makes it call everything blind. The firing half stays green (it still names
14
- * `probe/dead`, among others); the QUIET half must go red.
15
- * A battery in which both mutations died on the same assertion would prove only that one of the
16
- * two halves exists.
17
- */
18
- import { readFileSync, writeFileSync } from "node:fs";
19
- import { execFileSync } from "node:child_process";
20
-
21
- const GUARD = "scripts/rules-see-files.mjs";
22
- const HARNESS = "scripts/rules-see-files.harness.mjs";
23
-
24
- // Coverage mode — see `scripts/run-mutations.mjs` guard 2.
25
- if (process.env.MUTATIONS_REPORT_COVERAGE) {
26
- console.log(`MUTATION-COVERS\t${HARNESS}`);
27
- process.exit(0);
28
- }
29
-
30
- const PRISTINE = readFileSync(GUARD, "utf8");
31
-
32
- const M = [
33
- [
34
- "under-report",
35
- "never call a rule blind — the guard becomes the silent green it exists to catch",
36
- "blind: rules.filter((r) => r.files === 0).map((r) => r.rule)",
37
- "blind: [] /* MUT */",
38
- ],
39
- [
40
- "over-report",
41
- "call every rule blind — the guard becomes noise, and noise gets switched off within a day",
42
- " if (effective.rules?.[id] && !isOff(effective.rules[id]))\n seen.set(id, seen.get(id) + 1);",
43
- " if (false)\n seen.set(id, seen.get(id) + 1); /* MUT */",
44
- ],
45
- ];
46
-
47
- const runHarness = () => {
48
- try {
49
- execFileSync("npx", ["vigiles", "test", HARNESS], {
50
- encoding: "utf8",
51
- stdio: ["ignore", "pipe", "pipe"],
52
- });
53
- return { failed: false, out: "" };
54
- } catch (e) {
55
- return { failed: true, out: (e.stdout ?? "") + (e.stderr ?? "") };
56
- }
57
- };
58
-
59
- {
60
- const base = runHarness();
61
- if (base.failed) {
62
- console.log(
63
- "❌ THE HARNESS IS RED BEFORE ANY MUTATION — the battery cannot tell a killed mutation " +
64
- "from that:\n" +
65
- base.out.slice(-1500),
66
- );
67
- process.exit(1);
68
- }
69
- }
70
-
71
- let ok = 0;
72
- let bad = 0;
73
- const rows = [];
74
- for (const [label, what, from, to] of M) {
75
- const hits = PRISTINE.split(from).length - 1;
76
- if (hits !== 1) {
77
- console.log(
78
- `❌ ${label}: TARGET ${hits === 0 ? "NOT FOUND" : `NOT UNIQUE (${hits})`} — ${from.slice(0, 70)}`,
79
- );
80
- bad++;
81
- continue;
82
- }
83
- writeFileSync(GUARD, PRISTINE.replace(from, to));
84
- const on = readFileSync(GUARD, "utf8");
85
- if (!on.includes(to) || on.includes(from)) {
86
- console.log(
87
- `❌ ${label}: THE MUTATION DID NOT LAND (checked by re-reading the file)`,
88
- );
89
- bad++;
90
- writeFileSync(GUARD, PRISTINE);
91
- continue;
92
- }
93
- const { failed, out } = runHarness();
94
- writeFileSync(GUARD, PRISTINE);
95
- if (readFileSync(GUARD, "utf8") !== PRISTINE) {
96
- console.log(`❌ ${label}: THE ROLLBACK FAILED`);
97
- bad++;
98
- continue;
99
- }
100
- const at = (out.match(/rules-see-files\.harness\.mjs:(\d+)/) ?? [])[1];
101
- const msg = (out.match(/AssertionError[^:]*: ([^\n]+)/) ?? [])[1] ?? "";
102
- if (failed) {
103
- ok++;
104
- rows.push([
105
- label,
106
- what,
107
- ((at ? `harness:${at} · ` : "") + msg).trim().slice(0, 130),
108
- ]);
109
- } else {
110
- console.log(
111
- `🔴 ${label}: THE HARNESS IS GREEN UNDER THE MUTATION — a finding about the TEST, not a conclusion about the guard`,
112
- );
113
- bad++;
114
- }
115
- }
116
- console.log("\n| direction | mutation | what the harness died on |");
117
- console.log("|---|---|---|");
118
- for (const [a, b, c] of rows)
119
- console.log(`| \`${a}\` | ${b} | ${c.replace(/\|/g, "\\|")} |`);
120
- // The two must die on DIFFERENT lines, or only one half of the guard is actually tested.
121
- const lines = rows.map(
122
- ([, , where]) => (where.match(/harness:(\d+)/) ?? [])[1],
123
- );
124
- if (rows.length === 2 && lines[0] && lines[0] === lines[1]) {
125
- console.log(
126
- "\n🔴 BOTH mutations died on the SAME assertion — only one half of the guard is tested",
127
- );
128
- bad++;
129
- }
130
- console.log(`\n${ok} mutation(s) killed, ${bad} problem(s)`);
131
- process.exit(bad ? 1 : 0);
@@ -1,100 +0,0 @@
1
- /**
2
- * run-mutations.mjs — run EVERY mutation battery in the repository, found rather than listed.
3
- *
4
- * ── WHY THIS EXISTS, AND IT IS NOT TIDINESS ─────────────────────────────────
5
- * 🔴 It replaces a hand-written list in `package.json` that had ALREADY ROTTED. On 2026-09-11
6
- * the `mutations` script read:
7
- *
8
- * node eslint-rules/tex-build.mutations.mjs &&
9
- * node eslint-rules/latex-language.mutations.mjs &&
10
- * node scripts/rules-see-files.mutations.mjs
11
- *
12
- * — three of the four batteries on disk. `papers.mutations.mjs`, added the same day, was not in
13
- * it, and nothing said so: the command exited 0 and printed three confident green tables. A
14
- * list of paths maintained by hand is the thing that goes stale, and it goes stale SILENTLY,
15
- * which is the same failure class the batteries themselves exist to catch.
16
- *
17
- * ── THE TWO GUARDS ──────────────────────────────────────────────────────────
18
- * 1. FINDING ZERO BATTERIES IS AN ERROR, not an empty success. A run that discovers nothing and
19
- * exits 0 is byte-identical to a run where every battery passed — the exact green zero this
20
- * repository has a dedicated script for (`rules-see-files.mjs`), applied here to itself.
21
- * 2. (REMOVED 2026-09-24) A harness without a battery used to be an error. It no longer is —
22
- * see the note above the run loop.
23
- *
24
- * ⚠️ Batteries WRITE to the file under test and roll it back. They are safe to run on a clean
25
- * tree and NOT safe to run with uncommitted edits to a rule: a crash between the write and the
26
- * rollback leaves the mutant on disk. Each battery re-reads the file and verifies the rollback
27
- * byte for byte, so the window is small, but it is not zero.
28
- *
29
- * Run: `node scripts/run-mutations.mjs` (also part of `npm run check`)
30
- */
31
- import { execFileSync } from "node:child_process";
32
- import { readdirSync, statSync } from "node:fs";
33
- import { dirname, join, relative } from "node:path";
34
- import { fileURLToPath } from "node:url";
35
-
36
- const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
37
- // 🔴 `.claude` IS A VIEW OF `skills/`, NOT A SECOND COPY OF IT. This repository is its own first
38
- // consumer: `.claude/skills/<name>` are symlinks back to `skills/<name>` so that the path every
39
- // SKILL.md's prose names resolves here too (see `skills/skill-contract.mutations.mjs`). Walking
40
- // into it counts each harness TWICE under two spellings of one file, and the second spelling is
41
- // never what a battery names — so every skill harness read as an orphan while being perfectly
42
- // covered. Measured 2026-09-12: 11 phantom orphans, all of them the same files seen through the
43
- // symlink. Skipping the view is not an exemption: the real files are still walked under `skills/`.
44
- const SKIP = new Set(["node_modules", ".git", "fixtures", ".claude"]);
45
-
46
- /** Every `*.mutations.mjs` on disk, as repo-relative paths. */
47
- function collect(dir, found = { mutations: [] }) {
48
- for (const entry of readdirSync(dir)) {
49
- if (SKIP.has(entry)) continue;
50
- const p = join(dir, entry);
51
- if (statSync(p).isDirectory()) {
52
- collect(p, found);
53
- continue;
54
- }
55
- if (entry.endsWith(".mutations.mjs"))
56
- found.mutations.push(relative(ROOT, p));
57
- }
58
- return found;
59
- }
60
-
61
- const { mutations } = collect(ROOT);
62
- mutations.sort();
63
-
64
- // Guard 1 — the green zero, applied to this script itself.
65
- if (mutations.length === 0) {
66
- console.error(
67
- "❌ NO mutation batteries found under " +
68
- ROOT +
69
- ".\n" +
70
- " This is an error rather than an empty success: a run that discovers nothing and exits " +
71
- "0\n is indistinguishable from a run where every battery passed.",
72
- );
73
- process.exit(1);
74
- }
75
-
76
- // 🔴 NO ORPHAN GUARD ANY MORE (2026-09-24). Until then this script refused every `*.harness.mjs`
77
- // that no battery named — so a new harness could not land without a new hand-written battery of
78
- // string replacements over source lines. Those batteries are the pattern issue #52 is replacing:
79
- // they break on every reformat, rerun a whole harness per case, and are slow. Forcing more of them
80
- // into existence was the wrong direction, so the requirement is gone. Existing batteries still run
81
- // below, and one that fails to kill its mutant still fails the run.
82
-
83
- console.log(
84
- `Running ${mutations.length} mutation batter${mutations.length === 1 ? "y" : "ies"}:\n`,
85
- );
86
- const failed = [];
87
- for (const m of mutations) {
88
- console.log(`──────── ${m}`);
89
- try {
90
- execFileSync(process.execPath, [m], { cwd: ROOT, stdio: "inherit" });
91
- } catch {
92
- failed.push(m);
93
- }
94
- }
95
-
96
- console.log(
97
- `\n${mutations.length - failed.length}/${mutations.length} batter${mutations.length === 1 ? "y" : "ies"} clean` +
98
- (failed.length ? `, FAILED: ${failed.join(", ")}` : ""),
99
- );
100
- process.exit(failed.length ? 1 : 0);
@@ -1,218 +0,0 @@
1
- /**
2
- * artifact-coverage.mutations.mjs — proof that every check in `artifact-coverage.mjs` is watched
3
- * FAILING, with the reverse (DISK_UNREPORTED) leg and its ignore set as the reason this file exists.
4
- * Run: `node .claude/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs`
5
- *
6
- * This is not a test. It is the evidence behind the claim that the tests test something.
7
- *
8
- * WHY THIS LEG IN PARTICULAR. The reverse leg is the only thing in the pipeline that looks for a
9
- * result nobody reported, and it is guarded by an ignore set. Both halves fail silently in the same
10
- * direction: a broken detector finds nothing, and an over-wide ignore set also finds nothing, and
11
- * from outside those are the same output. A green harness proves the CHECKER fires on planted
12
- * defects; it does not prove the HARNESS would notice if a check were deleted. Every mutations file
13
- * written in this repository has found something on its first run, and this one is no exception —
14
- * see the notes in the table.
15
- *
16
- * WHAT IT DOES. For each check, it neuters exactly that check in the source, runs the harness, and
17
- * requires the harness to go RED with the named assertion. Then it restores the file. A mutation
18
- * the harness survives is a check nobody is watching, and it is reported as SURVIVED.
19
- *
20
- * The source is restored in a `finally`, and the run refuses to start on a dirty working tree, so
21
- * an interrupted run cannot leave a neutered checker behind.
22
- */
23
- import { runMutations } from "../../../lib/mutation-driver.mjs";
24
- import { dirname, join } from "node:path";
25
- import { fileURLToPath } from "node:url";
26
- import { consumerRoot } from "./consumer.mjs";
27
-
28
- const HERE = dirname(fileURLToPath(import.meta.url));
29
- const ROOT = consumerRoot();
30
- const SRC = join(HERE, "artifact-coverage.mjs");
31
- const HARNESS = join(HERE, "artifact-coverage.harness.mjs");
32
-
33
- /** [name, what it disables, exact source substring, replacement, harness case expected to go red] */
34
- const MUTATIONS = [
35
- // ── the two forward legs, which had a harness and no mutation proof until today ──
36
- [
37
- "fwd/section-indexed",
38
- "the rule that a number-reporting section must be named in the index",
39
- " if (!named)\n",
40
- " if (false)\n",
41
- "is named NOWHERE",
42
- ],
43
- [
44
- "fwd/index-path-exists",
45
- "the rule that a path the index names must exist",
46
- " if (!existsSync(join(bundle, head)) && !readdirSync(bundle).includes(head))",
47
- " if (false)",
48
- "is not in the bundle",
49
- ],
50
- [
51
- "fwd/no-index",
52
- "the refusal to accept a bundle that maps nothing to anything",
53
- "if (!indexPath) {",
54
- "if (false) {",
55
- "a bundle that maps nothing to anything was accepted",
56
- ],
57
- [
58
- "fwd/repo-not-path",
59
- "the guard that `owner/repo` is not a path (extension required)",
60
- " if (!/\\.[a-z0-9]{1,5}$/i.test(p)) continue;",
61
- " if (false) continue;",
62
- "a complete bundle was failed",
63
- ],
64
-
65
- // ── the reverse leg: the detector itself ──
66
- [
67
- "rev/finding",
68
- "the DISK_UNREPORTED finding",
69
- " for (const c of unreported)\n findings.push(",
70
- " for (const c of [])\n findings.push(",
71
- "cherry-picking case this leg was ported for",
72
- ],
73
- [
74
- "rev/headline-results",
75
- "RESULTS.md as a headline write-up",
76
- "/^(RESULTS|SUMMARY|FINDINGS|METRICS)\\.",
77
- "/^(SUMMARY|FINDINGS|METRICS)\\.",
78
- "cherry-picking case this leg was ported for",
79
- ],
80
- [
81
- "rev/paper-mention",
82
- "crediting a mention in the paper",
83
- "if (named(paper)) {",
84
- "if (false) {",
85
- "an experiment the paper discusses by name was reported as unmentioned",
86
- ],
87
- [
88
- "rev/index-mention",
89
- "crediting a mention in the index",
90
- "if (named(indexAll)) {",
91
- "if (false) {",
92
- "named in the bundle's README.md and nowhere else",
93
- ],
94
- [
95
- "rev/all-index-files",
96
- "reading BOTH index files (reverts to check 1's single indexPath)",
97
- " if (named(indexAll)) {",
98
- " if (named(index)) {",
99
- "named in the bundle's README.md and nowhere else",
100
- ],
101
- [
102
- "rev/date-slug",
103
- "stripping the -YYYY-MM-DD suffix before matching",
104
- 'const slug = c.name.replace(/-\\d{4}-\\d{2}-\\d{2}$/, "");',
105
- "const slug = c.name;",
106
- "names without its date suffix",
107
- ],
108
- [
109
- "rev/whole-token",
110
- "whole-token matching (reverts to substring `includes`)",
111
- "const mentions = (t, s) =>\n s.length >= 4 && new RegExp(`(?<![\\\\w-])${esc(s)}(?![\\\\w-])`).test(t);",
112
- "const mentions = (t, s) => t.includes(s);",
113
- "the header does not state the real finding count",
114
- ],
115
- [
116
- "rev/bundle-skipped",
117
- "the structural ignore that skips the released bundle",
118
- " if (name === bundleName) {\n swallowed.get([...swallowed.keys()][0]).push(name);\n continue;\n }",
119
- " if (false) {\n swallowed.get([...swallowed.keys()][0]).push(name);\n continue;\n }",
120
- "the bundle's own RESULTS.md was counted",
121
- ],
122
-
123
- // ── the ignore set, which can neuter the detector without changing a line of it ──
124
- [
125
- "ign/applied",
126
- "applying an allow row at all",
127
- "if (allowed.has(c.name)) {",
128
- "if (false) {",
129
- "an allowed row still counted as a finding",
130
- ],
131
- [
132
- "ign/rows-parsed",
133
- "parsing the allow file",
134
- " if (name)\n allowed.set(",
135
- " if (false)\n allowed.set(",
136
- "an allowed row still counted as a finding",
137
- ],
138
- [
139
- "ign/ships-as-verified",
140
- "re-verifying a `ships-as:` claim against the bundle",
141
- "if (m && !existsSync(join(bundle, m[1])))",
142
- "if (false)",
143
- "the allowance has rotted",
144
- ],
145
- [
146
- "ign/stale-rows",
147
- "reporting a row that names no directory",
148
- " if (!candidates.some((c) => c.name === name))",
149
- " if (false)",
150
- "sat in the ignore file unreported",
151
- ],
152
- [
153
- "ign/printed-on-clean",
154
- "printing the ledger when there are no findings",
155
- ' console.log(\n " every number-reporting section is indexed, every named path exists",\n );\n ledger.forEach((l) => console.log(l));',
156
- ' console.log(\n " every number-reporting section is indexed, every named path exists",\n );',
157
- // Killed by case 9 rather than case 12: case 9 is also a 0-finding report-mode run and asserts
158
- // the same line, and it runs first. Both cases assert the property; the row records WHICH one
159
- // catches it so a future reader does not mistake this for an off-target kill.
160
- "was applied and NOT printed",
161
- ],
162
- [
163
- "ign/reasons-printed",
164
- "printing each allow row's reason",
165
- " for (const [name, reason] of ignored)\n ledger.push(` ${name} → ${reason}`);",
166
- " for (const [name, reason] of ignored)\n ledger.push(` ${name}`);",
167
- "was applied and NOT printed",
168
- ],
169
- [
170
- "ign/empty-file-said",
171
- "saying so when nothing is being waved through",
172
- ' if (!ignored.length)\n ledger.push(" (the file is empty — nothing is being waved through)");',
173
- ' if (false)\n ledger.push(" (the file is empty — nothing is being waved through)");',
174
- "an empty allow file printed nothing at all",
175
- ],
176
- [
177
- "ign/ratio",
178
- "the ratio line that says whether this check still does anything",
179
- " ` Ratio: ${unreported.length} reported as unreported, ${ignored.length} deliberately ignored, ` +",
180
- " ` Ratio: ` +",
181
- "printed no ratio",
182
- ],
183
-
184
- // ── the machine-readable contract with run-mechanical.mjs ──
185
- [
186
- "out/stated-count",
187
- "the stated finding count in the header",
188
- "const header = `📦 artifact coverage — ${findings.length} finding(s) in ${dir}`;",
189
- "const header = `📦 artifact coverage — ${dir}`;",
190
- "does not state `— 0 finding(s)`",
191
- ],
192
- [
193
- "out/flags-quiet",
194
- "the silence of --flags-only on a clean tree",
195
- "if (!flagsOnly) {\n console.log(header);",
196
- "if (true) {\n console.log(header);",
197
- "--flags-only printed something about a complete bundle",
198
- ],
199
- ];
200
-
201
- // The table above is DATA about this checker; the engine below is shared with the other mutation
202
- // files (`mutation-driver.mjs`). The split gave this file two protections it never had: the no-op
203
- // guard (a replacement equal to the original leaves a green harness proving nothing) and one retry
204
- // on a non-kill (a flake reported as a survivor sends the next reader to rewrite a working
205
- // assertion). It also refuses to start when a named harness does not exist — the defect that made
206
- // three files report SURVIVED for every case after the 2026-08-11 colocation.
207
- process.exit(
208
- runMutations({
209
- root: ROOT,
210
- cases: MUTATIONS.map(([name, disables, find, replace, expect]) => ({
211
- name,
212
- disables,
213
- edits: [[SRC, find, replace]],
214
- harness: HARNESS,
215
- expect,
216
- })),
217
- }),
218
- );