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,122 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * population-map.selftest.mjs — planted defects, both directions.
4
- *
5
- * This repository has been burned three times by checkers and hooks that were green and dead, and
6
- * the rule that came out of it is that a checker which has never failed is not a checker.
7
- *
8
- * 🔴 SHRUNK 2026-08-26 TOGETHER WITH THE CHECKER ITSELF. Nine of the thirteen cases checked
9
- * `untied` and `undeclared` — both findings moved into ESLint rules (`paper/population-untied`,
10
- * `paper/population-undeclared`), and their cases moved there too, into
11
- * `eslint-rules/paper-registry.harness.mjs`, where they sit next to a run against a REAL paper.
12
- * What is left here are the two findings that speak about the registry itself: `stale` and `badref`.
13
- *
14
- * 🔴 ONE CASE WAS DELETED, NOT MOVED, and that is worth remembering. It looked like this:
15
- *
16
- * check('189 does not match inside 1,189',
17
- * !sentences('x 1,189 y').some((s) => /(?<![\d,.])189(?!\d|[,.]\d)/.test(s)));
18
- *
19
- * that is, it ran a HAND-COPIED COPY of the checker's regex over a string and asserted about a
20
- * literal inside the test itself. A mutation of the body of `printed()` does not touch such a test
21
- * by construction — which is exactly what the `eslint-rules/paper-registry.mutations.mjs` battery
22
- * showed, where that mutation SURVIVED the first run. The property is now pinned by a fixture that
23
- * checks BEHAVIOUR, not the agreement of two copies of a regex.
24
- */
25
- import { findings, bodyOf } from "./population-map.mjs";
26
-
27
- const doc = (bodyLines) =>
28
- `## Abstract\n\n${bodyLines}\n\n## References\n\n[1] x.\n`;
29
- let pass = 0,
30
- fail = 0;
31
- const check = (name, cond) => {
32
- if (cond) {
33
- pass++;
34
- } else {
35
- fail++;
36
- console.log(` FAIL ${name}`);
37
- }
38
- };
39
- const kinds = (md, reg) =>
40
- findings(md, reg).map((f) => `${f.kind}:${f.row?.print ?? f.print}`);
41
-
42
- const REG = [
43
- "id\tprinted\trelation\trelated_to\tgloss",
44
- "broad\t1,921\troot\t\tthe corpus",
45
- "sub\t1,836\tsubset\tbroad\tthose it could read",
46
- ].join("\n");
47
-
48
- // 1. Everything the registry declares is printed -> silence. The direction that matters more:
49
- // a checker firing on a paper that already obeys gets muted the same week, and a muted checker
50
- // is indistinguishable from an absent one.
51
- check(
52
- "a registry that matches the body is silent",
53
- kinds(
54
- doc(
55
- "The resolver ran over all 1,921 repositories, and read 1,836 of them.",
56
- ),
57
- REG,
58
- ).length === 0,
59
- );
60
-
61
- // 2. A registry row for a quantity the paper no longer prints. The registry shrinks when the
62
- // paper does; a row nobody removed is a claim nobody can check.
63
- check(
64
- "stale registry row fires",
65
- kinds(doc("The resolver ran over all 1,921 repositories."), REG).includes(
66
- "stale:1,836",
67
- ),
68
- );
69
-
70
- // 3. ...but a row explicitly marked `retired` is exempt — that is what the marker is FOR.
71
- // Without this the only way to keep a set declared after cutting it from the paper would be
72
- // to delete the row, which loses the record.
73
- check(
74
- "a retired row does not fire",
75
- !kinds(
76
- doc("The resolver ran over all 1,921 repositories."),
77
- REG + "\ngold\t7,310\tretired\t\ta public gold set",
78
- ).includes("stale:7,310"),
79
- );
80
-
81
- // 4. A relation pointing at an id that does not exist. Purely a defect of the TSV: the row claims
82
- // a parent that is not a row. This is why the check stayed a script — there is nothing in the
83
- // paper to point a finding at.
84
- check(
85
- "dangling related_to fires",
86
- findings(
87
- doc("We read 1,836 repositories."),
88
- "id\tprinted\trelation\trelated_to\tgloss\nsub\t1,836\tsubset\tnosuch\tx",
89
- ).some((f) => f.kind === "badref"),
90
- );
91
-
92
- // 5. `badref` is reached only for a quantity the body actually prints: a row whose number is
93
- // absent is `stale`, and reporting both about one row would double-count it.
94
- check(
95
- "a row that is absent is stale, not badref",
96
- kinds(
97
- doc("Nothing numeric here."),
98
- "id\tprinted\trelation\trelated_to\tgloss\nsub\t1,836\tsubset\tnosuch\tx",
99
- ).join() === "stale:1,836",
100
- );
101
-
102
- // 6. `printed()` boundaries, tested through BEHAVIOUR: 189 must not count as printed inside 1,189.
103
- // Without the guard the row would be considered present and `stale` would go quiet — a silent
104
- // loss, which is the failure mode this file exists to prevent. (The deleted case asserted the
105
- // same property against a hand-copied regex, and a mutation of the real one survived it.)
106
- check(
107
- '189 is not "printed" inside 1,189',
108
- kinds(
109
- doc("A follow-up read covered 1,189 records."),
110
- "id\tprinted\trelation\trelated_to\tgloss\nbroad\t1,921\troot\t\tthe corpus\nspread\t189\tsubset\tbroad\tenumerable",
111
- ).includes("stale:189"),
112
- );
113
-
114
- // 7. bodyOf must stop at the bibliography. Appendices sit after it and are not under the page
115
- // limit, so a number printed only there does not reach the reader who stops at References.
116
- check(
117
- "bodyOf stops at References",
118
- !bodyOf(doc("nothing here")).includes("[1] x."),
119
- );
120
-
121
- console.log(`population-map selftest: ${pass} passed, ${fail} failed`);
122
- process.exit(fail ? 1 : 0);
@@ -1,59 +0,0 @@
1
- /**
2
- * provenance.mutations.mjs — plant each defect `provenance.harness.mjs` claims to catch, and require it to die.
3
- *
4
- * Run: `node skills/paper-pipeline/scripts/provenance.mutations.mjs` (~1 min; it rewrites the source and
5
- * restores it, so it is not part of the plain test run).
6
- *
7
- * WHY IT EXISTS. A harness whose success state is silence cannot be observed working: «it passed»
8
- * and «it cannot fail» produce identical output. This battery is the difference. It was written on
9
- * 2026-09-12, when the mechanism moved into this package and `scripts/run-mutations.mjs` reported
10
- * this harness as one nothing could kill — a gap that had been open for as long as the harness had
11
- * existed, invisible because nothing asked the question.
12
- *
13
- * Every replacement below differs from the text it replaces (the driver refuses a no-op, which
14
- * would leave a green harness proving nothing), every case names the assertion that must fire, and
15
- * the driver verifies the patch actually landed before believing a result.
16
- */
17
- import { dirname, join } from "node:path";
18
- import { fileURLToPath } from "node:url";
19
- import { runMutations } from "../../../lib/mutation-driver.mjs";
20
- import { consumerRoot } from "./consumer.mjs";
21
-
22
- const HERE = dirname(fileURLToPath(import.meta.url));
23
- const SRC = join(HERE, "check-provenance.mjs");
24
- const HARNESS = join(HERE, "provenance.harness.mjs");
25
-
26
- const MUTATIONS = [
27
- [
28
- "the scope gate is inverted",
29
- [
30
- [
31
- SRC,
32
- "if (!provenanceScope(dir).covered) return [];",
33
- "if (provenanceScope(dir).covered) return [];",
34
- ],
35
- ],
36
- "coverage measurement must always print",
37
- "the in-scope/out-of-scope distinction: covered papers stop being checked and the report still prints",
38
- ],
39
- [
40
- "the numbers gate is removed",
41
- [[SRC, "return [...checkNumbersGate(dir)];", "return [];"]],
42
- "coverage measurement must always print",
43
- "the only finding this checker can make — it becomes a script that always says clean",
44
- ],
45
- ];
46
-
47
- process.exit(
48
- runMutations({
49
- root: consumerRoot(),
50
- runner: "vigiles",
51
- cases: MUTATIONS.map(([name, edits, expect, disables]) => ({
52
- name,
53
- disables,
54
- edits,
55
- harness: HARNESS,
56
- expect,
57
- })),
58
- }),
59
- );
@@ -1,276 +0,0 @@
1
- /**
2
- * round-diff.mutations.mjs — proof that every check in `round-diff.mjs` is watched FAILING.
3
- * Run: `node .claude/skills/paper-pipeline/scripts/round-diff.mutations.mjs` (~40 s; not in CI, it rewrites the source)
4
- *
5
- * This is not a test. It is the evidence behind the claim that the tests test something.
6
- *
7
- * WHY IT EXISTS. The house rule is «a checker that has never failed once is not considered working», and
8
- * it has been broken repeatedly here: three hooks dead on arrival, a statistics script that printed
9
- * "All recomputed values match paper.md" without opening paper.md. A green harness proves the
10
- * CHECKER fires on planted defects. It does not prove the HARNESS would notice if a check were
11
- * DELETED — an assertion can be vacuous, and a vacuous assertion is indistinguishable from a real
12
- * one in a passing run. Two were found in this repository in one week:
13
- *
14
- * - one where removing a check left the test green because a LATER BRANCH produced the same
15
- * finding KIND. The harness asserts on messages as well as kinds for that reason, and this file
16
- * is what proves the message assertions are load-bearing;
17
- * - one where the mutation DID NOT MUTATE — the `find` string was absent or matched twice, the
18
- * rewrite was a no-op, and the run reported a killed mutant. Hence the arity check below:
19
- * anything other than exactly one occurrence is reported as NOT FOUND / AMBIGUOUS and never
20
- * as a pass.
21
- *
22
- * WHAT IT DOES. For each check: neuter exactly that check in the source, run the harness, and
23
- * require the harness to go RED **at the named assertion**. Restore. A mutation the harness survives
24
- * is a check nobody is watching, and it is reported as SURVIVED.
25
- *
26
- * The source is restored in a `finally`, and the run refuses to start on a dirty working tree, so an
27
- * interrupted run cannot leave a neutered checker behind.
28
- */
29
- import { runMutations } from "../../../lib/mutation-driver.mjs";
30
- import { dirname, join } from "node:path";
31
- import { fileURLToPath } from "node:url";
32
- import { consumerRoot } from "./consumer.mjs";
33
-
34
- const HERE = dirname(fileURLToPath(import.meta.url));
35
- const ROOT = consumerRoot();
36
- const SRC = join(HERE, "round-diff.mjs");
37
- const HARNESS = join(HERE, "round-diff.harness.mjs");
38
-
39
- /**
40
- * [name, what it disables, exact source substring, replacement, harness assertion expected to fire]
41
- *
42
- * `false && add(...)` is the standard neuter for a finding: it keeps the statement, the control flow
43
- * and any `return` beside it, and removes only the report. Guards and thresholds are neutered at the
44
- * condition instead, because for those the question is whether the THRESHOLD is watched, not whether
45
- * the push is.
46
- */
47
- const MUTATIONS = [
48
- // ── the findings ──
49
- [
50
- "no-round-ledger",
51
- "reporting a paper with no round manifests",
52
- "if (!since && manifests.length === 0) {",
53
- "if (false) {",
54
- "must say so rather than pass",
55
- ],
56
- [
57
- "two-open-rounds",
58
- "reporting two manifests with no `closed:`",
59
- "if (open.length > 1) {",
60
- "if (false) {",
61
- "silently pick one base",
62
- ],
63
- [
64
- "no-open-round",
65
- "reporting edits made with every round closed",
66
- "if (dirty) {",
67
- "if (false) {",
68
- "authorised by nothing",
69
- ],
70
- [
71
- "unresolvable-base",
72
- "reporting a base revision that does not resolve",
73
- 'add(\n "unresolvable-base",\n `cannot read paper.md at',
74
- "a round whose base does not resolve is a round with no gate",
75
- ],
76
- [
77
- "ratchet/push",
78
- "reporting growth past the declared budget",
79
- "if (grew > budget) {",
80
- "if (false) {",
81
- "growth past the declared budget is the finding this gate exists for",
82
- ],
83
- // 🔴 Killed by the CLEAN CASE, not by the block that names the budget, and that is the honest
84
- // record. Any mutation that makes the gate noisier trips block 1 before it reaches its own block,
85
- // because block 1's fixture is a legal round and block 1 demands total silence. Block 1 is
86
- // therefore the strongest assertion in the harness, and the three rows marked this way are its
87
- // proof — see the note under the table at the bottom of this file.
88
- [
89
- "ratchet/budget",
90
- "honouring the declared budget (every round is charged against 0)",
91
- "const budget = num(active?.budget, 0);",
92
- "const budget = 0;",
93
- "a round that changed only its declared section, inside budget",
94
- ],
95
- [
96
- "ratchet-cumulative",
97
- "the compounding check across all rounds",
98
- "if (cum > total) {",
99
- "if (false) {",
100
- "compounding to +66 must be caught",
101
- ],
102
- [
103
- "hedge/push",
104
- "reporting hedge density growth",
105
- "if (dh > hb + 1e-9) {",
106
- "if (false) {",
107
- "hedges deposited by a review round, inside the word budget, must still be caught",
108
- ],
109
- [
110
- "hedge/budget",
111
- "honouring the declared hedge budget",
112
- 'const hb = num(active?.["hedge-budget"], 0);',
113
- "const hb = 0;",
114
- "declared its hedge budget in advance has paid for it",
115
- ],
116
- [
117
- "overbroad-scope",
118
- "reporting a `touches:` list that covers the paper",
119
- "if (all.length && covered.length * 2 > all.length) {",
120
- "if (false) {",
121
- "declaring most of the document defeats the declaration",
122
- ],
123
- [
124
- "undeclared/changed",
125
- "reporting a changed section nobody declared",
126
- 'add(\n "undeclared-section",\n `"${s.heading}" ${how}',
127
- 'false && add(\n "undeclared-section",\n `"${s.heading}" ${how}',
128
- "a change to Limitations under a manifest declaring only §3",
129
- ],
130
- [
131
- "undeclared/removed",
132
- "reporting a REMOVED section nobody declared",
133
- 'add(\n "undeclared-section",\n `"${h}" was removed',
134
- 'false && add(\n "undeclared-section",\n `"${h}" was removed',
135
- "a REMOVED section must be caught",
136
- ],
137
- [
138
- "cite/push",
139
- "reporting a citation added mid-round",
140
- 'add(\n "unauthorised-citation",',
141
- 'false && add(\n "unauthorised-citation",',
142
- "a cite added mid-round is ARIS's `new_cite`",
143
- ],
144
- [
145
- "cite/allows",
146
- "honouring `allows: new-citation`",
147
- 'if (!allows.has("new-citation")) {',
148
- "if (true) {",
149
- "a declared `new-citation` is authorised",
150
- ],
151
- [
152
- "cite/bibitem",
153
- "scanning the References list (prose markers only)",
154
- "/gm)].map((m) => m[1]),\n );",
155
- "/gm)].map(() => null),\n ).filter(Boolean);",
156
- "a bibliography entry with NO inline marker",
157
- ],
158
- [
159
- "number/push",
160
- "reporting a numeric literal added mid-round",
161
- 'add(\n "unauthorised-number",',
162
- 'false && add(\n "unauthorised-number",',
163
- "a figure that appears during a revision round",
164
- ],
165
- [
166
- "number/allows",
167
- "honouring `allows: new-number`",
168
- 'if (!allows.has("new-number")) {',
169
- "if (true) {",
170
- "a declared `new-number` is authorised",
171
- ],
172
- // Also killed by block 1 rather than by block 11b — the clean fixture's body already carries
173
- // numbers, so a gate that stops filtering out numbers already present fires on a legal round.
174
- [
175
- "number/novelty",
176
- "the rule that a number already in the paper is free to restate",
177
- "const added = [...numbers(now)].filter((n) => !wasNums.has(n));",
178
- "const added = [...numbers(now)];",
179
- "a round that changed only its declared section, inside budget",
180
- ],
181
- [
182
- "number/direction",
183
- "comparing against the BASE (the 'already present' set becomes the current one)",
184
- "const wasNums = numbers(base);",
185
- "const wasNums = numbers(now);",
186
- "a figure that appears during a revision round has no provenance row",
187
- ],
188
- [
189
- "unknown-op",
190
- "reporting an `allows:` entry this gate does not implement",
191
- 'add(\n "unknown-op",',
192
- 'false && add(\n "unknown-op",',
193
- "must be reported. ARIS carries `new_theorem_env`",
194
- ],
195
- [
196
- "manifest/base",
197
- "rejecting a manifest with no `base:`",
198
- "if (!out.base)\n return {\n error:",
199
- "if (false)\n return {\n error:",
200
- "a manifest with no base must be reported, not skipped",
201
- ],
202
- [
203
- "manifest/touches",
204
- "rejecting a manifest with no `touches:`",
205
- "if (!Array.isArray(out.touches) || out.touches.length === 0) {",
206
- "if (false) {",
207
- "authorises nothing and must say so",
208
- ],
209
-
210
- // ── the shared machinery every finding above is computed from ──
211
- [
212
- "covers/nesting",
213
- "number-prefix nesting in `touches:` (4 no longer covers 4.1)",
214
- 'num.startsWith(e + ".")',
215
- "false",
216
- "must cover §2.1",
217
- ],
218
- [
219
- "covers/text",
220
- "the substring rule for NON-numeric entries (`Appendix` covers nothing)",
221
- "return h.includes(e);",
222
- "return false;",
223
- "moving a passage into an appendix must not read as growth",
224
- ],
225
- [
226
- "split/appendix",
227
- "the body↔appendix boundary (everything counts as body)",
228
- "const i = secs.findIndex((s) => /^references\\b/i.test(s.heading));",
229
- "const i = -1;",
230
- "the appendix delta is still reported as a fact",
231
- ],
232
- [
233
- "census/appendix",
234
- "keeping appendix words out of the body count",
235
- " bodyWords: bw,",
236
- ' bodyWords: bw + words(appendix.map((s) => s.text).join("\\n")),',
237
- "appendix growth under a zero body budget must be free",
238
- ],
239
- [
240
- "strip/comments",
241
- "stripping HTML comments before every measurement",
242
- 'const stripComments = (s) => s.replace(/<!--[\\s\\S]*?-->/g, " ");',
243
- "const stripComments = (s) => s;",
244
- "rewriting a section's own TIGHTEN note must change nothing",
245
- ],
246
- [
247
- "since/mode",
248
- "`--since` running without a manifest",
249
- 'const since =\n (args.find((a) => a.startsWith("--since=")) ?? "").split("=")[1] ||\n undefined;',
250
- "const since = undefined;",
251
- "`--since` must still measure",
252
- ],
253
- ];
254
-
255
- // Two case shapes, and BOTH are preserved: the long `[name, disables, find, replace, expect]` and
256
- // the `false &&` shorthand `[name, disables, find, expect]`, where the replacement is the find with
257
- // `false && ` in front. The shorthand is the standard neuter for a finding — it keeps the statement,
258
- // the control flow and any `return` beside it, and removes only the report — so it earns its place
259
- // as data, and expanding it here keeps the engine ignorant of what a "finding" is.
260
- process.exit(
261
- runMutations({
262
- root: ROOT,
263
- cases: MUTATIONS.map((m) => {
264
- const [name, disables, find] = m;
265
- const [replace, expect] =
266
- m.length === 4 ? [`false && ${find}`, m[3]] : [m[3], m[4]];
267
- return {
268
- name,
269
- disables,
270
- edits: [[SRC, find, replace]],
271
- harness: HARNESS,
272
- expect,
273
- };
274
- }),
275
- }),
276
- );
@@ -1,65 +0,0 @@
1
- /**
2
- * plan-paper-timeline.effects.mutations.mjs — the non-vacuity proof for the EFFECTS harness.
3
- *
4
- * Run: `node skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs` (slow: each
5
- * case spawns the real `claude` CLI against a scripted mock model).
6
- *
7
- * ── WHY THIS ONE IS EASY TO GET WRONG ───────────────────────────────────────
8
- * The effects harness spends most of its length proving that a run HAPPENED — the skill
9
- * activated, its body reached the model, a control tool answered. Those are preconditions, and a
10
- * harness made of preconditions passes for reasons that have nothing to do with the thing under
11
- * test. The two cases below plant defects in the two claims that are actually about §3.
12
- *
13
- * ── CASE 1: the zone in the prescription stops being the documented default ─
14
- * §3's example must carry `DEFAULT_TIMEZONE`, because the zone is the `timezone` CARRIER — it
15
- * belongs to the author, not to the pipeline. A REAL zone in the example is the silent defect:
16
- * it is valid, the calendar API accepts it, the harness's schema check passes, and every author
17
- * who copies the example schedules their deadlines in somebody else's day. `Asia/Tokyo` is
18
- * chosen precisely because it is well-formed; a malformed zone would be caught by the other
19
- * assertion and would prove the wrong half.
20
- *
21
- * ── CASE 2: the instruction to substitute disappears ────────────────────────
22
- * With the example showing a default and the "read the carrier" line deleted, the default
23
- * silently BECOMES the prescription — every anchor lands in UTC and nothing anywhere says so.
24
- * Removing the line leaves a file that still reads sensibly, which is why a human diff is not
25
- * enough and this case exists.
26
- */
27
- import { dirname, join } from "node:path";
28
- import { fileURLToPath } from "node:url";
29
- import { runMutations } from "../../lib/mutation-driver.mjs";
30
- import { consumerRoot } from "../paper-pipeline/scripts/consumer.mjs";
31
-
32
- const HERE = dirname(fileURLToPath(import.meta.url));
33
- const SKILL = join(HERE, "SKILL.md");
34
- const HARNESS = join(HERE, "plan-paper-timeline.effects.harness.mjs");
35
-
36
- process.exit(
37
- runMutations({
38
- root: consumerRoot(),
39
- runner: "vigiles",
40
- cases: [
41
- {
42
- name: "§3's example carries a real zone instead of the default",
43
- disables:
44
- "the rule that the prescription shows DEFAULT_TIMEZONE — a real zone reads as the zone to use",
45
- edits: [[SKILL, '"timeZone": "UTC"', '"timeZone": "Asia/Tokyo"']],
46
- harness: HARNESS,
47
- expect: "must show the documented default",
48
- },
49
- {
50
- name: "§3 stops telling the model to read the `timezone` carrier",
51
- disables:
52
- "the substitution instruction — without it the default stops being an example and becomes the prescription",
53
- edits: [
54
- [
55
- SKILL,
56
- "require('./paperlint.json')?.timezone } catch {} })() ?? 'UTC'",
57
- "require('./paperlint.json')?.papersDir } catch {} })() ?? 'papers'",
58
- ],
59
- ],
60
- harness: HARNESS,
61
- expect: "tells the model to READ the `timezone` carrier",
62
- },
63
- ],
64
- }),
65
- );