paperlint 3.1.3 → 3.2.0

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 (371) hide show
  1. package/.github/workflows/ci.yml +12 -7
  2. package/CLAUDE.md +3 -3
  3. package/CONTRIBUTING.md +34 -5
  4. package/dist/adapters/banal/index.d.ts.map +1 -1
  5. package/dist/adapters/banal/index.js +9 -5
  6. package/dist/adapters/banal/index.js.map +1 -1
  7. package/dist/adapters/banal/invocation.d.ts +1 -1
  8. package/dist/adapters/node/files.io.d.ts.map +1 -1
  9. package/dist/adapters/node/files.io.js +2 -1
  10. package/dist/adapters/node/files.io.js.map +1 -1
  11. package/dist/adapters/node/process.io.d.ts +0 -5
  12. package/dist/adapters/node/process.io.d.ts.map +1 -1
  13. package/dist/adapters/node/process.io.js +4 -3
  14. package/dist/adapters/node/process.io.js.map +1 -1
  15. package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
  16. package/dist/adapters/pdfjs/fill.js +13 -7
  17. package/dist/adapters/pdfjs/fill.js.map +1 -1
  18. package/dist/adapters/references/index.d.ts.map +1 -1
  19. package/dist/adapters/references/index.js +4 -5
  20. package/dist/adapters/references/index.js.map +1 -1
  21. package/dist/adapters/references/pool.d.ts.map +1 -1
  22. package/dist/adapters/references/pool.js +4 -5
  23. package/dist/adapters/references/pool.js.map +1 -1
  24. package/dist/adapters/references/reach.io.d.ts.map +1 -1
  25. package/dist/adapters/references/reach.io.js +2 -1
  26. package/dist/adapters/references/reach.io.js.map +1 -1
  27. package/dist/build-engine.js +2 -2
  28. package/dist/build-engine.js.map +1 -1
  29. package/dist/build.d.ts.map +1 -1
  30. package/dist/build.js +14 -11
  31. package/dist/build.js.map +1 -1
  32. package/dist/cli.d.ts.map +1 -1
  33. package/dist/cli.js +31 -34
  34. package/dist/cli.js.map +1 -1
  35. package/dist/doctor.d.ts.map +1 -1
  36. package/dist/doctor.js +3 -2
  37. package/dist/doctor.js.map +1 -1
  38. package/dist/domain/lookup-cache.d.ts.map +1 -1
  39. package/dist/domain/lookup-cache.js +10 -8
  40. package/dist/domain/lookup-cache.js.map +1 -1
  41. package/dist/domain/text.d.ts +12 -0
  42. package/dist/domain/text.d.ts.map +1 -1
  43. package/dist/domain/text.js +17 -0
  44. package/dist/domain/text.js.map +1 -1
  45. package/dist/engine.d.ts.map +1 -1
  46. package/dist/engine.js +4 -3
  47. package/dist/engine.js.map +1 -1
  48. package/dist/eslint-rules/bib-reachable-entry.d.ts +22 -0
  49. package/dist/eslint-rules/bib-reachable-entry.d.ts.map +1 -0
  50. package/dist/eslint-rules/bib-reachable-entry.js +75 -0
  51. package/dist/eslint-rules/bib-reachable-entry.js.map +1 -0
  52. package/dist/eslint-rules/install-path-literals.d.ts +80 -0
  53. package/dist/eslint-rules/install-path-literals.d.ts.map +1 -0
  54. package/dist/eslint-rules/install-path-literals.js +105 -0
  55. package/dist/eslint-rules/install-path-literals.js.map +1 -0
  56. package/dist/eslint-rules/latex-language.d.ts +136 -0
  57. package/dist/eslint-rules/latex-language.d.ts.map +1 -0
  58. package/dist/eslint-rules/latex-language.js +639 -0
  59. package/dist/eslint-rules/latex-language.js.map +1 -0
  60. package/dist/eslint-rules/paper-research-question.d.ts +31 -0
  61. package/dist/eslint-rules/paper-research-question.d.ts.map +1 -0
  62. package/dist/eslint-rules/paper-research-question.js +188 -0
  63. package/dist/eslint-rules/paper-research-question.js.map +1 -0
  64. package/dist/eslint-rules/paper-stages.d.ts +83 -0
  65. package/dist/eslint-rules/paper-stages.d.ts.map +1 -0
  66. package/dist/eslint-rules/paper-stages.js +358 -0
  67. package/dist/eslint-rules/paper-stages.js.map +1 -0
  68. package/dist/eslint-rules/paper-typography.d.ts +74 -0
  69. package/dist/eslint-rules/paper-typography.d.ts.map +1 -0
  70. package/dist/eslint-rules/paper-typography.js +404 -0
  71. package/dist/eslint-rules/paper-typography.js.map +1 -0
  72. package/dist/eslint-rules/papers.d.ts +42 -0
  73. package/dist/eslint-rules/papers.d.ts.map +1 -0
  74. package/dist/eslint-rules/papers.js +145 -0
  75. package/dist/eslint-rules/papers.js.map +1 -0
  76. package/dist/eslint-rules/pdf-last-page-balance.d.ts +72 -0
  77. package/dist/eslint-rules/pdf-last-page-balance.d.ts.map +1 -0
  78. package/dist/eslint-rules/pdf-last-page-balance.js +237 -0
  79. package/dist/eslint-rules/pdf-last-page-balance.js.map +1 -0
  80. package/dist/eslint-rules/review-frontmatter.d.ts +57 -0
  81. package/dist/eslint-rules/review-frontmatter.d.ts.map +1 -0
  82. package/dist/eslint-rules/review-frontmatter.js +126 -0
  83. package/dist/eslint-rules/review-frontmatter.js.map +1 -0
  84. package/dist/eslint-rules/review-frontmatter.schema.json +36 -0
  85. package/dist/eslint-rules/rule-context.d.ts +55 -0
  86. package/dist/eslint-rules/rule-context.d.ts.map +1 -0
  87. package/dist/eslint-rules/rule-context.js +9 -0
  88. package/dist/eslint-rules/rule-context.js.map +1 -0
  89. package/dist/eslint-rules/sibling-frontmatter.d.ts +25 -0
  90. package/dist/eslint-rules/sibling-frontmatter.d.ts.map +1 -0
  91. package/dist/eslint-rules/sibling-frontmatter.js +16 -0
  92. package/dist/eslint-rules/sibling-frontmatter.js.map +1 -0
  93. package/dist/eslint-rules/sibling-frontmatter.schema.json +14 -0
  94. package/dist/eslint-rules/temp-root-realpath.d.ts +71 -0
  95. package/dist/eslint-rules/temp-root-realpath.d.ts.map +1 -0
  96. package/dist/eslint-rules/temp-root-realpath.js +139 -0
  97. package/dist/eslint-rules/temp-root-realpath.js.map +1 -0
  98. package/dist/eslint-rules/tex-build.d.ts +35 -0
  99. package/dist/eslint-rules/tex-build.d.ts.map +1 -0
  100. package/dist/eslint-rules/tex-build.js +313 -0
  101. package/dist/eslint-rules/tex-build.js.map +1 -0
  102. package/dist/facts-file.d.ts.map +1 -1
  103. package/dist/facts-file.js +2 -1
  104. package/dist/facts-file.js.map +1 -1
  105. package/dist/hooks-settings.d.ts +1 -1
  106. package/dist/hooks-settings.d.ts.map +1 -1
  107. package/dist/hooks-settings.js +14 -9
  108. package/dist/hooks-settings.js.map +1 -1
  109. package/dist/init.d.ts +5 -5
  110. package/dist/init.d.ts.map +1 -1
  111. package/dist/init.js +15 -7
  112. package/dist/init.js.map +1 -1
  113. package/dist/latex-loop.d.ts +2 -2
  114. package/dist/latex-loop.d.ts.map +1 -1
  115. package/dist/latex-loop.js +2 -2
  116. package/dist/latex-loop.js.map +1 -1
  117. package/dist/lib/agent-cli-version.d.ts +65 -0
  118. package/dist/lib/agent-cli-version.d.ts.map +1 -0
  119. package/dist/lib/agent-cli-version.js +77 -0
  120. package/dist/lib/agent-cli-version.js.map +1 -0
  121. package/dist/lib/check.d.ts +19 -0
  122. package/dist/lib/check.d.ts.map +1 -0
  123. package/dist/lib/check.js +52 -0
  124. package/dist/lib/check.js.map +1 -0
  125. package/dist/lib/markdown.d.ts +180 -0
  126. package/dist/lib/markdown.d.ts.map +1 -0
  127. package/dist/lib/markdown.js +302 -0
  128. package/dist/lib/markdown.js.map +1 -0
  129. package/dist/lib/paper-config.d.ts +84 -0
  130. package/dist/lib/paper-config.d.ts.map +1 -0
  131. package/dist/lib/paper-config.js +142 -0
  132. package/dist/lib/paper-config.js.map +1 -0
  133. package/dist/lib/skill-eval-fixture.d.ts +29 -0
  134. package/dist/lib/skill-eval-fixture.d.ts.map +1 -0
  135. package/dist/lib/skill-eval-fixture.js +62 -0
  136. package/dist/lib/skill-eval-fixture.js.map +1 -0
  137. package/dist/link-skills.d.ts.map +1 -1
  138. package/dist/link-skills.js +5 -4
  139. package/dist/link-skills.js.map +1 -1
  140. package/dist/new-paper.d.ts.map +1 -1
  141. package/dist/new-paper.js +2 -2
  142. package/dist/new-paper.js.map +1 -1
  143. package/dist/paper-settings.d.ts.map +1 -1
  144. package/dist/paper-settings.js +5 -4
  145. package/dist/paper-settings.js.map +1 -1
  146. package/dist/pdf-facts.d.ts.map +1 -1
  147. package/dist/pdf-facts.js +2 -1
  148. package/dist/pdf-facts.js.map +1 -1
  149. package/dist/pdf-geometry.d.ts.map +1 -1
  150. package/dist/pdf-geometry.js +1 -1
  151. package/dist/pdf-geometry.js.map +1 -1
  152. package/dist/presets.d.ts.map +1 -1
  153. package/dist/presets.js +3 -2
  154. package/dist/presets.js.map +1 -1
  155. package/dist/references.d.ts.map +1 -1
  156. package/dist/references.js +11 -6
  157. package/dist/references.js.map +1 -1
  158. package/dist/rules-config.d.ts.map +1 -1
  159. package/dist/rules-config.js +6 -3
  160. package/dist/rules-config.js.map +1 -1
  161. package/dist/structure.js +1 -1
  162. package/dist/structure.js.map +1 -1
  163. package/dist/tex-requirements.d.ts.map +1 -1
  164. package/dist/tex-requirements.js +8 -3
  165. package/dist/tex-requirements.js.map +1 -1
  166. package/dist/toolchain.d.ts.map +1 -1
  167. package/dist/toolchain.js +21 -16
  168. package/dist/toolchain.js.map +1 -1
  169. package/dist/venue-rules.js +1 -1
  170. package/dist/venue-rules.js.map +1 -1
  171. package/docs/e2e.md +7 -7
  172. package/docs/install.md +1 -1
  173. package/docs/prior-art/README.md +1 -1
  174. package/docs/prior-art/package-location.md +5 -5
  175. package/docs/prior-art/repro/README.md +1 -1
  176. package/docs/prior-art/test-tooling.md +2 -2
  177. package/docs/testing.md +11 -8
  178. package/docs/toolchain.md +1 -1
  179. package/eslint-rules/{bib-reachable-entry.mjs → bib-reachable-entry.ts} +4 -3
  180. package/eslint-rules/install-path-literals.harness.mjs +1 -1
  181. package/eslint-rules/{install-path-literals.mjs → install-path-literals.ts} +16 -8
  182. package/eslint-rules/latex-language.harness.mjs +2 -2
  183. package/eslint-rules/latex-language.test.mjs +7 -1
  184. package/eslint-rules/{latex-language.mjs → latex-language.ts} +297 -109
  185. package/eslint-rules/paper-research-question.harness.mjs +3 -3
  186. package/eslint-rules/{paper-research-question.mjs → paper-research-question.ts} +34 -15
  187. package/eslint-rules/paper-stages.harness.mjs +2 -2
  188. package/eslint-rules/paper-stages.test.mjs +8 -2
  189. package/eslint-rules/{paper-stages.mjs → paper-stages.ts} +74 -29
  190. package/eslint-rules/paper-typography.test.mjs +3 -3
  191. package/eslint-rules/{paper-typography.mjs → paper-typography.ts} +132 -54
  192. package/eslint-rules/papers.harness.mjs +2 -2
  193. package/eslint-rules/{papers.mjs → papers.ts} +7 -4
  194. package/eslint-rules/pdf-last-page-balance.harness.mjs +3 -3
  195. package/eslint-rules/{pdf-last-page-balance.mjs → pdf-last-page-balance.ts} +88 -31
  196. package/eslint-rules/review-frontmatter.test.mjs +2 -2
  197. package/eslint-rules/{review-frontmatter.mjs → review-frontmatter.ts} +37 -15
  198. package/eslint-rules/rule-context.ts +62 -0
  199. package/eslint-rules/{sibling-frontmatter.mjs → sibling-frontmatter.ts} +1 -1
  200. package/eslint-rules/temp-root-realpath.harness.mjs +1 -1
  201. package/eslint-rules/{temp-root-realpath.mjs → temp-root-realpath.ts} +19 -6
  202. package/eslint-rules/tex-build.harness.mjs +2 -2
  203. package/eslint-rules/{tex-build.mjs → tex-build.ts} +4 -3
  204. package/eslint.config.mjs +139 -30
  205. package/fixtures/pdf-facts/README.md +2 -2
  206. package/fixtures/real-markdown-paper/{baseline.mjs → baseline.ts} +31 -10
  207. package/hooks/{hooks.harness.mjs → hooks.harness.ts} +51 -34
  208. package/hooks/paper-edit-guard.hook.mjs +1 -1
  209. package/hooks/{paper-edit-guard.test.mjs → paper-edit-guard.test.ts} +3 -7
  210. package/hooks/paper-skills-nudge.hook.mjs +1 -1
  211. package/hooks/{paper-skills-nudge.test.mjs → paper-skills-nudge.test.ts} +2 -6
  212. package/hooks/{paper-status-gates.test.mjs → paper-status-gates.test.ts} +2 -6
  213. package/lib/agent-cli-version.harness.mjs +1 -1
  214. package/lib/{agent-cli-version.mjs → agent-cli-version.ts} +32 -18
  215. package/lib/check.test.ts +14 -9
  216. package/lib/{check.mjs → check.ts} +22 -9
  217. package/lib/markdown.test.mjs +7 -7
  218. package/lib/{markdown.mjs → markdown.ts} +68 -29
  219. package/lib/paper-config.harness.mjs +8 -8
  220. package/lib/{paper-config.mjs → paper-config.ts} +19 -14
  221. package/lib/skill-checks.mjs +1 -1
  222. package/lib/skill-checks.test.mjs +1 -1
  223. package/lib/skill-corpus.mjs +1 -1
  224. package/lib/skill-eval-kit.mjs +1 -1
  225. package/package.json +42 -10
  226. package/scripts/{check.harness.mjs → check.harness.ts} +111 -10
  227. package/scripts/{check.test.mjs → check.test.ts} +13 -10
  228. package/scripts/{check.mjs → check.ts} +116 -42
  229. package/scripts/coverage-config.test.ts +80 -37
  230. package/scripts/eslint-report-guard.test.mjs +1 -1
  231. package/scripts/harness-api.frozen.json +14 -15
  232. package/scripts/harness-api.test.ts +8 -5
  233. package/scripts/layer-legacy-frozen.test.ts +17 -13
  234. package/scripts/{layer-legacy-frozen.mjs → layer-legacy-frozen.ts} +128 -39
  235. package/scripts/release-config.test.ts +12 -4
  236. package/scripts/rules-see-files.test.mjs +1 -1
  237. package/scripts/test.test.ts +78 -0
  238. package/scripts/test.ts +87 -0
  239. package/skills/grade-paper-writing/prose-lint.mjs +1 -1
  240. package/skills/grade-paper-writing/{prose-lint.test.mjs → prose-lint.test.ts} +2 -2
  241. package/skills/paper-pipeline/description-language.eval.mjs +1 -1
  242. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +1 -1
  243. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +1 -1
  244. package/skills/paper-pipeline/pipeline-firing.eval.mjs +1 -1
  245. package/skills/paper-pipeline/pipeline-language.eval.mjs +1 -1
  246. package/skills/paper-pipeline/scripts/README.md +1 -1
  247. package/skills/paper-pipeline/scripts/announce.test.mjs +1 -1
  248. package/skills/paper-pipeline/scripts/artifact-coverage.mjs +1 -1
  249. package/skills/paper-pipeline/scripts/{artifact-coverage.test.mjs → artifact-coverage.test.ts} +2 -2
  250. package/skills/paper-pipeline/scripts/consumer.mjs +1 -5
  251. package/skills/paper-pipeline/scripts/consumer.test.mjs +1 -1
  252. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +1 -1
  253. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +1 -4
  254. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +1 -1
  255. package/skills/paper-pipeline/scripts/fixtures/{scorecard.mjs → scorecard.ts} +36 -7
  256. package/skills/paper-pipeline/scripts/{generated-code.test.mjs → generated-code.test.ts} +3 -3
  257. package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +1 -1
  258. package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +2 -2
  259. package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -2
  260. package/skills/paper-pipeline/scripts/pipeline-check.mjs +1 -4
  261. package/skills/paper-pipeline/scripts/population-map.mjs +1 -4
  262. package/skills/paper-pipeline/scripts/population-map.test.mjs +1 -1
  263. package/skills/paper-pipeline/scripts/{round-diff.harness.mjs → round-diff.harness.ts} +45 -18
  264. package/skills/paper-pipeline/scripts/round-diff.mjs +1 -1
  265. package/skills/paper-pipeline/scripts/round-diff.test.mjs +1 -1
  266. package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +1 -1
  267. package/skills/paper-pipeline/scripts/status.test.mjs +1 -1
  268. package/skills/plan-paper-timeline/fixtures/{fake-google-calendar.mjs → fake-google-calendar.ts} +101 -70
  269. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +3 -3
  270. package/skills/render-paper/{extract-pdf-facts.harness.mjs → extract-pdf-facts.harness.ts} +34 -9
  271. package/skills/render-paper/render-paper.harness.mjs +2 -2
  272. package/skills/tighten-paper/structure.mjs +1 -1
  273. package/skills/tighten-paper/{structure.test.mjs → structure.test.ts} +4 -3
  274. package/skills/verify-citations/scripts/{bib-authors.cli.test.mjs → bib-authors.cli.test.ts} +14 -4
  275. package/skills/verify-citations/scripts/bib-authors.d.mts +63 -0
  276. package/skills/verify-citations/scripts/bib-authors.mjs +1 -4
  277. package/skills/verify-citations/scripts/{verify-cites.cli.test.mjs → verify-cites.cli.test.ts} +32 -9
  278. package/skills/verify-citations/scripts/verify-cites.d.mts +58 -0
  279. package/src/CLAUDE.md +1 -1
  280. package/src/adapters/banal/index.test.ts +3 -6
  281. package/src/adapters/banal/index.ts +7 -4
  282. package/src/adapters/banal/invocation.test.ts +3 -1
  283. package/src/adapters/banal/invocation.ts +1 -1
  284. package/src/adapters/banal/output.test.ts +1 -1
  285. package/src/adapters/banal/run.test.ts +3 -3
  286. package/src/adapters/banal/xml.test.ts +1 -1
  287. package/src/adapters/curl/download.test.ts +4 -2
  288. package/src/adapters/node/files.io.ts +2 -2
  289. package/src/adapters/node/files.test.ts +3 -1
  290. package/src/adapters/node/process.io.ts +4 -3
  291. package/src/adapters/node/process.test.ts +1 -1
  292. package/src/adapters/node/workspace.test.ts +3 -1
  293. package/src/adapters/pdfjs/fill.test.ts +2 -2
  294. package/src/adapters/pdfjs/fill.ts +14 -7
  295. package/src/adapters/references/index.test.ts +31 -38
  296. package/src/adapters/references/index.ts +26 -58
  297. package/src/adapters/references/pool.ts +3 -5
  298. package/src/adapters/references/reach.io.ts +2 -1
  299. package/src/build-engine.harness.mjs +1 -1
  300. package/src/build-engine.test.ts +2 -2
  301. package/src/build-engine.ts +2 -2
  302. package/src/build.harness.mjs +2 -2
  303. package/src/build.test.ts +15 -14
  304. package/src/build.ts +28 -19
  305. package/src/cli.harness.mjs +6 -6
  306. package/src/cli.test.ts +13 -6
  307. package/src/cli.ts +39 -37
  308. package/src/doctor.harness.mjs +3 -3
  309. package/src/doctor.ts +3 -2
  310. package/src/domain/lookup-cache.ts +21 -11
  311. package/src/domain/result.test.ts +2 -2
  312. package/src/domain/text.test.ts +32 -0
  313. package/src/domain/text.ts +23 -0
  314. package/src/engine.harness.mjs +2 -2
  315. package/src/engine.ts +4 -3
  316. package/src/facts-file.test.ts +9 -7
  317. package/src/facts-file.ts +2 -1
  318. package/src/{hooks-settings.harness.mjs → hooks-settings.harness.ts} +39 -20
  319. package/src/hooks-settings.ts +20 -9
  320. package/src/init.test.ts +12 -9
  321. package/src/init.ts +22 -12
  322. package/src/{latex-log.harness.mjs → latex-log.harness.ts} +14 -14
  323. package/src/{latex-loop.harness.mjs → latex-loop.harness.ts} +64 -36
  324. package/src/latex-loop.ts +3 -3
  325. package/src/{link-skills.harness.mjs → link-skills.harness.ts} +19 -19
  326. package/src/link-skills.test.ts +3 -3
  327. package/src/link-skills.ts +5 -4
  328. package/src/lint-fix.test.ts +4 -6
  329. package/src/linted-set.test.ts +2 -2
  330. package/src/new-paper.harness.mjs +2 -2
  331. package/src/new-paper.test.ts +8 -10
  332. package/src/new-paper.ts +2 -2
  333. package/src/paper-files.test.ts +3 -5
  334. package/src/paper-settings-commands.test.ts +5 -4
  335. package/src/paper-settings.test.ts +5 -5
  336. package/src/paper-settings.ts +8 -9
  337. package/src/{pdf-facts.harness.mjs → pdf-facts.harness.ts} +9 -8
  338. package/src/pdf-facts.test.ts +10 -11
  339. package/src/pdf-facts.ts +2 -1
  340. package/src/{pdf-geometry.harness.mjs → pdf-geometry.harness.ts} +19 -12
  341. package/src/pdf-geometry.ts +3 -1
  342. package/src/presets.ts +3 -2
  343. package/src/references.test.ts +51 -43
  344. package/src/references.ts +14 -7
  345. package/src/{rules-config.harness.mjs → rules-config.harness.ts} +7 -13
  346. package/src/rules-config.ts +7 -4
  347. package/src/rules-off.test.ts +6 -4
  348. package/src/{structure.harness.mjs → structure.harness.ts} +26 -15
  349. package/src/structure.ts +1 -1
  350. package/src/{tex-requirements.harness.mjs → tex-requirements.harness.ts} +50 -22
  351. package/src/tex-requirements.ts +8 -5
  352. package/src/toolchain.harness.mjs +2 -2
  353. package/src/toolchain.test.ts +1 -1
  354. package/src/toolchain.ts +31 -17
  355. package/src/venue-rules.test.ts +62 -64
  356. package/src/venue-rules.ts +1 -1
  357. package/tsconfig.json +6 -3
  358. package/tsconfig.pkg.json +10 -0
  359. package/lib/check.d.mts +0 -17
  360. package/lib/paper-config.d.mts +0 -31
  361. package/scripts/layer-legacy-frozen.d.mts +0 -36
  362. package/scripts/rules-are-content-only.harness.mjs +0 -105
  363. package/scripts/rules-are-content-only.mjs +0 -148
  364. package/scripts/rules-are-content-only.test.mjs +0 -61
  365. package/scripts/test.mjs +0 -44
  366. package/scripts/test.test.mjs +0 -52
  367. /package/bin/{paperlint.test.mjs → paperlint.test.ts} +0 -0
  368. /package/lib/{skill-eval-fixture.mjs → skill-eval-fixture.ts} +0 -0
  369. /package/skills/grade-paper-writing/__snapshots__/{prose-lint.test.mjs.snap → prose-lint.test.ts.snap} +0 -0
  370. /package/skills/osf-artifact-upload/{osf-artifact-upload.harness.mjs → osf-artifact-upload.harness.ts} +0 -0
  371. /package/skills/tighten-paper/__snapshots__/{structure.test.mjs.snap → structure.test.ts.snap} +0 -0
