paperlint 3.1.0 → 3.1.2

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 (311) hide show
  1. package/.github/workflows/ci.yml +28 -41
  2. package/.github/workflows/dependabot-automerge.yml +1 -1
  3. package/CLAUDE.md +21 -105
  4. package/CONTRIBUTING.md +23 -25
  5. package/README.md +11 -7
  6. package/bin/paperlint.test.mjs +45 -0
  7. package/dist/adapters/node/files.io.d.ts +2 -0
  8. package/dist/adapters/node/files.io.d.ts.map +1 -1
  9. package/dist/adapters/node/files.io.js +3 -2
  10. package/dist/adapters/node/files.io.js.map +1 -1
  11. package/dist/adapters/pdfjs/fill.d.ts +15 -4
  12. package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
  13. package/dist/adapters/pdfjs/fill.js +31 -21
  14. package/dist/adapters/pdfjs/fill.js.map +1 -1
  15. package/dist/adapters/references/clock.io.d.ts +5 -0
  16. package/dist/adapters/references/clock.io.d.ts.map +1 -0
  17. package/dist/adapters/references/clock.io.js +7 -0
  18. package/dist/adapters/references/clock.io.js.map +1 -0
  19. package/dist/adapters/references/index.d.ts +24 -2
  20. package/dist/adapters/references/index.d.ts.map +1 -1
  21. package/dist/adapters/references/index.js +131 -12
  22. package/dist/adapters/references/index.js.map +1 -1
  23. package/dist/adapters/references/pool.d.ts +8 -0
  24. package/dist/adapters/references/pool.d.ts.map +1 -0
  25. package/dist/adapters/references/pool.js +19 -0
  26. package/dist/adapters/references/pool.js.map +1 -0
  27. package/dist/build.d.ts +14 -2
  28. package/dist/build.d.ts.map +1 -1
  29. package/dist/build.js +17 -11
  30. package/dist/build.js.map +1 -1
  31. package/dist/cli.d.ts +27 -1
  32. package/dist/cli.d.ts.map +1 -1
  33. package/dist/cli.js +71 -55
  34. package/dist/cli.js.map +1 -1
  35. package/dist/doctor.d.ts +27 -1
  36. package/dist/doctor.d.ts.map +1 -1
  37. package/dist/doctor.js +4 -3
  38. package/dist/doctor.js.map +1 -1
  39. package/dist/domain/lookup-cache.d.ts +75 -0
  40. package/dist/domain/lookup-cache.d.ts.map +1 -0
  41. package/dist/domain/lookup-cache.js +137 -0
  42. package/dist/domain/lookup-cache.js.map +1 -0
  43. package/dist/domain/result.d.ts +0 -2
  44. package/dist/domain/result.d.ts.map +1 -1
  45. package/dist/domain/result.js +0 -4
  46. package/dist/domain/result.js.map +1 -1
  47. package/dist/hooks-settings.d.ts +8 -5
  48. package/dist/hooks-settings.d.ts.map +1 -1
  49. package/dist/hooks-settings.js +32 -32
  50. package/dist/hooks-settings.js.map +1 -1
  51. package/dist/init.d.ts +8 -0
  52. package/dist/init.d.ts.map +1 -1
  53. package/dist/init.js +3 -3
  54. package/dist/init.js.map +1 -1
  55. package/dist/latex-log.d.ts +0 -2
  56. package/dist/latex-log.d.ts.map +1 -1
  57. package/dist/latex-log.js +7 -13
  58. package/dist/latex-log.js.map +1 -1
  59. package/dist/link-skills.d.ts +15 -2
  60. package/dist/link-skills.d.ts.map +1 -1
  61. package/dist/link-skills.js +36 -20
  62. package/dist/link-skills.js.map +1 -1
  63. package/dist/paper-files.d.ts.map +1 -1
  64. package/dist/paper-files.js +6 -4
  65. package/dist/paper-files.js.map +1 -1
  66. package/dist/pdf-facts.d.ts +11 -0
  67. package/dist/pdf-facts.d.ts.map +1 -1
  68. package/dist/pdf-facts.js +16 -18
  69. package/dist/pdf-facts.js.map +1 -1
  70. package/dist/pdf-geometry.js +1 -1
  71. package/dist/pdf-geometry.js.map +1 -1
  72. package/dist/ports/check-references.d.ts +12 -2
  73. package/dist/ports/check-references.d.ts.map +1 -1
  74. package/dist/ports/check-references.js +0 -5
  75. package/dist/ports/check-references.js.map +1 -1
  76. package/dist/presets.d.ts +2 -0
  77. package/dist/presets.d.ts.map +1 -1
  78. package/dist/presets.js +3 -1
  79. package/dist/presets.js.map +1 -1
  80. package/dist/references.d.ts +11 -3
  81. package/dist/references.d.ts.map +1 -1
  82. package/dist/references.js +63 -15
  83. package/dist/references.js.map +1 -1
  84. package/dist/tex-requirements.d.ts +8 -0
  85. package/dist/tex-requirements.d.ts.map +1 -1
  86. package/dist/tex-requirements.js +6 -5
  87. package/dist/tex-requirements.js.map +1 -1
  88. package/dist/toolchain.d.ts +3 -2
  89. package/dist/toolchain.d.ts.map +1 -1
  90. package/dist/toolchain.js +4 -5
  91. package/dist/toolchain.js.map +1 -1
  92. package/dist/venue-rules.d.ts.map +1 -1
  93. package/dist/venue-rules.js +11 -9
  94. package/dist/venue-rules.js.map +1 -1
  95. package/docs/prior-art/README.md +11 -10
  96. package/docs/prior-art/citation-checkers.md +174 -0
  97. package/docs/prior-art/test-tooling.md +7 -30
  98. package/docs/references.md +126 -0
  99. package/docs/testing.md +118 -0
  100. package/docs/toolchain.md +6 -4
  101. package/eslint-rules/install-path-literals.harness.mjs +17 -1
  102. package/eslint-rules/latex-language.harness.mjs +3 -3
  103. package/eslint-rules/latex-language.mjs +12 -9
  104. package/eslint-rules/latex-language.test.mjs +200 -0
  105. package/eslint-rules/paper-research-question.harness.mjs +73 -7
  106. package/eslint-rules/paper-stages.harness.mjs +9 -14
  107. package/eslint-rules/paper-stages.test.mjs +135 -0
  108. package/eslint-rules/paper-typography.mjs +9 -2
  109. package/eslint-rules/paper-typography.test.mjs +146 -1
  110. package/eslint-rules/pdf-last-page-balance.harness.mjs +63 -7
  111. package/eslint-rules/review-frontmatter.mjs +2 -1
  112. package/eslint-rules/review-frontmatter.test.mjs +37 -1
  113. package/eslint-rules/temp-root-realpath.harness.mjs +49 -0
  114. package/eslint-rules/tex-build.mjs +13 -13
  115. package/eslint.config.mjs +6 -2
  116. package/hooks/hooks.harness.mjs +6 -11
  117. package/hooks/paper-edit-guard.hook.mjs +2 -2
  118. package/hooks/paper-edit-guard.test.mjs +61 -0
  119. package/hooks/paper-skills-nudge.hook.mjs +2 -2
  120. package/hooks/paper-skills-nudge.test.mjs +38 -0
  121. package/hooks/paper-status-gates.test.mjs +42 -0
  122. package/lib/agent-cli-version.harness.mjs +7 -0
  123. package/lib/check.d.mts +17 -0
  124. package/lib/check.mjs +50 -0
  125. package/lib/check.test.ts +67 -0
  126. package/lib/markdown.mjs +16 -69
  127. package/lib/markdown.test.mjs +246 -0
  128. package/lib/paper-config.harness.mjs +28 -9
  129. package/lib/paper-config.mjs +7 -2
  130. package/lib/skill-checks.mjs +18 -46
  131. package/lib/skill-checks.test.mjs +297 -0
  132. package/lib/skill-corpus.mjs +11 -13
  133. package/lib/skill-corpus.test.mjs +54 -0
  134. package/lib/skill-trigger-cases.harness.mjs +57 -4
  135. package/package.json +6 -5
  136. package/scripts/check.harness.mjs +4 -10
  137. package/scripts/check.mjs +45 -42
  138. package/scripts/check.test.mjs +104 -0
  139. package/scripts/coverage-config.test.ts +406 -0
  140. package/scripts/eslint-report-guard.test.mjs +48 -0
  141. package/scripts/harness-api.frozen.json +0 -2
  142. package/scripts/layer-legacy-frozen.d.mts +9 -1
  143. package/scripts/layer-legacy-frozen.mjs +28 -11
  144. package/scripts/layer-legacy-frozen.test.ts +143 -1
  145. package/scripts/rules-are-content-only.harness.mjs +3 -11
  146. package/scripts/rules-are-content-only.mjs +29 -19
  147. package/scripts/rules-are-content-only.test.mjs +61 -0
  148. package/scripts/rules-see-files.mjs +11 -9
  149. package/scripts/rules-see-files.test.mjs +71 -0
  150. package/scripts/test.mjs +44 -0
  151. package/scripts/test.test.mjs +52 -0
  152. package/skills/grade-paper-writing/__snapshots__/prose-lint.test.mjs.snap +57 -0
  153. package/skills/grade-paper-writing/prose-lint.mjs +31 -12
  154. package/skills/grade-paper-writing/prose-lint.test.mjs +153 -0
  155. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +3 -3
  156. package/skills/paper-pipeline/references/artifact-checklist.md +7 -7
  157. package/skills/paper-pipeline/scripts/README.md +13 -132
  158. package/skills/paper-pipeline/scripts/announce.test.mjs +102 -0
  159. package/skills/paper-pipeline/scripts/artifact-coverage.test.mjs +86 -0
  160. package/skills/paper-pipeline/scripts/consumer.d.mts +8 -1
  161. package/skills/paper-pipeline/scripts/consumer.harness.mjs +3 -3
  162. package/skills/paper-pipeline/scripts/consumer.mjs +13 -25
  163. package/skills/paper-pipeline/scripts/consumer.test.mjs +113 -0
  164. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +9 -4
  165. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +380 -0
  166. package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +71 -0
  167. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +4 -4
  168. package/skills/paper-pipeline/scripts/generated-code.mjs +3 -2
  169. package/skills/paper-pipeline/scripts/generated-code.test.mjs +62 -0
  170. package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +302 -0
  171. package/skills/paper-pipeline/scripts/ledger.mjs +1 -1
  172. package/skills/paper-pipeline/scripts/{ledger.selftest.mjs → ledger.test.mjs} +44 -76
  173. package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +474 -0
  174. package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -63
  175. package/skills/paper-pipeline/scripts/pipeline-check.mjs +37 -21
  176. package/skills/paper-pipeline/scripts/pipeline-edges.mjs +2 -2
  177. package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +73 -0
  178. package/skills/paper-pipeline/scripts/population-map.harness.mjs +4 -47
  179. package/skills/paper-pipeline/scripts/population-map.mjs +12 -2
  180. package/skills/paper-pipeline/scripts/population-map.test.mjs +154 -0
  181. package/skills/paper-pipeline/scripts/provenance.harness.mjs +26 -0
  182. package/skills/paper-pipeline/scripts/round-diff.harness.mjs +1 -2
  183. package/skills/paper-pipeline/scripts/round-diff.mjs +13 -6
  184. package/skills/paper-pipeline/scripts/round-diff.test.mjs +159 -0
  185. package/skills/paper-pipeline/scripts/run-mechanical.mjs +30 -6
  186. package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +468 -0
  187. package/skills/paper-pipeline/scripts/status.mjs +2 -2
  188. package/skills/paper-pipeline/scripts/status.test.mjs +232 -0
  189. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +2 -2
  190. package/skills/render-paper/checkers-requirements.txt +30 -19
  191. package/skills/render-paper/ensure-checkers.sh +20 -4
  192. package/skills/render-paper/extract-pdf-facts.harness.mjs +56 -7
  193. package/skills/tighten-paper/__snapshots__/structure.test.mjs.snap +133 -0
  194. package/skills/tighten-paper/structure.test.mjs +116 -0
  195. package/skills/verify-citations/SKILL.md +30 -14
  196. package/skills/verify-citations/SKILL.md.spec.ts +29 -13
  197. package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +253 -0
  198. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +274 -0
  199. package/skills/verify-citations/scripts/bib-authors.cli.test.mjs +178 -0
  200. package/skills/verify-citations/scripts/bib-authors.mjs +6 -6
  201. package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
  202. package/skills/verify-citations/scripts/verify-cites.mjs +360 -215
  203. package/skills/verify-citations/scripts/verify-cites.net.test.mjs +588 -0
  204. package/skills/verify-citations/scripts/verify-cites.test.mjs +151 -5
  205. package/src/adapters/banal/failure.test.ts +70 -1
  206. package/src/adapters/banal/index.test.ts +16 -0
  207. package/src/adapters/banal/output.test.ts +5 -0
  208. package/src/adapters/curl/download.test.ts +43 -1
  209. package/src/adapters/memory/index.test.ts +25 -0
  210. package/src/adapters/node/files.io.ts +3 -2
  211. package/src/adapters/node/files.test.ts +13 -1
  212. package/src/adapters/node/process.test.ts +38 -0
  213. package/src/adapters/pdfjs/fill.test.ts +34 -0
  214. package/src/adapters/pdfjs/fill.ts +44 -25
  215. package/src/adapters/references/clock.io.ts +8 -0
  216. package/src/adapters/references/index.test.ts +560 -0
  217. package/src/adapters/references/index.ts +203 -19
  218. package/src/adapters/references/pool.test.ts +31 -0
  219. package/src/adapters/references/pool.ts +24 -0
  220. package/src/build-engine.harness.mjs +3 -7
  221. package/src/build-engine.test.ts +137 -0
  222. package/src/build.harness.mjs +5 -9
  223. package/src/build.test.ts +362 -0
  224. package/src/build.ts +23 -12
  225. package/src/cli.harness.mjs +10 -17
  226. package/src/cli.test.ts +584 -0
  227. package/src/cli.ts +112 -66
  228. package/src/doctor.harness.mjs +3 -8
  229. package/src/doctor.test.ts +64 -0
  230. package/src/doctor.ts +12 -4
  231. package/src/domain/lookup-cache.test.ts +178 -0
  232. package/src/domain/lookup-cache.ts +202 -0
  233. package/src/domain/result.test.ts +4 -0
  234. package/src/domain/result.ts +0 -5
  235. package/src/engine.harness.mjs +27 -7
  236. package/src/facts-file.test.ts +110 -0
  237. package/src/hooks-settings.harness.mjs +43 -13
  238. package/src/hooks-settings.ts +41 -35
  239. package/src/init.test.ts +308 -5
  240. package/src/init.ts +3 -3
  241. package/src/latex-log.harness.mjs +32 -7
  242. package/src/latex-log.ts +6 -13
  243. package/src/latex-loop.harness.mjs +8 -7
  244. package/src/link-skills.harness.mjs +3 -8
  245. package/src/link-skills.test.ts +105 -0
  246. package/src/link-skills.ts +75 -21
  247. package/src/new-paper.harness.mjs +3 -7
  248. package/src/new-paper.test.ts +31 -1
  249. package/src/paper-files.test.ts +12 -0
  250. package/src/paper-files.ts +9 -5
  251. package/src/paper-settings.test.ts +20 -0
  252. package/src/pdf-facts.harness.mjs +3 -6
  253. package/src/pdf-facts.test.ts +144 -0
  254. package/src/pdf-facts.ts +17 -19
  255. package/src/pdf-geometry.harness.mjs +3 -7
  256. package/src/pdf-geometry.ts +1 -1
  257. package/src/ports/check-references.ts +16 -2
  258. package/src/presets.test.ts +2 -0
  259. package/src/presets.ts +7 -1
  260. package/src/references.test.ts +257 -10
  261. package/src/references.ts +93 -17
  262. package/src/rules-config.harness.mjs +10 -8
  263. package/src/structure.harness.mjs +3 -7
  264. package/src/tex-requirements.harness.mjs +34 -7
  265. package/src/tex-requirements.ts +12 -6
  266. package/src/toolchain.harness.mjs +3 -7
  267. package/src/toolchain.test.ts +280 -0
  268. package/src/toolchain.ts +8 -7
  269. package/src/venue-rules.test.ts +60 -0
  270. package/src/venue-rules.ts +13 -11
  271. package/action.mutations.mjs +0 -162
  272. package/eslint-rules/install-path-literals.mutations.mjs +0 -62
  273. package/eslint-rules/latex-language.mutations.mjs +0 -196
  274. package/eslint-rules/paper-research-question.mutations.mjs +0 -127
  275. package/eslint-rules/paper-stages.mutations.mjs +0 -81
  276. package/eslint-rules/papers.mutations.mjs +0 -186
  277. package/eslint-rules/temp-root-realpath.mutations.mjs +0 -99
  278. package/eslint-rules/tex-build.mutations.mjs +0 -258
  279. package/hooks/hooks.mutations.mjs +0 -337
  280. package/lib/agent-cli-version.mutations.mjs +0 -109
  281. package/lib/mutation-driver.harness.mjs +0 -227
  282. package/lib/mutation-driver.mjs +0 -397
  283. package/lib/mutation-driver.mutations.mjs +0 -68
  284. package/lib/paper-config.mutations.mjs +0 -143
  285. package/lib/skill-trigger-cases.mutations.mjs +0 -65
  286. package/scripts/check.mutations.mjs +0 -110
  287. package/scripts/exclusive.mjs +0 -138
  288. package/scripts/mutation-batteries-frozen.harness.mjs +0 -204
  289. package/scripts/mutation-batteries-frozen.mjs +0 -238
  290. package/scripts/mutation-batteries.frozen.json +0 -108
  291. package/scripts/rules-are-content-only.mutations.mjs +0 -81
  292. package/scripts/rules-see-files.mutations.mjs +0 -131
  293. package/scripts/run-mutations.mjs +0 -100
  294. package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +0 -218
  295. package/skills/paper-pipeline/scripts/consumer.mutations.mjs +0 -190
  296. package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +0 -54
  297. package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +0 -254
  298. package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +0 -54
  299. package/skills/paper-pipeline/scripts/population-map.mutations.mjs +0 -65
  300. package/skills/paper-pipeline/scripts/population-map.selftest.mjs +0 -122
  301. package/skills/paper-pipeline/scripts/provenance.mutations.mjs +0 -59
  302. package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +0 -276
  303. package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +0 -65
  304. package/skills/skill-contract.mutations.mjs +0 -179
  305. package/src/build.mutations.mjs +0 -196
  306. package/src/cli.mutations.mjs +0 -414
  307. package/src/doctor.mutations.mjs +0 -175
  308. package/src/hooks-settings.mutations.mjs +0 -99
  309. package/src/link-skills.mutations.mjs +0 -140
  310. package/src/new-paper.mutations.mjs +0 -65
  311. package/src/structure.mutations.mjs +0 -83
