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,179 +0,0 @@
1
- /**
2
- * skill-contract.mutations.mjs — the non-vacuity proof for the colocated skill harnesses.
3
- *
4
- * Run: `node skills/skill-contract.mutations.mjs` (~1 min; it rewrites each SKILL.md and restores
5
- * it, so it is not part of the plain test run).
6
- *
7
- * ── WHY IT EXISTS, AND WHY IT ARRIVED WITH THE SKILLS ───────────────────────
8
- * `scripts/run-mutations.mjs` refuses a repository where some `*.harness.mjs` is named by no
9
- * battery, and the refusal is the whole argument of this package: silence is the success state of
10
- * every check here, so «it passed» and «it cannot fail» are byte-identical from outside. The ten
11
- * skill harnesses arrived on 2026-09-12 and were orphans the moment they landed — the guard said
12
- * so on the first run, which is exactly what a guard is for.
13
- *
14
- * ── HOW THE SKILLS ARE REACHED FROM INSIDE THIS PACKAGE ─────────────────────
15
- * 🔴 `checkSkill()` looks at `<consumerRoot>/.claude/skills/<name>/SKILL.md` — the fixed Claude
16
- * Code layout — not at `skills/<name>` where this repository keeps them. So this repository is its
17
- * OWN FIRST CONSUMER: `.claude/skills/<name>` is a symlink to `../../skills/<name>`, and
18
- * `.claude/skills/paper-pipeline/scripts` is a symlink to the real scripts directory, so the path
19
- * every SKILL.md's prose names (`node .claude/skills/paper-pipeline/scripts/announce.mjs …`)
20
- * resolves here too. Without those symlinks `skill-corpus.mjs` throws while LOADING, and a harness
21
- * whose module fails to load is reported by `vigiles test` as SKIPPED with exit code 0 — a green
22
- * run that tested nothing. That is not a hypothetical: it is what the first attempt printed.
23
- *
24
- * Because the symlinks point back at `skills/`, an edit to `skills/<n>/SKILL.md` IS the edit the
25
- * harness sees. The driver therefore writes the real file and restores it, verifying both.
26
- *
27
- * ── THE DEFECT EACH CASE PLANTS, AND WHY THAT ONE ───────────────────────────
28
- * Nine of the ten are `checkSkill()` one-liners, so they all fail the same way and one defect
29
- * shape covers them: make the skill announce under a SIBLING'S name. It is the defect the
30
- * assertion exists for — these record blocks are copy-paste templates differing in one token,
31
- * `announce.mjs` validates nothing, and the wrong gate then reads FRESH in `status.mjs` while the
32
- * right one reads NEVER-RUN. Each case names a DIFFERENT sibling so that no two cases could pass
33
- * on one shared accident.
34
- *
35
- * `osf-artifact-upload` is not a `checkSkill()` skill (it carries no `announce.mjs <self>` line —
36
- * it is a utility `submit-paper` composes with, not a stage), so its case plants the defect ITS
37
- * harness is about: a `curl` that loses `-g`, which makes a bracketed OSF parameter glob and the
38
- * request fail silently.
39
- */
40
- import { readdirSync } from "node:fs";
41
- import { dirname, join } from "node:path";
42
- import { fileURLToPath } from "node:url";
43
- import { runMutations } from "../lib/mutation-driver.mjs";
44
- import { consumerRoot } from "./paper-pipeline/scripts/consumer.mjs";
45
-
46
- const HERE = dirname(fileURLToPath(import.meta.url));
47
- const md = (s) => join(HERE, s, "SKILL.md");
48
- const harness = (s) => join(HERE, s, `${s}.harness.mjs`);
49
-
50
- /** [skill, the sibling it is made to announce under] — every sibling distinct. */
51
- const ANNOUNCERS = [
52
- ["analyze-sibling-paper", "map-prior-work"],
53
- ["argument-arc", "tighten-paper"],
54
- ["cold-read-diff", "argument-arc"],
55
- ["draft-paper", "build-benchmark"],
56
- ["map-prior-work", "find-venue"],
57
- ["paper-adversarial-review", "pc-panel-review"],
58
- ["pc-panel-review", "harden-paper"],
59
- ["sweep-design-space", "research-ideate"],
60
- ["tighten-paper", "grade-paper-writing"],
61
- // ── wave ② (2026-09-12). Same defect shape: all twelve are `checkSkill()` stage skills
62
- // carrying exactly one `announce.mjs <self>` line, verified unambiguous before these were
63
- // written. Siblings stay pairwise distinct across the WHOLE list, so no two cases can pass
64
- // on one shared accident.
65
- ["build-benchmark", "draft-paper"],
66
- ["camera-ready", "extend-paper"],
67
- ["harden-paper", "paper-adversarial-review"],
68
- ["paper-status", "render-paper"],
69
- ["extend-paper", "camera-ready"],
70
- ["study-accepted-papers", "analyze-sibling-paper"],
71
- ["verify-citations", "cold-read-diff"],
72
- ["render-paper", "paper-status"],
73
- ["find-venue", "plan-paper-timeline"],
74
- ["research-ideate", "sweep-design-space"],
75
- ["plan-paper-timeline", "study-accepted-papers"],
76
- ["grade-paper-writing", "verify-citations"],
77
- // ── wave ③ (2026-09-12). `submit-paper` is a `checkSkill()` stage skill with exactly one
78
- // `announce.mjs submit-paper` line (verified unambiguous before this was written). Its sibling
79
- // is the conductor itself — the only name left that keeps the whole list pairwise distinct, and
80
- // a fitting one: recording a submission under the orchestrator's gate is precisely the
81
- // copy-paste slip that reads FRESH on the wrong row.
82
- ["submit-paper", "paper-pipeline"],
83
- ];
84
-
85
- // 🔴 "Every sibling is distinct" is an ASSERTION, not a wish stated in a comment. The list is
86
- // written by hand, and exactly this kind of list has already rotted silently before. If two cases
87
- // mutate into the same name, they can pass on one shared accident, and the battery stops being
88
- // able to tell their failures apart.
89
- {
90
- const seen = new Set();
91
- for (const [skill, sibling] of ANNOUNCERS) {
92
- if (skill === sibling)
93
- throw new Error(
94
- `${skill}: the sibling must be DIFFERENT from the skill itself`,
95
- );
96
- if (seen.has(sibling))
97
- throw new Error(
98
- `sibling "${sibling}" is named twice — the cases stopped being independent. ` +
99
- `Each case needs its own, otherwise two of them can pass on one shared accident.`,
100
- );
101
- seen.add(sibling);
102
- }
103
- }
104
-
105
- const CASES = ANNOUNCERS.map(([skill, sibling]) => ({
106
- name: `${skill}: announces as ${sibling}`,
107
- disables:
108
- "the identity check — a run recorded under a sibling's gate, which then reads FRESH",
109
- edits: [[md(skill), `announce.mjs ${skill} `, `announce.mjs ${sibling} `]],
110
- harness: harness(skill),
111
- expect: "its announce command files under",
112
- }));
113
-
114
- CASES.push({
115
- name: "osf-artifact-upload: one curl loses -g",
116
- disables:
117
- "the globbing rule this skill states in its own §gotchas, three paragraphs above the command",
118
- // 🔴 ANCHORED ON THE ONE CALL THAT IS UNIQUE, not on the shared prefix. `curl -g -s --retry 3`
119
- // appears three times, and the driver refuses an ambiguous target rather than guessing — which
120
- // is how the first version of this case reported AMBIGUOUS instead of quietly mutating one of
121
- // three and calling it a kill.
122
- edits: [
123
- [
124
- md("osf-artifact-upload"),
125
- 'curl -g -s --retry 3 --cacert $CA -H "$AUTH" https://api.osf.io/v2/users/me/',
126
- 'curl -s --retry 3 --cacert $CA -H "$AUTH" https://api.osf.io/v2/users/me/',
127
- ],
128
- ],
129
- harness: harness("osf-artifact-upload"),
130
- expect: 'command(s) omit "-g"',
131
- });
132
-
133
- // ── wave ③: the conductor. NOT a `checkSkill()` skill — its harness (`paper-pipeline.harness.mjs`)
134
- // checks ROUTING INTEGRITY, so the defect has to be a routing one, the same way
135
- // `osf-artifact-upload` above gets the defect ITS harness is about.
136
- //
137
- // 🔴 THE FORWARD DIRECTION IS THE ONE A MUTATION CAN REACH. Backward ("a wired stage the map never
138
- // names") cannot be planted by editing the map alone — deleting a route makes the skill un-named,
139
- // which is exactly what the MISSING allowance list tolerates for three skills, so the kill would
140
- // depend on which name was picked. Forward is unconditional: a bolded backticked token in a routing
141
- // table that resolves to no skill directory is a route to nowhere, and a run following the map
142
- // stalls there. `**\`camera-ready\`**` occurs exactly once, so the edit cannot be ambiguous.
143
- CASES.push({
144
- name: "paper-pipeline: the map routes to a skill that does not exist",
145
- disables:
146
- "forward routing integrity — the conductor pointing at a name with no SKILL.md behind it",
147
- edits: [
148
- [md("paper-pipeline"), "**`camera-ready`**", "**`camera-ready-v2`**"],
149
- ],
150
- harness: harness("paper-pipeline"),
151
- expect: "no .claude/skills/camera-ready-v2/SKILL.md exists",
152
- });
153
-
154
- // 🔴 A FLOOR, NOT A COMMENT. The list above is written by hand, and a hand-written list of paths
155
- // is the thing this package replaced in `package.json` on 2026-09-11 because it had already
156
- // rotted SILENTLY. A skill added to `skills/` with a colocated harness and no case here would be
157
- // caught by `run-mutations.mjs` — but only on the run AFTER someone added it, and only if they
158
- // ran it. This asserts the pairing from the other side, at import time.
159
- // 🔴 THE `paper-pipeline` EXCLUSION IS GONE (wave ③, 2026-09-12). It was written when that
160
- // directory held only `scripts/` — no SKILL.md, no colocated harness, nothing for a case to kill.
161
- // Both arrived with wave ③, so the exclusion would now hide the orchestrator from the very floor
162
- // that exists to notice an uncovered harness: `e.name !== "paper-pipeline"` and «it has no harness»
163
- // were the same statement for one day and are opposite statements now.
164
- const withHarness = readdirSync(HERE, { withFileTypes: true })
165
- .filter((e) => e.isDirectory())
166
- .map((e) => e.name)
167
- .filter((n) => readdirSync(join(HERE, n)).includes(`${n}.harness.mjs`));
168
- const covered = new Set(CASES.map((c) => c.harness));
169
- const missing = withHarness.filter((n) => !covered.has(harness(n)));
170
- if (missing.length)
171
- throw new Error(
172
- `skill-contract.mutations.mjs covers no case for: ${missing.join(", ")}.\n` +
173
- `Each skill directory carrying <name>.harness.mjs needs a case here, or that harness is a ` +
174
- `test nothing can kill — green and silent whether or not it still checks anything.`,
175
- );
176
-
177
- process.exit(
178
- runMutations({ root: consumerRoot(), runner: "vigiles", cases: CASES }),
179
- );
@@ -1,196 +0,0 @@
1
- /**
2
- * Battery for `build.ts` — the shell that compiles a paper.
3
- *
4
- * Every case reintroduces a defect this command either really had or exists to prevent, and names
5
- * the assertion that must kill it. Two of them are checked on DISK rather than on the returned
6
- * object, because that is where the damage lands: a paper script that runs anyway, and a stale
7
- * PDF left beside a red build.
8
- */
9
- import { fileURLToPath } from "node:url";
10
- import { dirname, join, resolve } from "node:path";
11
- import { runMutations } from "../lib/mutation-driver.mjs";
12
-
13
- const HERE = dirname(fileURLToPath(import.meta.url));
14
- const ROOT = resolve(HERE, "..");
15
- const SRC = join(HERE, "build.ts");
16
- const PORT = join(ROOT, "skills", "paper-pipeline", "scripts", "consumer.mjs");
17
- const HARNESS = join(HERE, "build.harness.mjs");
18
-
19
- process.exit(
20
- runMutations({
21
- root: ROOT,
22
- runner: "node",
23
- cases: [
24
- {
25
- name: "a leftover build.sh is executed again",
26
- harness: HARNESS,
27
- expect: "the paper's build scripts were NOT executed",
28
- disables:
29
- "the decision this command is built on: paperlint compiles the paper itself and never runs a " +
30
- "file from the paper directory. The mutation keeps the 'is ignored' note and runs the " +
31
- "script anyway — the worst form, because the output still claims it was ignored",
32
- edits: [
33
- [
34
- SRC,
35
- " for (const s of facts.ignoredScripts)\n log(` note: ${s} is ignored — paperlint builds the paper itself`);\n",
36
- ' for (const s of facts.ignoredScripts) {\n log(` note: ${s} is ignored — paperlint builds the paper itself`);\n spawnSync("bash", [join(paperDir, s)], { stdio: "ignore" });\n }\n',
37
- ],
38
- ],
39
- },
40
- {
41
- name: "a failed build keeps the stale PDF",
42
- harness: HARNESS,
43
- expect: "the stale paper.pdf is DELETED",
44
- disables:
45
- "removing paper.pdf on failure. An old PDF beside a red build looks current, and 'the " +
46
- "PDF is there' is exactly what a human checks first",
47
- edits: [
48
- [
49
- SRC,
50
- ' removePdf(paperDir);\n return {\n dir,\n status: "failed",',
51
- ' return {\n dir,\n status: "failed",',
52
- ],
53
- ],
54
- },
55
- {
56
- name: "the stale PDF is no longer removed before the engine is resolved",
57
- harness: HARNESS,
58
- expect:
59
- "no engine: the stale paper.pdf is removed BEFORE the engine is resolved",
60
- disables:
61
- "the one up-front removal. Without it every path that ends before a step — no " +
62
- "qualifying TeX Live, no paper.tex — leaves yesterday's PDF looking like today's",
63
- edits: [
64
- [
65
- SRC,
66
- " const stale = new Set(dryRun ? [] : targets.filter(removePdf));",
67
- " const stale = new Set<string>();",
68
- ],
69
- ],
70
- },
71
- {
72
- name: "`--dry-run` removes the stale PDF too",
73
- harness: HARNESS,
74
- expect: "buildPapers --dry-run: the PDF on disk is untouched",
75
- disables:
76
- "the dry-run guard on the up-front removal. A plan that deletes the author's PDF is " +
77
- "not a plan",
78
- edits: [
79
- [
80
- SRC,
81
- " const stale = new Set(dryRun ? [] : targets.filter(removePdf));",
82
- " const stale = new Set(targets.filter(removePdf));",
83
- ],
84
- ],
85
- },
86
- {
87
- name: "pdflatex exit 0 with no PDF counts as built",
88
- harness: HARNESS,
89
- expect: "empty document: pdflatex exit 0 with no PDF FAILS the build",
90
- disables:
91
- "the existence check. An empty document compiles with exit 0 and no output, and the " +
92
- "result line would print a green paper.pdf that does not exist",
93
- edits: [
94
- [
95
- SRC,
96
- " if (!existsSync(join(ctx.paperDir, `${JOB}.pdf`)))",
97
- " if (false)",
98
- ],
99
- ],
100
- },
101
- {
102
- name: "`--dry-run` runs the build anyway",
103
- harness: HARNESS,
104
- expect: "--dry-run: nothing was started",
105
- disables:
106
- "the one flag whose whole meaning is 'do not touch anything'. Its first version was " +
107
- "swallowed by options destructuring and rewrote paper.pdf while looking like a verbose " +
108
- "dry run",
109
- edits: [
110
- [
111
- SRC,
112
- ' if (dryRun) return { dir, status: "built", plan, dry: true };\n',
113
- "",
114
- ],
115
- ],
116
- },
117
- {
118
- name: "TEXINPUTS loses its trailing separator",
119
- harness: HARNESS,
120
- expect: "with no TEXINPUTS set, the value ENDS in the separator",
121
- disables:
122
- "the empty element that tells kpathsea 'and then the system tree'. Without it " +
123
- "article.cls stops resolving, and every paper fails for a reason unrelated to it",
124
- edits: [
125
- [
126
- SRC,
127
- ' TEXINPUTS: [...dirs, env.TEXINPUTS ?? ""].join(delimiter),',
128
- " TEXINPUTS: [...dirs, ...(env.TEXINPUTS ? [env.TEXINPUTS] : [])].join(delimiter),",
129
- ],
130
- ],
131
- },
132
- {
133
- name: "the port resolves the venues directory one level off",
134
- harness: HARNESS,
135
- expect:
136
- "the venues directory is paperlint's own and holds paper-guards.tex",
137
- disables:
138
- "the one answer to 'where are paperlint's venue files'. A wrong directory does not fail " +
139
- "pdflatex by itself — \\input{paper-guards} would fail later, in a paper, far from here",
140
- edits: [
141
- [
142
- PORT,
143
- '"../../submit-paper/references/venues"',
144
- '"../../submit-paper/references"',
145
- ],
146
- ],
147
- },
148
- {
149
- name: "a paper with no paper.tex stops being a refusal",
150
- harness: HARNESS,
151
- expect: "no paper.tex — status no-source",
152
- disables:
153
- "'nothing to compile' as a failure. Reading it as 'built' is the green run over an " +
154
- "unbuilt paper this command was written against",
155
- edits: [
156
- [
157
- SRC,
158
- " if (plan.some((p) => !p.applies && p.required))",
159
- " if (plan.some((p) => !p.applies && p.required && false))",
160
- ],
161
- ],
162
- },
163
- {
164
- name: "the final pass stops defining \\finalpass",
165
- harness: HARNESS,
166
- expect: "the FINAL pass defines \\finalpass",
167
- disables:
168
- "the switch that arms paper-guards.tex. Without it an undefined \\ref or \\cite builds " +
169
- "green and ships as '??' in the PDF",
170
- edits: [
171
- [
172
- SRC,
173
- " `\\\\def\\\\finalpass{}\\\\input{${MAIN}}`,\n",
174
- " MAIN,\n",
175
- ],
176
- ],
177
- },
178
- {
179
- name: "the failure drops the log excerpt",
180
- harness: HARNESS,
181
- expect:
182
- "…quotes the first error line and the l.NNN context from the log",
183
- disables:
184
- "the reason a failure is actionable: the error line and where in the source it is. " +
185
- "'pdflatex exited with 1' alone sends the author to a 2 000-line log",
186
- edits: [
187
- [
188
- SRC,
189
- ' ...(end.lines.length\n ? end.lines\n : ["(no error line found in the log)"]),\n',
190
- ' "(no error line found in the log)",\n',
191
- ],
192
- ],
193
- },
194
- ],
195
- }),
196
- );