@@ -0,0 +1,63 @@
1
+ // Types for bib-authors.mjs, for the exports `src/` imports — the same arrangement as
2
+ // `verify-cites.d.mts` beside it. Only what `src/` imports is declared.
3
+
4
+ /** One entry as `parseBib` returns it; a field the entry lacks is "". */
5
+ export interface BibEntry {
6
+ readonly type: string;
7
+ readonly key: string;
8
+ readonly author: string;
9
+ readonly title: string;
10
+ readonly booktitle: string;
11
+ readonly journal: string;
12
+ }
13
+
14
+ export declare function parseBib(text: string): BibEntry[];
15
+
16
+ /** True when the author list ends in `and others` — there is nothing complete to compare. */
17
+ export declare const truncated: (authorField: string) => boolean;
18
+
19
+ /** True when the entry names a venue that is not a preprint server. */
20
+ export declare function claimsPublished(
21
+ entry: Pick<BibEntry, "booktitle" | "journal">,
22
+ ): boolean;
23
+
24
+ /** One DBLP search hit, as `dblpHits` normalises it. */
25
+ export interface DblpHit {
26
+ readonly venue: string;
27
+ readonly year: string;
28
+ readonly type: string;
29
+ readonly title: string;
30
+ readonly authors: readonly string[];
31
+ }
32
+
33
+ /** DBLP's hits for a title; throws on a failed request (with `retryable` for a 429). */
34
+ export declare function dblpHits(title: string): Promise<DblpHit[]>;
35
+
36
+ export interface AuthorFinding {
37
+ readonly key: string;
38
+ readonly venue: string;
39
+ readonly missing: readonly string[];
40
+ readonly extra: readonly string[];
41
+ readonly orderDiffers: boolean;
42
+ }
43
+
44
+ export interface KeyWhy {
45
+ readonly key: string;
46
+ readonly why: string;
47
+ }
48
+
49
+ /** The buckets of one comparison; an entry in none of them was a preprint entry. */
50
+ export interface AuthorBuckets {
51
+ readonly findings: readonly AuthorFinding[];
52
+ readonly skipped: readonly KeyWhy[];
53
+ readonly unchecked: readonly KeyWhy[];
54
+ readonly matched: readonly string[];
55
+ }
56
+
57
+ export declare function checkAuthors(
58
+ parsed: readonly BibEntry[],
59
+ options?: {
60
+ readonly lookup?: (title: string) => Promise<readonly DblpHit[]>;
61
+ readonly pause?: (ms: number) => Promise<void> | void;
62
+ },
63
+ ): Promise<AuthorBuckets>;
@@ -45,10 +45,7 @@
45
45
  */