@@ -3,7 +3,7 @@
3
3
  # ── WHY ONE JOB ─────────────────────────────────────────────────────────────
4
4
  # Parallelism does not save billable minutes, it ADDS them: every job pays for its own
5
5
  # setup + checkout + install (~20-30 s). Splitting is worth it for signal isolation on a big
6
- # matrix; here the whole suite is 84 seconds, so a second job would cost more than it isolates.
6
+ # matrix; here the whole suite is ~96 seconds, so a second job would cost more than it isolates.
7
7
  #
8
8
  # ⛽ THIS REPOSITORY IS PUBLIC, so its minutes are FREE — they do not touch the account's
9
9
  # 3000/month quota (verified against the API 2026-09-12: `"private": false`). The header said
@@ -11,23 +11,24 @@
11
11
  # rather than guessed. Keep measuring them anyway: the bond exists to stop a HANG, and a hang
12
12
  # costs wall-clock and a red check whether or not anyone is billed.
13
13
  #
14
- # ── THE MEASUREMENT THE BONDS COME FROM (local, 2026-09-12) ─────────────────
15
- # npm test (12 harnesses) 11 s
16
- # npm run lint (eslint over fixtures) 1 s
17
- # rules-see-files.mjs 1 s
18
- # vigiles lint 3 s ← added 2026-09-12 with the first ten skills
19
- # run-mutations.mjs (12 batteries) 242 s ← 130 mutations, the expensive and load-bearing part
14
+ # ── THE MEASUREMENT THE BONDS COME FROM (local, 4 cores, 2026-09-26) ─────────
15
+ # npm run build 2 s
16
+ # npm test -- --no-skip (69 harnesses) 41 s ← vitest + every harness
17
+ # npm run lint 6 s
18
+ # npm run fmt:check 5 s
19
+ # vigiles lint . README.md 6 s
20
+ # rules-see-files.mjs 5 s
21
+ # layer-legacy-frozen.mjs 3 s
22
+ # install e2e --strict (npm + pnpm) 28 s
20
23
  # ───────────────────────────────────────────