46
46
  import { readFileSync, existsSync, readdirSync } from "node:fs";
47
47
  import { join, extname } from "node:path";
48
- import {
49
- headings as mdHeadings,
50
- requireMarkdown,
51
- } from "../../../lib/markdown.mjs";
48
+ import { headings as mdHeadings, requireMarkdown } from "#lib/markdown";
52
49
  import { isMain } from "../../paper-pipeline/scripts/consumer.mjs";
53
50
 
54
51
  const DBLP = "https://dblp.org/search/publ/api";
@@ -9,7 +9,23 @@ import { readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
9
9
  import { dirname, join } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { test } from "vitest";
12
- import { runNode, useTempDir, writeTree } from "../../../test/support.mjs";
12
+ import { z } from "zod";
13
+ import {
14
+ runNode,
15
+ useTempDir,
16
+ writeTree,
17
+ type ScriptResult,
18
+ } from "../../../test/support.ts";
19
+
20
+ /** The verdicts `verify-cites --json`… prints: one per citation, with the fields these tests read. */
21
+ const Verdicts = z.array(
22
+ z.looseObject({
23
+ id: z.unknown().optional(),
24
+ verdict: z.unknown().optional(),
25
+ flags: z.array(z.unknown()).nullish(),
26
+ }),
27
+ );
28
+ const Cache = z.record(z.string(), z.unknown());
13
29
 
14
30
  const HERE = dirname(fileURLToPath(import.meta.url));
15
31
  const root = useTempDir("verify-cites-cli-");
@@ -48,12 +64,17 @@ writeTree(root, {
48
64
  " return new Response(null, { status: 404 });\n};\n",
49
65
  "consumer/paperlint.json": JSON.stringify({ contactEmail: "me@example.org" }),
50
66
  });
51
- const offline = (args, opts = {}) =>
52
- runNode(SCRIPT, [...args, "--offline"], { env, ...opts });
53
- const parsed = (r) => ({ ...r, stdout: JSON.parse(r.stdout) });
54
- const SUMMARY_PASS = (n, t, u, flagged) =>
55
- `\nverify-cites: ${n} citation(s) — ${t} true, 0 false (fabrication), ${u} unresolvable` +
56
- (flagged ? `, ${flagged} with hygiene flag(s)` : "") +
67
+ const offline = (
68
+ args: readonly string[],
69
+ opts: { input?: string } = {},
70
+ ): ScriptResult => runNode(SCRIPT, [...args, "--offline"], { env, ...opts });
71
+ const parsed = (r: ScriptResult) => ({
72
+ ...r,
73
+ stdout: Verdicts.parse(JSON.parse(r.stdout)),
74
+ });
75
+ const SUMMARY_PASS = (n: number, t: number, u: number, flagged: number) =>
76
+ `\nverify-cites: ${String(n)} citation(s) — ${String(t)} true, 0 false (fabrication), ${String(u)} unresolvable` +
77
+ (flagged ? `, ${String(flagged)} with hygiene flag(s)` : "") +
57
78
  "\nPASS — no fabrication (unresolvable is advisory)\n";
58
79
 
59
80
  test("-h is --help", () => {
@@ -118,9 +139,11 @@ test("online, a DOI the authority does not know is FALSE: FAIL, exit 1, and the
118
139
  assert.deepEqual(
119
140
  {
120
141
  status: r.status,
121
- verdicts: JSON.parse(r.stdout).map((x) => x.verdict),
142
+ verdicts: Verdicts.parse(JSON.parse(r.stdout)).map((x) => x.verdict),
122
143
  tail: r.stderr.trim().split("\n").pop(),
123
- cached: Object.keys(JSON.parse(readFileSync(CACHE, "utf8"))).sort(),
144
+ cached: Object.keys(
145
+ Cache.parse(JSON.parse(readFileSync(CACHE, "utf8"))),
146
+ ).sort(),
124
147
  },
125
148
  {
126
149
  status: 1,
@@ -0,0 +1,58 @@
1
+ // Types for verify-cites.mjs, for the exports `src/` imports — the same arrangement as
2
+ // `skills/paper-pipeline/scripts/consumer.d.mts`. Without this file TypeScript sees an untyped
3
+ // module, and every import needed a `@ts-expect-error` plus a cast at each call.
4
+ //
5
+ // Only what `src/` imports is declared. The `.mjs` is not typechecked, so a declaration nobody
6
+ // compiles against would drift without anyone noticing.
7
+
8
+ /** One entry as `parseBib` returns it: the BibTeX key as `id`, plus the fields it recognised. */
9
+ export interface Citation {
10
+ readonly id: string;
11
+ readonly [field: string]: unknown;
12
+ }
13
+
14
+ /** Every entry of a BibTeX text, `@comment`/`@string`/`@preamble` skipped. */
15
+ export declare function parseBib(text: string): Citation[];
16
+
17
+ /** The title reduced to what identifies it: case, braces and punctuation removed. */
18
+ export declare function titleIdentity(title: string): string;
19
+
20
+ /** Answers the resolvers gave, keyed by request — shared between citations and runs. */
21
+ export type ResponseCache = Record<string, unknown>;
22
+
23
+ /** Would verifying `citation` make a request, given `cache`? Answers from the cache only. */
24
+ export declare function wouldAsk(
25
+ citation: Citation,
26
+ cache: ResponseCache,
27
+ ): Promise<boolean>;
28
+
29
+ /** One run's memory of which service refused: a refusing service is not asked again. */
30
+ export interface Breaker {
31
+ call<T>(
32
+ service: string,
33
+ attempt: () => Promise<
34
+ { ok: true; value: T } | { ok: false; reason: string }
35
+ >,
36
+ ): Promise<{ ok: true; value: T } | { ok: false; reason: string }>;
37
+ }
38
+
39
+ export declare function createBreaker(): Breaker;
40
+
41
+ /** The verdict on one citation. */
42
+ export interface CiteVerdict {
43
+ readonly id: string;
44
+ readonly verdict: "true" | "false" | "unresolvable";
45
+ readonly reason?: string;
46
+ /** "<service>: <reason>" for each question a refusing service did not answer. */
47
+ readonly refused?: readonly string[];
48
+ }
49
+
50
+ /** Verify ONE citation live, answering from `cache` first and writing new answers into it. */
51
+ export declare function verifyCitationLive(
52
+ citation: Citation,
53
+ options?: {
54
+ readonly cache?: ResponseCache;
55
+ readonly offline?: boolean;
56
+ readonly breaker?: Breaker;
57
+ },
58
+ ): Promise<CiteVerdict>;
package/src/CLAUDE.md CHANGED
@@ -35,7 +35,7 @@ through the `io` file category in `boundaries/dependencies`; the `process` and `
35
35
  carries `// eslint-disable-next-line <rule> -- legacy <I/O|layer>, moves behind a port in #76`.
36
36
  Unused directives are errors (`reportUnusedDisableDirectives`), so a fixed site must drop its
37
37
  comment; the per-file counts are frozen in `scripts/layer-legacy.frozen.json` and checked by
38
- `scripts/layer-legacy-frozen.mjs` (`npm run check`, CI), which counts ESLint's own
38
+ `scripts/layer-legacy-frozen.ts` (`npm run check`, CI), which counts ESLint's own
39
39
  `suppressedMessages` and fails when a file's count grows, a file not in the list carries one, or
40
40
  anything under `src/domain/`, `src/ports/` or `src/adapters/` does. A NEW module that needs the
41
41
  world is a new `*.io.ts` in the adapter of the program it talks to — never a new directive.
@@ -2,7 +2,7 @@
2
2
  * `adapters/banal/index.ts` — banal as the two ports the app sees. What `run.test.ts` checks in
3
3
  * banal's own terms is checked here as the domain receives it: a `Geometry` whose provenance names
4
4
  * banal and the rule that found it, a reason that is one line of text, a `Ready` that says what was
5
- * verified. In-memory ports throughout; the real banal is `test/e2e/banal.mjs`.
5
+ * verified. In-memory ports throughout; the real banal is `test/e2e/banal.ts`.
6
6
  */
7
7
  import assert from "node:assert/strict";
8
8
  import { test } from "vitest";
@@ -44,15 +44,12 @@ test("with $BANAL: banal gets the .xml and a quoted $PDFTOHTML, and the geometry
44
44
  const { m, run } = measurer({ BANAL: "/own/banal" });
45
45
  const g = m.measure([]);
46
46
  assert.equal(g.kind, "measured");
47
- assert.deepEqual(g.kind === "measured" && g.by, {
47
+ assert.deepEqual(g.by, {
48
48
  tool: "banal",
49
49
  path: "/own/banal",
50
50
  how: "$BANAL",
51
51
  });
52
- assert.deepEqual(
53
- g.kind === "measured" && [g.geometry.columns, g.geometry.bodyPages],
54
- [2, 1],
55
- );
52
+ assert.deepEqual([g.geometry.columns, g.geometry.bodyPages], [2, 1]);
56
53
  const c = run.calls[0];
57
54
  // Guards: the poppler-free path — banal is handed paperlint's XML, never the PDF.
58
55
  assert.match(c?.args.at(-1) ?? "", /\.xml$/);
@@ -65,12 +65,15 @@ export function banalMeasurer(
65
65
  }
66
66
 
67
67
  /** The one place banal's `Ready` is minted: from an `Installed`, whose `PinnedBanal` only run.ts mints. */
68
- const ready = (i: Installed): Ready =>
69
- ({
70
- where: i.banal.path as string,
68
+ const ready = (i: Installed): Ready => {
69
+ // Widened by annotation: the brand of the path is not part of `Ready`'s shape.
70
+ const where: string = i.banal.path;
71
+ return {
72
+ where,
71
73
  fresh: i.fresh,
72
74
  verified: "sha256 verified, and it measured a probe page",
73
- }) as Ready;
75
+ } as Ready;
76
+ };
74
77
 
75
78
  /**
76
79
  * banal as the `ToolInstaller` port: download at the pin, check the sha256, accept by measuring a
@@ -51,7 +51,9 @@ test("shQuote survives a single quote", () => {
51
51
  });
52
52
 
53
53
  const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-stage-test-")));
54
- after(() => rmSync(root, { recursive: true, force: true }));
54
+ after(() => {
55
+ rmSync(root, { recursive: true, force: true });
56
+ });
55
57
 
56
58
  test("🔴 stage: on a real scratch directory the stub answers `-v` through the shell, from a path with a space and a quote", () => {
57
59
  const awkward = join(root, "it's a dir");
@@ -7,7 +7,7 @@
7
7
  * (`Error: Failed to run pdftohtml`, measured); told a version below 0.85 it applies a larger
8
8
  * correction and every font size moves. So `$PDFTOHTML` (banal's own override, line 166) points at a
9
9
  * stub that answers `-v` with `XML_DIALECT.version` — the pdftohtml whose XML `xml.ts` writes — and
10
- * refuses anything else. `test/e2e/banal.mjs` shows both failures with the real banal.
10
+ * refuses anything else. `test/e2e/banal.ts` shows both failures with the real banal.
11
11
  */
12
12
  import type { Opaque } from "ts-essentials";
13
13
  import type { AbsolutePath } from "../../domain/paths.ts";
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `output.ts` — every way a banal run can end, as a table of `ProcessExit` → `Result`, and the
3
- * measurement → geometry projection. Pure: no perl, no disk. The real banal is `test/e2e/banal.mjs`.
3
+ * measurement → geometry projection. Pure: no perl, no disk. The real banal is `test/e2e/banal.ts`.
4
4
  */
5
5
  import assert from "node:assert/strict";
6
6
  import { test } from "vitest";
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `adapters/banal/run.ts` — running and installing banal, on in-memory ports: which `Command` it builds, what it stages,
3
3
  * what it downloads and writes, and what it concludes. No perl, no disk, no network: the adapters
4
- * record, and each assertion reads the record. Real perl and the real banal are `test/e2e/banal.mjs`;
4
+ * record, and each assertion reads the record. Real perl and the real banal are `test/e2e/banal.ts`;
5
5
  * the adapters' own behaviour is `src/adapters/node/*.test.ts`.
6
6
  */
7
7
  import assert from "node:assert/strict";
@@ -45,10 +45,10 @@ test("measureGeometry: perl runs the found banal on the staged XML, and the geom
45
45
  });
46
46
  const g = measureGeometry(io, s, project, []);
47
47
  assert.equal(g.source, "banal");
48
- assert.equal(g.source === "banal" && g.geometry.bodyPt, 10.3);
48
+ assert.equal(g.geometry.bodyPt, 10.3);
49
49
  const [c] = run.calls;
50
50
  assert.equal(c?.file, "perl");
51
- assert.equal(c?.args.at(-1), "/scratch/paper.xml");
51
+ assert.equal(c.args.at(-1), "/scratch/paper.xml");
52
52
  // Guards: both files staged, once, and the scratch scope ended normally (cleanup ran).
53
53
  assert.deepEqual(
54
54
  workspace.written.map((w) => [w.name, w.mode]),
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Every page here is written by hand. The real-PDF half — pdf.js boxes of a committed fixture,
6
6
  * written as XML, measured by the real banal and compared with banal on real pdftohtml — is
7
- * `test/e2e/banal.mjs`, which runs where `paperlint toolchain` has installed banal.
7
+ * `test/e2e/banal.ts`, which runs where `paperlint toolchain` has installed banal.
8
8
  */
9
9
  import assert from "node:assert/strict";
10
10
  import { test } from "vitest";
@@ -23,7 +23,9 @@ import { spawnProcess } from "../node/index.ts";
23
23
  const root = realpathSync(
24
24
  mkdtempSync(join(tmpdir(), "paperlint-download-test-")),
25
25
  );
26
- after(() => rmSync(root, { recursive: true, force: true }));
26
+ after(() => {
27
+ rmSync(root, { recursive: true, force: true });
28
+ });
27
29
  const at = (...p: string[]) => join(root, ...p) as AbsolutePath;
28
30
 
29
31
  const dl = (env: Record<string, string>) => {
@@ -45,7 +47,7 @@ test("Download: a URL that does not answer is a failure with curl's words", () =
45
47
  const { d } = dl({ PATH: "/usr/bin:/bin" });
46
48
  const r = d.fetch(pathToFileURL(at("missing")).href, 10_000);
47
49
  assert.equal(r.ok, false);
48
- assert.ok(!r.ok && r.error.detail.length > 0);
50
+ assert.ok(r.error.detail.length > 0);
49
51
  });
50
52
 
51
53
  test("🔴 Download: no curl on PATH is named as that, not as a failed URL", () => {
@@ -10,10 +10,10 @@ import {
10
10
  import { dirname } from "node:path";
11
11
  import type { AbsolutePath } from "../../domain/paths.ts";
12
12
  import type { Files } from "../../ports/files.ts";
13
+ import { codeOf } from "../../domain/text.ts";
13
14
 
14
15
  /** An error's errno code; anything thrown that is not an Error has none. */
15
- export const errorCode = (e: unknown): unknown =>
16
- e instanceof Error ? (e as NodeJS.ErrnoException).code : undefined;
16
+ export const errorCode = (e: unknown): unknown => codeOf(e);
17
17
 
18
18
  export const nodeFiles: Files = {
19
19
  isFile(p: AbsolutePath): boolean {
@@ -8,7 +8,9 @@ import type { AbsolutePath } from "../../domain/paths.ts";
8
8
  import { errorCode, nodeFiles } from "./files.io.ts";
9
9
 
10
10
  const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-files-test-")));
11
- after(() => rmSync(root, { recursive: true, force: true }));
11
+ after(() => {
12
+ rmSync(root, { recursive: true, force: true });
13
+ });
12
14
  const at = (...p: string[]) => join(root, ...p) as AbsolutePath;
13
15
 
14
16
  test("Files: a missing file reads as null and is not a file", () => {
@@ -3,6 +3,7 @@
3
3
  * it — `error`, `status`, `signal`, all nullable — and turns it into a `ProcessExit`, so no caller
4
4
  * re-derives "what happened" from three fields.
5
5
  */
6
+ import { codeOf, printed } from "../../domain/text.ts";
6
7
  import {
7
8
  spawnSync,
8
9
  type SpawnSyncOptionsWithStringEncoding,
@@ -14,13 +15,13 @@ import type { Command, ProcessExit, RunProcess } from "../../ports/process.ts";
14
15
  export type SpawnSync = typeof spawnSync;
15
16
 
16
17
  const errno = (r: SpawnSyncReturns<string>): string | undefined =>
17
- (r.error as NodeJS.ErrnoException | undefined)?.code;
18
+ codeOf(r.error);
18
19
 
19
20
  /** What a finished `spawnSync` came to. ENOENT is the executable itself: `c.file` was not found. */
20
21
  export function exitOf(r: SpawnSyncReturns<string>, c: Command): ProcessExit {
21
22
  const out = {
22
- stdout: String(r.stdout ?? ""),
23
- stderr: String(r.stderr ?? ""),
23
+ stdout: printed(r.stdout),
24
+ stderr: printed(r.stderr),
24
25
  };
25
26
  const code = errno(r);
26
27
  if (code === "ENOENT") return { kind: "not-found", file: c.file };
@@ -58,7 +58,7 @@ test("a program killed by a signal is `signalled`, with what it printed first",
58
58
  test("output past maxOutputBytes is a spawn failure, named by Node's own message", () => {
59
59
  const r = run.run({ ...sh("head -c 100000 /dev/zero"), maxOutputBytes: 10 });
60
60
  assert.equal(r.kind, "spawn-failed");
61
- assert.match(r.kind === "spawn-failed" ? r.message : "", /ENOBUFS/);
61
+ assert.match(r.message, /ENOBUFS/);
62
62
  });
63
63
 
64
64
  test("cwd is the Command's when it names one", () => {
@@ -20,7 +20,9 @@ import { afterAll as after, test } from "vitest";
20
20
  import { tmpWorkspace } from "./workspace.io.ts";
21
21
 
22
22
  const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-ws-test-")));
23
- after(() => rmSync(root, { recursive: true, force: true }));
23
+ after(() => {
24
+ rmSync(root, { recursive: true, force: true });
25
+ });
24
26
 
25
27
  test("the directory exists inside the scope and is gone after it", () => {
26
28
  const ws = tmpWorkspace(root);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `fill.ts` — what is recovered from pdf.js's exports for each text item: whether it is upright, and
3
3
  * the fill it was drawn in (the operator-list walk). Operator lists here are written by hand; the
4
- * real-PDF half runs in `pdf-facts.harness.mjs` and `test/e2e/banal.mjs`.
4
+ * real-PDF half runs in `pdf-facts.harness.ts` and `test/e2e/banal.ts`.
5
5
  */
6
6
  import assert from "node:assert/strict";
7
7
  import { test } from "vitest";
@@ -34,7 +34,7 @@ const OPS = {
34
34
  showText: 5,
35
35
  showSpacedText: 6,
36
36
  };
37
- const glyphs = (s: string) => [[...s].map((unicode) => ({ unicode }))];
37
+ const glyphs = (s: string) => [Array.from(s, (unicode) => ({ unicode }))];
38
38
  const ops = (...pairs: [number, unknown?][]): OperatorList => ({
39
39
  fnArray: pairs.map((p) => p[0]),
40
40
  argsArray: pairs.map((p) => p[1] ?? null),
@@ -33,9 +33,12 @@ export interface OperatorList {
33
33
  /** Text as the walk compares it: normalised, with no whitespace (items and glyphs split spaces differently). */
34
34
  const squeeze = (s: string): string => s.normalize("NFKC").replace(/\s+/g, "");
35
35
 
36
+ /** An operator's argument list; pdf.js types it `any`, and a malformed one reads as empty. */
37
+ const listOf = (v: unknown): readonly unknown[] => (Array.isArray(v) ? v : []);
38
+
36
39
  /** `setFillRGBColor`'s argument: a `#rrggbb` string in current pdf.js, three 0-255 numbers in older ones. */
37
40
  export function hexOf(args: unknown): string {
38
- const a = Array.isArray(args) ? (args as unknown[]) : [];
41
+ const a = listOf(args);
39
42
  if (typeof a[0] === "string") return a[0].toLowerCase();
40
43
  const byte = (v: unknown): string =>
41
44
  Math.round(typeof v === "number" ? v : 0)
@@ -46,12 +49,13 @@ export function hexOf(args: unknown): string {
46
49
 
47
50
  /** The characters a show-text operator draws, from its glyph objects. */
48
51
  function glyphText(args: unknown): string {
49
- const a = Array.isArray(args) ? (args as unknown[]) : [];
50
- const glyphs = Array.isArray(a[0]) ? (a[0] as unknown[]) : [];
52
+ const glyphs = listOf(listOf(args)[0]);
51
53
  return glyphs
52
54
  .map((g) =>
53
55
  typeof g === "object" && g !== null && "unicode" in g
54
- ? String((g as { unicode: unknown }).unicode ?? "")
56
+ ? typeof g.unicode === "string"
57
+ ? g.unicode
58
+ : ""
55
59
  : "",
56
60
  )
57
61
  .join("");
@@ -76,7 +80,7 @@ function applyState(
76
80
  else if (fn === ops.setFillRGBColor)
77
81
  s.cur.fill = { kind: "rgb", hex: hexOf(args) };
78
82
  else if (fn === ops.setTextRenderingMode)
79
- s.cur.mode = Number((args as unknown[] | undefined)?.[0] ?? 0);
83
+ s.cur.mode = Number(listOf(args)[0] ?? 0);
80
84
  else return false;
81
85
  return true;
82
86
  }
@@ -89,7 +93,10 @@ interface Drawn {
89
93
 
90
94
  /** Every drawn character, in content order, with the fill it was drawn in. */
91
95
  export function drawnCharacters(ops: ColourOps, list: OperatorList): Drawn[] {
92
- const s = { cur: { fill: { kind: "unknown" } as Fill, mode: 0 }, stack: [] };
96
+ const s: { cur: State; stack: State[] } = {
97
+ cur: { fill: { kind: "unknown" }, mode: 0 },
98
+ stack: [],
99
+ };
93
100
  const drawn: Drawn[] = [];
94
101
  list.fnArray.forEach((fn, i) => {
95
102
  const args = list.argsArray[i];
@@ -149,7 +156,7 @@ export function fillsFor<T>(
149
156
  const drawn = drawnCharacters(ops, list);
150
157
  let at = 0;
151
158
  const fillOf = (text: string): Fill => {
152
- const want = [...squeeze(text)];
159
+ const want = Array.from(squeeze(text));
153
160
  if (want.length === 0) return { kind: "unknown" };
154
161
  const hit = findRun(drawn, want, at);
155
162
  if (hit === null) return { kind: "unknown" };
@@ -10,7 +10,6 @@ import {
10
10
  EMPTY_LOOKUP_CACHE,
11
11
  type LookupCache,
12
12
  } from "../../domain/lookup-cache.ts";
13
- // @ts-expect-error — a skill script in .mjs, it has no types
14
13
  import * as cites from "../../../skills/verify-citations/scripts/verify-cites.mjs";
15
14
 
16
15
  /** The verdicts of a cold run — an empty cache, the adapter the CLI wires. */
@@ -46,29 +45,29 @@ const dblp = (title: string, authors: string[]) =>
46
45
  /** Route by URL prefix; the longest prefix wins. */
47
46
  function fakeFetch(routes: Record<string, () => Response>): string[] {
48
47
  const calls: string[] = [];
49
- vi.stubGlobal("fetch", async (url: string) => {
48
+ vi.stubGlobal("fetch", (url: string) => {
50
49
  calls.push(url);
51
- const hit = Object.keys(routes)
52
- .filter((p) => url.startsWith(p))
53
- .sort((a, b) => b.length - a.length)[0];
54
- if (hit === undefined) return json(404);
55
- return routes[hit]!();
50
+ const hit = Object.entries(routes)
51
+ .filter(([p]) => url.startsWith(p))
52
+ .sort(([a], [b]) => b.length - a.length)[0];
53
+ return Promise.resolve(hit === undefined ? json(404) : hit[1]());
56
54
  });
57
55
  return calls;
58
56
  }
59
57
 
60
58
  /** Await `p` while fake time runs, so DBLP's pauses between requests elapse at once. */
61
59
  async function settle<T>(p: Promise<T>): Promise<T> {
62
- let done = false;
63
- const out = p.finally(() => (done = true));
64
- while (!done) await vi.advanceTimersByTimeAsync(1000);
60
+ // A field, not a `let`: the flag is set in a callback, which flow analysis cannot see.
61
+ const state = { done: false };
62
+ const out = p.finally(() => (state.done = true));
63
+ while (!state.done) await vi.advanceTimersByTimeAsync(1000);
65
64
  return out;
66
65
  }
67
66
 
68
67
  test("no service answers: not-checked, with the reason", async () => {
69
- vi.stubGlobal("fetch", async () => {
70
- throw new Error("getaddrinfo ENOTFOUND api.crossref.org");
71
- });
68
+ vi.stubGlobal("fetch", () =>
69
+ Promise.reject(new Error("getaddrinfo ENOTFOUND api.crossref.org")),
70
+ );
72
71
  assert.deepEqual(await checkOnly("@misc{k, title={T}}"), {
73
72
  kind: "not-checked",
74
73
  why: "the citation services cannot be reached (getaddrinfo ENOTFOUND api.crossref.org)",
@@ -104,19 +103,19 @@ test("each entry gets its existence and its authors: confirmed, fabricated, mism
104
103
  const r = await settle(checkOnly(bib));
105
104
  assert.equal(r.kind, "checked");
106
105
  const byKey = Object.fromEntries(
107
- (r.kind === "checked" ? r.entries : []).map((e) => [
108
- e.key,
109
- [e.exists, e.authors, e.why ?? ""],
110
- ]),
106
+ r.entries.map((e) => [e.key, [e.exists, e.authors, e.why ?? ""]]),
111
107
  );
112
108
  assert.deepEqual(
113
109
  {
114
- good: byKey["good"]!.slice(0, 2),
115
- fake: byKey["fake"]!.slice(0, 2),
116
- drift: byKey["drift"]!.slice(0, 2),
117
- driftWhy: String(byKey["drift"]![2]).split("; ").pop(),
118
- flaky: [byKey["flaky"]![1], String(byKey["flaky"]![2]).split("; ").pop()],
119
- pre: byKey["pre"]!.slice(0, 2),
110
+ good: byKey["good"]?.slice(0, 2),
111
+ fake: byKey["fake"]?.slice(0, 2),
112
+ drift: byKey["drift"]?.slice(0, 2),
113
+ driftWhy: String(byKey["drift"]?.[2]).split("; ").pop(),
114
+ flaky: [
115
+ byKey["flaky"]?.[1],
116
+ String(byKey["flaky"]?.[2]).split("; ").pop(),
117
+ ],
118
+ pre: byKey["pre"]?.slice(0, 2),
120
119
  },
121
120
  {
122
121
  good: ["true", "match"],
@@ -209,20 +208,14 @@ test(
209
208
  slowFetch(40);
210
209
  const bib = manyBib(20);
211
210
  const serial: string[] = [];
212
- for (const c of cites.parseBib(bib) as { id: string }[])
213
- serial.push(
214
- (
215
- (await cites.verifyCitationLive(c, { cache: {} })) as {
216
- verdict: string;
217
- }
218
- ).verdict,
219
- );
211
+ for (const c of cites.parseBib(bib))
212
+ serial.push((await cites.verifyCitationLive(c, { cache: {} })).verdict);
220
213
  const t0 = Date.now();
221
214
  const r = await checkOnly(bib);
222
215
  const ms = Date.now() - t0;
223
216
  assert.equal(r.kind, "checked");
224
217
  assert.deepEqual(
225
- r.kind === "checked" ? r.entries.map((e) => e.exists) : [],
218
+ r.entries.map((e) => e.exists),
226
219
  serial,
227
220
  );
228
221
  assert.ok(ms < (20 * 200) / 2, `${String(ms)} ms for 20 lookups`);
@@ -249,7 +242,7 @@ const PAPERS = [
249
242
  "@inproceedings{good, author={Ada Lovelace}, title={Good Paper}, booktitle={ICSE}, doi={10.1/good}}",
250
243
  "@inproceedings{other, author={Ada Lovelace}, title={Other Paper}, booktitle={ICSE}}",
251
244
  "@misc{pre, author={Ada Lovelace}, title={A Preprint}, journal={arXiv preprint}}",
252
- ];
245
+ ] as const;
253
246
  /** Every service answers; DBLP knows "Good Paper" and nothing else. */
254
247
  const answering = () =>
255
248
  fakeFetch({
@@ -414,7 +407,7 @@ test("a failed lookup is not cached: the next run asks it again", async () => {
414
407
  "https://api.crossref.org/": () => json(200, { message: { items: [] } }),
415
408
  "https://dblp.org/": () => json(500),
416
409
  });
417
- const bib = PAPERS[1]!;
410
+ const bib = PAPERS[1];
418
411
  const r = await settle(cached(bib, EMPTY_LOOKUP_CACHE));
419
412
  const failed: LookupCache = r.cache;
420
413
  assert.deepEqual(
@@ -427,10 +420,10 @@ test("a failed lookup is not cached: the next run asks it again", async () => {
427
420
  });
428
421
 
429
422
  test("offline with an incomplete cache: not-checked, and the cache comes back unchanged", async () => {
430
- vi.stubGlobal("fetch", async () => {
431
- throw new Error("getaddrinfo ENOTFOUND api.crossref.org");
432
- });
433
- const r = await cached(PAPERS[0]!, EMPTY_LOOKUP_CACHE);
423
+ vi.stubGlobal("fetch", () =>
424
+ Promise.reject(new Error("getaddrinfo ENOTFOUND api.crossref.org")),
425
+ );
426
+ const r = await cached(PAPERS[0], EMPTY_LOOKUP_CACHE);
434
427
  assert.deepEqual(r, {
435
428
  check: {
436
429
  kind: "not-checked",