21
- # total work ~255 s, plus checkout + install
24
+ # total work ~96 s, plus checkout + install + the claude binary
22
25
  #
23
- # 🔴 THE PREVIOUS NUMBERS ARE KEPT NOWHERE BUT THIS SENTENCE, ON PURPOSE: they said ~84 s total
24
- # with `timeout-minutes: 8`, i.e. a ~2.7× bond. The pipeline's checkers arrived on 2026-09-12 and
25
- # tripled every count — 4 harnesses became 12, 45 mutations became 130 — which would have left an
26
- # 8-minute bond at ~1.3× of a ~6-minute run. A bond that tight fails on a slow runner and reads
27
- # as a broken suite rather than as a bond. Re-measure when the counts move again; do not cite the
28
- # table above without re-running it.
26
+ # 🔴 THE PREVIOUS TABLE (2026-09-12) had one more row, `run-mutations.mjs`, at 242 s of ~255 s, and
27
+ # by 2026-09-23 it was 8m39s of a 10m02s job (#52). The batteries were removed in #52, which is
28
+ # what took the job from `timeout-minutes: 20` to 10. Re-measure when the counts move again; do not
29
+ # cite the table above without re-running it.
29
30
  #
30
- # Expect ~6 minutes warm. `timeout-minutes: 20` is ~3.3×, and `npm ci` carries its OWN bond
31
+ # Expect ~3 minutes warm. `timeout-minutes: 10` is ~3×, and `npm ci` carries its OWN bond
31
32
  # because a job-level bond cannot tell "the registry hung" from "the suite is slow" and so hands
32
33
  # a hang the whole job.
33
34
  name: ci
@@ -62,7 +63,7 @@ jobs:
62
63
  # there is no pull_request object, and a bare `.draft` would be missing and kill every check there.
63
64
  if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
64
65
  runs-on: ubuntu-latest
65
- timeout-minutes: 20
66
+ timeout-minutes: 10
66
67
  steps:
67
68
  - uses: actions/checkout@v7
68
69
 
@@ -88,14 +89,10 @@ jobs:
88
89
  # i.e., claimed a protection that does not exist. Protection against this case comes from `--no-skip` below.
89
90
  # 🔴 THE `claude` CLI IS A TEST DEPENDENCY HERE, and its absence was NOT harmless.
90
91
  # `plan-paper-timeline.effects.harness.mjs` spawns the real agent binary; without it the
91
- # harness SKIPS, the skip exits 0 through `vigiles test`, and the mutation driver interprets
92
- # that as "the mutation survived". Measured 2026-09-15 on run 34966606186: two survivors in CI,
93
- # zero locally, the difference being this binary and nothing about the code.
94
- #
95
- # The driver now REFUSES a battery whose harness skips (lib/mutation-driver.mjs), so the
96
- # wrong diagnosis is gone either way. This step is the other half: it restores the evidence
97
- # instead of settling for an honest "no evidence". No API key — the tier drives a scripted
98
- # stand-in, which is why it costs seconds and $0.
92
+ # harness SKIPS, and a skip is not evidence. (Measured 2026-09-15 on run 34966606186, when the
93
+ # since-removed mutation batteries read that skip as two "survivors" in CI and none locally.)
94
+ # This step restores the evidence instead of settling for an honest "no evidence". No API
95
+ # key — the tier drives a scripted stand-in, which is why it costs seconds and $0.
99
96
  # 🔴 DELIBERATELY UNPINNED, and since issue #7 that is finally honest. The tier exists to
100
97
  # notice upstream moving, so a pin would defeat it — but until the version was OBSERVED
101
98
  # rather than probed for, an unpinned install meant the assertions characterized whatever
@@ -110,14 +107,16 @@ jobs:
110
107
  # that failed to LOAD. In other words, one flag closes both doors at once.
111
108
  #
112
109
  # ⚠️ The step order was incorrect and made the install above nearly pointless: `npm test`
113
- # ran BEFORE it, so the effects tier in this job was always skipped, and the binary
114
- # reached only the mutation batteries. The comment about "its absence was NOT harmless"
110
+ # ran BEFORE it, so the effects tier in this job was always skipped. The comment about "its absence was NOT harmless"
115
111
  # described exactly this harm yet did not protect against it itself.
116
112
  #
117
113
  # ⚠️ And the flag belongs IN THE STEP, not in `package.json`: the macOS cell of build-e2e runs the same `npm test`
118
114
  # WITHOUT installing the binary, where skips are legitimate, and a global flag would falsely color it.
119
- - name: npm test
120
- run: npm test -- --no-skip
115
+ #
116
+ # `npm run coverage` is `npm test` under c8 with the thresholds in .c8rc.json: the same tests,
117
+ # one run, and a drop below the coverage floor fails this step by name.
118
+ - name: npm test, under coverage
119
+ run: npm run coverage -- --no-skip
121
120
 
122
121
  # The rules themselves, over this repository's own fixtures. Two of the four fixtures are
123
122
  # DEFECTIVE ON PURPOSE, so a healthy checkout reports warnings here and still exits 0 —
@@ -161,22 +160,11 @@ jobs:
161
160
  - name: every rule sees a file
162
161
  run: node scripts/rules-see-files.mjs
163
162
 
164
- # Hand-written mutation batteries are being removed (#52): the frozen list may only
165
- # shrink — no new battery, no new case in an existing one.
166
- - name: mutation batteries are frozen
167
- run: node scripts/mutation-batteries-frozen.mjs
168
-
169
163
  # The legacy exemptions from src/'s layer rules (#76) may only shrink: counted from ESLint's
170
164
  # own suppressedMessages, so a new `-- legacy …` directive anywhere fails here.
171
165
  - name: legacy layer exemptions are frozen
172
166
  run: node scripts/layer-legacy-frozen.mjs
173
167
 
174
- # The batteries that remain still run until #52 replaces or removes them. The runner
175
- # DISCOVERS batteries rather than listing them and errors on finding zero — see
176
- # scripts/run-mutations.mjs for why (the hand list it replaced had gone stale, silently).
177
- - name: mutation batteries
178
- run: node scripts/run-mutations.mjs
179
-
180
168
  # pnpm is INSTALLED here rather than hoped for. The runner image may or may not carry it,
181
169
  # and the npm/pnpm difference is the entire reason the install e2e exists — so leaving it
182
170
  # to the image means the half that matters most is covered by luck. `corepack` ships with
@@ -244,8 +232,7 @@ jobs:
244
232
  : "DIVERGE — this is the platform this job exists for");
245
233
  '
246
234
 
247
- # Issue #9: `npm test` was red ONLY on macOS. Only `npm test`, not the mutation batteries:
248
- # they would verify on macOS exactly what the gates job verifies on Linux.
235
+ # Issue #9: `npm test` was red ONLY on macOS.
249
236
  - name: npm test (macOS)
250
237
  if: runner.os == 'macOS'
251
238
  run: npm test
@@ -16,7 +16,7 @@
16
16
  #
17
17
  # 🔴 WHY WE MERGE ANY BUMP, INCLUDING MAJOR, not just patch/minor.
18
18
  # Semver is a PUBLISHER'S CLAIM about their release; `ci` is a MEASUREMENT on our corpus (full harness suite,
19
- # mutations, gates for "rules see their files" and "rules don't touch git"). A major bump that breaks us
19
+ # gates for "rules see their files" and "rules don't touch git"). A major bump that breaks us
20
20
  # reds the run and does not merge; a minor one that breaks us does the same.
21
21
  # A version-number filter adds a manual step where measurement already exists, and would miss
22
22
  # exactly the class for which version filters are typically used.
package/CLAUDE.md CHANGED
@@ -35,7 +35,6 @@ assumed. Two things that sound like limits and are not:
35
35
  | ----------------------- | -----------------------------------------------------------------------------------------: |
36
36
  | ESLint rules | **5** — `latex-language` · `tex-build` · `papers` · `review-findings-cause` · `doc-fields` |
37
37
  | harnesses | **57** |
38
- | mutation batteries | **34** |
39
38
  | skills | **24** |
40
39
  | hooks (runnable `.mjs`) | **5** |
41
40
  | repo-wide scripts | 5 |
@@ -75,8 +74,8 @@ nodes.
75
74
 
76
75
  **3. Every check needs BOTH halves, or it is not tested.** It must FIRE on a planted defect
77
76
  and stay QUIET on a clean fixture. A check that has only been seen quiet is
78
- indistinguishable from a dead one — silence is its success state. Prove the fire half with a
79
- mutation, and assert the patch actually landed before trusting a green run.
77
+ indistinguishable from a dead one — silence is its success state. Prove the fire half by
78
+ seeing the test go RED before the fix lands ([`docs/testing.md`](docs/testing.md)).
80
79
 
81
80
  **4. `exit 0` with empty output is NOT "clean".** A rule whose glob matched no files reports
82
81
  exactly like a rule that passed. Any rule shipped here must be loud when its input set is
@@ -189,6 +188,12 @@ the instruction and never the evidence — the same split as rules 9 and 10 and
189
188
  🔴 The argument "we need another command for X" is, in every tool examined, an argument that
190
189
  the CONFIG is not declaring something. Check that before adding a verb.
191
190
 
191
+ Rejected on 2026-09-27, so it is not proposed again: a separate `paperlint refs` command for the
192
+ reference check. The slowness it would have moved (#107) was a cache thrown away after every build,
193
+ not a missing verb. The fix keeps the check in `build` and commits what the services answered in
194
+ `<paper>/repro/references-cache.json` — **raw responses, never verdicts**: verdicts are derived
195
+ again on every build, so a checker fix reaches every paper ([`docs/references.md`](docs/references.md)).
196
+
192
197
  ## Distribution — one install path: npm, then `paperlint init`
193
198
 
194
199
  `npm i -D paperlint` brings all the code — rules, skills, hooks, scripts. `paperlint init`
@@ -461,26 +466,8 @@ node scripts/rules-see-files.mjs # also part of npm run check
461
466
  It is per RULE, not per glob, and that distinction is the point: a rule can be enabled in one
462
467
  block whose glob is empty while a different block is busy, so "some glob matched something" is
463
468
  not evidence about the rule you care about. Both halves are tested
464
- (`scripts/rules-see-files.harness.mjs`) and both directions are mutated
465
- (`scripts/rules-see-files.mutations.mjs` — under-reporting and over-reporting must die on
466
- _different_ assertions, or only one half of the guard is really tested).
467
-
468
- ## Mutations — hand-written batteries are deprecated (#52)
469
-
470
- The `*.mutations.mjs` batteries (string replacements of source lines, run through
471
- `lib/mutation-driver.mjs`) are being removed. The idea stays — a test must be seen going red
472
- when the code breaks — but the vehicle is not this one.
473
-
474
- - **Do not create a new `*.mutations.mjs`**, and **do not add cases to an existing one.**
475
- - **Record what a test guards as a comment directly above its assertion** (`// Guards: …`).
476
- - The rule is enforced, not asked for: `scripts/mutation-batteries-frozen.mjs` (part of
477
- `npm run check` and CI) fails on a battery missing from `scripts/mutation-batteries.frozen.json`,
478
- on a listed battery with more or fewer cases than recorded, and on a listed file that is gone.
479
- The list may only shrink — delete a battery or a case, then delete or lower its entry.
480
- - The intended replacement is a real mutation-testing tool (StrykerJS) or nothing; that is
481
- decided in #52, not in a pull request that happens to touch a battery.
482
-
483
- The batteries that remain still run (`node scripts/run-mutations.mjs`) until #52 retires them.
469
+ (`scripts/rules-see-files.harness.mjs`), under-reporting and over-reporting each by an
470
+ assertion of its own.
484
471
 
485
472
  ## Cost
486
473
 
@@ -506,88 +493,17 @@ justified two hundred lines above precisely by these minutes being free.
506
493
 
507
494
  ## Testing
508
495
 
509
- ```bash
510
- npm test # vitest over *.test.ts, then the vigiles harnesses
511
- npx vitest run <file> # one unit test
512
- npx vigiles test <file> # one harness
513
- ```
514
-
515
- **Two kinds of test, told apart by what the file imports.** A `*.harness.*` file tests the agent
516
- surface and imports `runHook`, `runHarnessTest` or `runEval` from vigiles; everything else is a plain
517
- unit test, `*.test.ts`, run by vitest — and new tests are TypeScript. Older harnesses that
518
- import none of the three are frozen in `scripts/harness-api.frozen.json`, which only shrinks;
519
- `scripts/harness-api.test.ts` parses every harness's imports and holds both halves (#77). vitest
520
- exits 1 when no file matches, and it transpiles without type-checking, so `npm run check` runs
521
- `tsc -p tsconfig.test.json` as its own gate.
522
-
523
- **Local = the fast gates on what you touched; the full `npm run check` = CI.** Locally run vitest on
524
- the touched test files, `npx tsc --noEmit`, `npx eslint <touched files>` and `npm run fmt:check`, then
525
- push and read CI by job name. The repo is public, so CI is free, and it also runs the TeX e2e that
526
- cannot run locally; a local full run takes 10-15 min, most of it the mutation batteries (#52).
527
-
528
- ⚠️ **Not `vigiles test .`** — the `.` is read as a FILE, the runner dies with
529
- `ERR_UNSUPPORTED_DIR_IMPORT`, and it still exits 0. See the measured table below.
530
-
531
- Skills are tested **through vigiles** — a colocated `<skill>.harness.mjs` beside the skill.
532
- (This read «Skills, if and when they arrive» until 2026-09-17; there are 24 of them under
533
- `skills/` carrying a `SKILL.md`, and the README's opening line claimed the repository was
534
- empty — issue #6.) Not through a bespoke script: a home-grown runner here once printed
535
- confident, byte-identical "clean" verdicts for three different skills that had never loaded.
536
-
537
- ### Every npm script takes an exclusive lock, and that is not ceremony
538
-
539
- `npm run *` in this repository goes through `scripts/exclusive.mjs`, which holds
540
- `.vigiles/exclusive.lock` for the duration. A second gate started while one is running does not
541
- queue and does not race — it **refuses**, names the holder, and exits 3.
542
-
543
- 🔴 **The reason is that the mutation batteries edit the working tree in place.** That strategy is
544
- deliberate (see `lib/mutation-driver.mjs` — copying the repo per mutation costs minutes instead
545
- of seconds), and its one cost is that any parallel reader sees a source file mid-mutation. The
546
- resulting failure is **false, non-deterministic, and blames the wrong file**: it reports a broken
547
- assertion, not a mutation, and it reads as "the suite is flaky". That has already cost a wrong
548
- conclusion here — two runs in a row produced _different_ error messages and the diagnosis "I broke
549
- round-diff" was incorrect.
550
-
551
- A prose instruction "don't run them at the same time" existed and did not work: prose does not
552
- execute, so it does not apply to the person in the other terminal, the agent, or the editor with
553
- tests on save. Measured live, with the batteries running:
554
-
555
- ```
556
- $ npm test
557
- 🔴 refused: this repository is busy with a run that EDITS FILES IN PLACE.
558
- held by: pid 6645, "node scripts/run-mutations.mjs", since 2026-09-17T05:22:28.757Z
559
- RC=3
560
- ```
561
-
562
- ⚠️ A lock left behind by a process that no longer exists is **taken over** with a message, not
563
- respected. Otherwise one interrupted run would block the repository forever, and the first cure
564
- anybody reaches for would be "delete the lock by hand" — i.e. switching the mechanism off.
565
-
566
- ## `npm test` — `--min=1` stays, and here is what it is for
567
-
568
- The script is `vigiles test --min=1`. It went green on 2026-09-11 when the first harnesses
569
- landed; before that it correctly exited 1:
570
-
571
- ```
572
- ✗ vigiles test: --min=1 but only 0 test file(s) matched — evals never executed
573
- (check the paths/globs, or that the run was reached).
574
- ```
575
-
576
- Do **not** "fix" a future red by dropping `--min` — the flag is the only thing standing
577
- between "every test passed" and "no test ran", which is rule 4 applied to the test runner
578
- itself.
579
-
580
- 🔴 **Two ways this command lies if written differently, both measured 2026-09-11:**
581
-
582
- | form | what happens | exit |
583
- | ---------------------- | --------------------------------------------------------------------------- | ----- |
584
- | `vigiles test .` | `.` is read as a FILE — `ERR_UNSUPPORTED_DIR_IMPORT`, uncaught, runner dies | **0** |
585
- | `vigiles test` | `No **/*.harness.{mjs,cjs,js,mts,cts,ts} files found.` | **0** |
586
- | `vigiles test --min=1` | names the empty match and fails | **1** |
587
-
588
- The first row is the worse one: the runner crashed with a stack trace and still reported
589
- success. `package.json` shipped `vigiles test .` from the initial scaffold until this was
590
- measured — so the repo's own test command had never once executed a test, and said nothing.
496
+ | you are testing | kind |
497
+ | -------------------------------------------------------------------------- | --------------------------------------------- |
498
+ | a pure function | unit — vitest, `<module>.test.ts` beside it |
499
+ | a use case or adapter that reaches disk, a process, the network, the clock | integration — vitest, fakes through the ports |
500
+ | a hook's decision, a skill's contract | harness — vigiles, `<surface>.harness.mjs` |
501
+ | the installed package, a real TeX build | e2e — `test/e2e/*.mjs` |
502
+
503
+ Red first · assert the whole value · test what the code does, never what its source says · no test
504
+ touches the real network · 100% coverage, no `c8 ignore`. **Before every push: `npm run check`,
505
+ exit code read without a pipe.** Everything else — where files live, how fakes are injected, the
506
+ `--min=1` trap: [`docs/testing.md`](docs/testing.md).
591
507
 
592
508
  ## Commits
593
509
 
package/CONTRIBUTING.md CHANGED
@@ -10,20 +10,10 @@ it is below.
10
10
 
11
11
  ## How this is tested
12
12
 
13
- Every rule ships with a **harness**: a test that runs the rule twice. Once on a file with a defect
14
- planted in it, where the rule must find it. Once on a clean file, where the rule must say nothing.
15
-
16
- The second run is the one people skip, and it is the one that matters. A rule that is broken and
17
- finds nothing passes the first kind of test by accident — from outside, "there is nothing wrong
18
- here" and "this check never ran" look exactly the same.
19
-
20
- Older rules also have a **battery** (`*.mutations.mjs`): it deletes one thing the rule depends on
21
- and then demands the harness go red, at the specific assertion that thing belongs to. **These
22
- hand-written batteries are deprecated and being removed (#52).** Do not write a new one and do
23
- not add cases to an existing one; instead, say what an assertion guards in a comment directly
24
- above it (`// Guards: …`). `npm run check` fails on a new or grown battery — the frozen list in
25
- `scripts/mutation-batteries.frozen.json` may only shrink. Whether a real mutation-testing tool
26
- replaces them is decided in #52.
13
+ Unit and integration tests run under vitest beside the module they test; harnesses run under
14
+ vigiles and cover what an agent sees; end-to-end runs install the package and build real PDFs.
15
+ Every test is seen red first, asserts the whole value, and tests what the code does rather than
16
+ what a file says. [`docs/testing.md`](docs/testing.md) says which kind to write and how.
27
17
 
28
18
  A harness runs in this tree, against this working copy. That is the wrong shape for a defect that
29
19
  only exists once somebody else has installed the package — a path written inside a skill, a file
@@ -31,9 +21,9 @@ that never made it into the tarball, a PDF whose content is wrong while the exit
31
21
  Those are covered by the end-to-end runs, and [`docs/e2e.md`](docs/e2e.md) says which question
32
22
  belongs to which tier, and **when a change owes a new e2e rather than a harness**.
33
23
 
34
- It caught a real one on the way in: `js-yaml` 5 stopped parsing an unquoted date as a `Date`.
35
- Every harness stayed green under both majors, and only the battery noticed that the rule's
36
- date coercion had become dead code.
24
+ An example of why asserting the value matters: `js-yaml` 5 stopped parsing an unquoted date as a
25
+ `Date`, every harness stayed green under both majors, and the rule's date coercion had quietly
26
+ become dead code — no test asserted the coerced value.
37
27
 
38
28
  ## Running the checks: one command
39
29
 
@@ -41,8 +31,8 @@ date coercion had become dead code.
41
31
  npm run check
42
32
  ```
43
33
 
44
- That is the whole instruction. It runs every gate in order — build, lint, every harness, every
45
- mutation battery, the install e2e under npm and pnpm, and a real `pdflatex` build — and ends
34
+ That is the whole instruction. It runs every gate in order — build, lint, every harness, the
35
+ install e2e under npm and pnpm, and a real `pdflatex` build — and ends
46
36
 
47
37
  by printing **which CI jobs it does not reproduce, and why**.
48
38
 
@@ -65,8 +55,8 @@ nobody covered — verified by adding a `windows` job and watching it fail.
65
55
  ## Layout
66
56
 
67
57
  ```
68
- eslint-rules/ the rules, each with its .harness.mjs beside it (older ones also a .mutations.mjs)
69
- lib/ shared readers — markdown, skill corpus, the mutation driver
58
+ eslint-rules/ the rules, each with its .harness.mjs beside it
59
+ lib/ shared readers — markdown, skill corpus
70
60
  hooks/ three hooks for vigiles — the code
71
61
  plugin/ the Claude Code plugin: wiring for those hooks, no code, no package.json
72
62
  skills/ 24 stage skills
@@ -78,6 +68,7 @@ docs/ evidence that would otherwise bloat CLAUDE.md:
78
68
  incidents.md what broke, measured
79
69
  install.md the install contract
80
70
  e2e.md the end-to-end runs, and when a change owes one
71
+ testing.md which kind of test to write, and the rules for all of them
81
72
  ```
82
73
 
83
74
  Most of the package's rules run on users' papers and are described for users in
@@ -94,6 +85,8 @@ The README links only what a user needs. These are for people changing the packa
94
85
  `init` writes, supported package managers, troubleshooting, and why it is shaped this way
95
86
  - [`docs/e2e.md`](docs/e2e.md) — the end-to-end runs: what each proves, what they do not cover,
96
87
  and when a change owes one
88
+ - [`docs/testing.md`](docs/testing.md) — the kinds of test, which one a change needs, and the rules
89
+ for every one
97
90
  - [`docs/incidents.md`](docs/incidents.md) — what broke, measured
98
91
  - [`docs/package-shape-options.md`](docs/package-shape-options.md) — the options for the
99
92
  package's shape, and the ranking
@@ -102,13 +95,19 @@ The README links only what a user needs. These are for people changing the packa
102
95
 
103
96
  ```bash
104
97
  npm install
105
- npm test # every harness, the tests' type-check, every *.test.ts (Node >= 22.18)
106
- node scripts/run-mutations.mjs # run the remaining batteries (deprecated, #52 — do not add to them)
98
+ npm test # every *.test.ts, then every harness (Node >= 22.18)
99
+ npm run coverage # the same, under c8, failing below the thresholds in .c8rc.json
107
100
  ```
108
101
 
109
102
  None of these are needed to USE the tool — they are here because the gates are part of the
110
103
  argument, not decoration.
111
104
 
105
+ Coverage is 100% for lines, statements, functions and branches, and there is no `c8 ignore`:
106
+ code a test cannot reach directly — a race, a permission, a broken install, a real download — takes
107
+ the effect as a parameter (the ports in `src/ports`, an injected `readdir` or runner), and the test
108
+ passes a fake. `scripts/coverage-config.test.ts` fails on a coverage-ignore comment and on any
109
+ change to `.c8rc.json`'s exclude list that was not made there on purpose.
110
+
112
111
  ## Adding a venue
113
112
 
114
113
  A venue is a **preset**, a JSONC file in `skills/submit-paper/references/venues/`, validated by
@@ -166,8 +165,7 @@ Licensing differs too: that suite is CC BY-NC 4.0, this is MIT.
166
165
  - **scorecard** — `PIPELINE-STATUS.md`, the file above. One per paper.
167
166
  - **frozen** — a copy of the exact PDF or `.tex` that was sent, kept in `versions/`. Bytes, not a
168
167
  commit reference: squash and `gc` destroy commit references, and did.
169
- - **harness** — the test beside a rule. **battery** — a set of edits that try to break a harness
170
- (deprecated, being removed in #52).
168
+ - **harness** — the test beside a rule.
171
169
 
172
170
  ## Also in the box
173
171
 
package/README.md CHANGED
@@ -27,7 +27,7 @@ before you submit.** It comes in two parts:
27
27
 
28
28
  Smaller slips too: `§` for "Section", `.05` for `0.05` — fixed for you.
29
29
 
30
- - 🧠 **Skills for [Claude Code](https://claude.com/claude-code)** — optional, 24 of them, covering the
30
+ - 🧠 **Skills for [Claude Code](https://claude.com/claude-code)** — optional, covering the
31
31
  whole pipeline: the idea, the venue, the study, the draft, the reviews, submission.
32
32
 
33
33
  ## Contents
@@ -173,17 +173,20 @@ what it does — "is this idea worth a paper?", "find me a venue for this". `ini
173
173
  - **A ready / not ready verdict before you submit**, worst problem first. `harden-paper`
174
174
  - **Where your paper stands**, measured from the real build. `paper-status`
175
175
 
176
- All 24, by stage: [`docs/skills.md`](docs/skills.md).
176
+ Every skill, by stage: [`docs/skills.md`](docs/skills.md).
177
177
 
178
178
  ## 🔍 Lint and build
179
179
 
180
- | | `paperlint build` | `paperlint lint` |
181
- | ------ | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
182
- | needs | Node, TeX Live, the network | Node |
183
- | does | compiles `paper.tex`, measures the PDF, checks each reference online (exists, right authors) | checks the source, and judges what build recorded: page limit, fonts, the reference results — offline |
184
- | writes | `paper.pdf`, and in `_build/` the measurements and the reference results | nothing (`--fix`: the three fixable slips) |
180
+ | | `paperlint build` | `paperlint lint` |
181
+ | ------ | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
182
+ | needs | Node, TeX Live, the network | Node |
183
+ | does | compiles `paper.tex`, measures the PDF, checks each reference online (exists, right authors) | checks the source, and judges what build recorded: page limit, fonts, the reference results — offline |
184
+ | writes | `paper.pdf`, in `_build/` the measurements and the reference results, and `repro/references-cache.json` | nothing (`--fix`: the three fixable slips) |
185
185
 
186
186
  - If `paper.pdf` or the bibliography changed since the last build, lint fails and tells you to rebuild.
187
+ - Commit `repro/references-cache.json`: it keeps what the citation services answered, so a build
188
+ asks only about new or edited entries (and answers older than 30 days), so an unchanged
189
+ bibliography usually builds without the network ([`docs/references.md`](docs/references.md)).
187
190
  - Lint also checks the pipeline's own records: `PIPELINE-STATUS.md`, reviews, notes on related
188
191
  papers ([`docs/rules.md`](docs/rules.md)).
189
192
  - The rules run on ESLint, so a deliberate exception is a comment on the line above:
@@ -295,6 +298,7 @@ Only `paperlint lint --fix`, and only three rules: `paper/section-word` (`§`
295
298
  - [`docs/install.md`](docs/install.md) — what `init` does, the hooks, package managers, troubleshooting
296
299
  - [`docs/configuration.md`](docs/configuration.md) — every setting, how `build` compiles, using your own ESLint
297
300
  - [`docs/rules.md`](docs/rules.md) — every check, venue presets, recording a submitted PDF
301
+ - [`docs/references.md`](docs/references.md) — the reference check, and the cache file to commit
298
302
  - [`docs/optional-rules.md`](docs/optional-rules.md) — checks only some venues need
299
303
  - [`docs/toolchain.md`](docs/toolchain.md) — TeX Live, and Banal (HotCRP's page-geometry checker, GPL)
300
304
  - [`CONTRIBUTING.md`](CONTRIBUTING.md) — how the package is tested and released, and adding a venue to it
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `bin/paperlint.mjs` — the shim's one decision of its own: with no `dist/` beside it (a clone
3
+ * before `npm run build`, a git dependency installed with `--ignore-scripts`) it refuses loudly,
4
+ * naming the missing file and both remedies, instead of an ERR_MODULE_NOT_FOUND from the loader.
5
+ *
6
+ * The missing build is staged with a preload that makes `existsSync` answer "no" for exactly
7
+ * `dist/cli.js` — the shim reads the real file, so the real shim is what is measured.
8
+ */
9
+ import assert from "node:assert/strict";
10
+ import { spawnSync } from "node:child_process";
11
+ import { fileURLToPath } from "node:url";
12
+ import { test } from "vitest";
13
+
14
+ const BIN = fileURLToPath(new URL("./paperlint.mjs", import.meta.url));
15
+ const DIST = fileURLToPath(new URL("../dist/cli.js", import.meta.url));
16
+
17
+ const HIDE_DIST = [
18
+ 'import fs from "node:fs";',
19
+ 'import { syncBuiltinESMExports } from "node:module";',
20
+ "const real = fs.existsSync;",
21
+ 'fs.existsSync = (p) => (String(p).endsWith("/dist/cli.js") ? false : real(p));',
22
+ "syncBuiltinESMExports();",
23
+ ].join("\n");
24
+
25
+ test("no dist/: exit 2, the missing path, and both remedies — nothing from the loader", () => {
26
+ const r = spawnSync(
27
+ process.execPath,
28
+ [
29
+ "--import",
30
+ `data:text/javascript,${encodeURIComponent(HIDE_DIST)}`,
31
+ BIN,
32
+ "--version",
33
+ ],
34
+ { encoding: "utf8" },
35
+ );
36
+ assert.equal(r.status, 2);
37
+ assert.equal(r.stdout, "");
38
+ const [first, ...rest] = r.stderr.split("\n");
39
+ assert.equal(first, `paperlint: not built — ${DIST} missing.`);
40
+ assert.match(
41
+ rest.join("\n"),
42
+ /In a clone: `npm run build`\. For a git dependency: reinstall without `--ignore-scripts`\./,
43
+ );
44
+ assert.doesNotMatch(r.stderr, /ERR_MODULE_NOT_FOUND/);
45
+ });
@@ -1,3 +1,5 @@
1
1
  import type { Files } from "../../ports/files.ts";
2
+ /** An error's errno code; anything thrown that is not an Error has none. */
3
+ export declare const errorCode: (e: unknown) => unknown;
2
4
  export declare const nodeFiles: Files;
3
5
  //# sourceMappingURL=files.io.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"files.io.d.ts","sourceRoot":"","sources":["../../../src/adapters/node/files.io.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAKlD,eAAO,MAAM,SAAS,EAAE,KAsBvB,CAAC"}
1
+ {"version":3,"file":"files.io.d.ts","sourceRoot":"","sources":["../../../src/adapters/node/files.io.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAElD,4EAA4E;AAC5E,eAAO,MAAM,SAAS,GAAI,GAAG,OAAO,KAAG,OAC6B,CAAC;AAErE,eAAO,MAAM,SAAS,EAAE,KAsBvB,CAAC"}
@@ -1,7 +1,8 @@
1
1
  /** `Files` over `node:fs`. */
2
2
  import { mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
3
3
  import { dirname } from "node:path";
4
- const code = (e) => e instanceof Error ? e.code : undefined;
4
+ /** An error's errno code; anything thrown that is not an Error has none. */
5
+ export const errorCode = (e) => e instanceof Error ? e.code : undefined;
5
6
  export const nodeFiles = {
6
7
  isFile(p) {
7
8
  return statSync(p, { throwIfNoEntry: false })?.isFile() ?? false;
@@ -11,7 +12,7 @@ export const nodeFiles = {
11
12
  return readFileSync(p);
12
13
  }
13
14
  catch (e) {
14
- if (code(e) === "ENOENT")
15
+ if (errorCode(e) === "ENOENT")
15
16
  return null;
16
17
  throw e;
17
18
  }
@@ -1 +1 @@
1
- {"version":3,"file":"files.io.js","sourceRoot":"","sources":["../../../src/adapters/node/files.io.ts"],"names":[],"mappings":"AAAA,8BAA8B;AAC9B,OAAO,EACL,SAAS,EACT,YAAY,EACZ,UAAU,EACV,MAAM,EACN,QAAQ,EACR,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,MAAM,IAAI,GAAG,CAAC,CAAU,EAAW,EAAE,CACnC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAE,CAA2B,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AAErE,MAAM,CAAC,MAAM,SAAS,GAAU;IAC9B,MAAM,CAAC,CAAe;QACpB,OAAO,QAAQ,CAAC,CAAC,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,KAAK,CAAC;IACnE,CAAC;IACD,SAAS,CAAC,CAAe;QACvB,IAAI,CAAC;YACH,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC;QACzB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAC;YACtC,MAAM,CAAC,CAAC;QACV,CAAC;IACH,CAAC;IACD,WAAW,CAAC,CAAe,EAAE,KAAiB;QAC5C,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC;QACzB,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3C,IAAI,CAAC;YACH,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAC3B,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACtB,CAAC;gBAAS,CAAC;YACT,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAChC,CAAC;IACH,CAAC;CACF,CAAC"}
1
+ {"version":3,"file":"files.io.js","sourceRoot":"","sources":["../../../src/adapters/node/files.io.ts"],"names":[],"mappings":"AAAA,8BAA8B;AAC9B,OAAO,EACL,SAAS,EACT,YAAY,EACZ,UAAU,EACV,MAAM,EACN,QAAQ,EACR,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,4EAA4E;AAC5E,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,CAAU,EAAW,EAAE,CAC/C,CAAC,YAAY,KAAK,CAAC,CAAC,CAAE,CAA2B,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AAErE,MAAM,CAAC,MAAM,SAAS,GAAU;IAC9B,MAAM,CAAC,CAAe;QACpB,OAAO,QAAQ,CAAC,CAAC,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,KAAK,CAAC;IACnE,CAAC;IACD,SAAS,CAAC,CAAe;QACvB,IAAI,CAAC;YACH,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC;QACzB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,SAAS,CAAC,CAAC,CAAC,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAC;YAC3C,MAAM,CAAC,CAAC;QACV,CAAC;IACH,CAAC;IACD,WAAW,CAAC,CAAe,EAAE,KAAiB;QAC5C,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC;QACzB,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3C,IAAI,CAAC;YACH,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAC3B,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACtB,CAAC;gBAAS,CAAC;YACT,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAChC,CAAC;IACH,CAAC;CACF,CAAC"}
@@ -25,11 +25,13 @@ export interface OperatorList {
25
25
  }
26
26
  /** `setFillRGBColor`'s argument: a `#rrggbb` string in current pdf.js, three 0-255 numbers in older ones. */
27
27
  export declare function hexOf(args: unknown): string;
28
+ /** One drawn character and the fill it was drawn in. */
29
+ interface Drawn {
30
+ readonly ch: string;
31
+ readonly fill: Fill;
32
+ }
28
33
  /** Every drawn character, in content order, with the fill it was drawn in. */
29
- export declare function drawnCharacters(ops: ColourOps, list: OperatorList): {
30
- chars: string[];
31
- fills: Fill[];
32
- };
34
+ export declare function drawnCharacters(ops: ColourOps, list: OperatorList): Drawn[];
33
35
  /** How far ahead the walk looks for an item it cannot find where it expects it. */
34
36
  export declare const LOOKAHEAD = 4000;
35
37
  /**
@@ -39,4 +41,13 @@ export declare const LOOKAHEAD = 4000;
39
41
  * a missed match can keep light text, never drop dark text.
40
42
  */
41
43
  export declare function fillsOf(ops: ColourOps, list: OperatorList, items: readonly string[]): Fill[];
44
+ /**
45
+ * Each item paired with its fill — `fillsOf` for items that are not bare strings, so a caller never
46
+ * matches the two lists up by index.
47
+ */
48
+ export declare function fillsFor<T>(ops: ColourOps, list: OperatorList, items: readonly T[], textOf: (item: T) => string): {
49
+ readonly item: T;
50
+ readonly fill: Fill;
51
+ }[];
52
+ export {};
42
53
  //# sourceMappingURL=fill.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fill.d.ts","sourceRoot":"","sources":["../../../src/adapters/pdfjs/fill.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,6BAA6B,CAAC;AAExD,yFAAyF;AACzF,wBAAgB,SAAS,CAAC,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAGvD;AAED,0FAA0F;AAC1F,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,qFAAqF;AACrF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,SAAS,OAAO,EAAE,CAAC;CACxC;AAKD,6GAA6G;AAC7G,wBAAgB,KAAK,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAQ3C;AAuCD,8EAA8E;AAC9E,wBAAgB,eAAe,CAC7B,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,YAAY,GACjB;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,IAAI,EAAE,CAAA;CAAE,CAepC;AAED,mFAAmF;AACnF,eAAO,MAAM,SAAS,OAAO,CAAC;AAc9B;;;;;GAKG;AACH,wBAAgB,OAAO,CACrB,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,YAAY,EAClB,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,IAAI,EAAE,CAWR"}
1
+ {"version":3,"file":"fill.d.ts","sourceRoot":"","sources":["../../../src/adapters/pdfjs/fill.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,6BAA6B,CAAC;AAExD,yFAAyF;AACzF,wBAAgB,SAAS,CAAC,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAGvD;AAED,0FAA0F;AAC1F,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,qFAAqF;AACrF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,SAAS,OAAO,EAAE,CAAC;CACxC;AAKD,6GAA6G;AAC7G,wBAAgB,KAAK,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAQ3C;AAuCD,wDAAwD;AACxD,UAAU,KAAK;IACb,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;CACrB;AAED,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,GAAG,KAAK,EAAE,CAW3E;AAED,mFAAmF;AACnF,eAAO,MAAM,SAAS,OAAO,CAAC;AAoB9B;;;;;GAKG;AACH,wBAAgB,OAAO,CACrB,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,YAAY,EAClB,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,IAAI,EAAE,CAER;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EACxB,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,YAAY,EAClB,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAC1B;IAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;CAAE,EAAE,CAY7C"}