paperlint 2.0.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 (762) hide show
  1. package/.github/dependabot.yml +72 -0
  2. package/.github/workflows/ci.yml +297 -0
  3. package/.github/workflows/dependabot-automerge.yml +70 -0
  4. package/.github/workflows/pr-title.yml +59 -0
  5. package/.github/workflows/release.yml +54 -0
  6. package/CLAUDE.md +598 -0
  7. package/CONTRIBUTING.md +159 -0
  8. package/LICENSE +21 -0
  9. package/README.md +240 -0
  10. package/action.harness.mjs +287 -0
  11. package/action.mutations.mjs +162 -0
  12. package/action.yml +138 -0
  13. package/bin/rpp.mjs +43 -0
  14. package/dist/action-ref.d.ts +12 -0
  15. package/dist/action-ref.d.ts.map +1 -0
  16. package/dist/action-ref.js +16 -0
  17. package/dist/action-ref.js.map +1 -0
  18. package/dist/adapters/banal/failure.d.ts +73 -0
  19. package/dist/adapters/banal/failure.d.ts.map +1 -0
  20. package/dist/adapters/banal/failure.js +58 -0
  21. package/dist/adapters/banal/failure.js.map +1 -0
  22. package/dist/adapters/banal/index.d.ts +17 -0
  23. package/dist/adapters/banal/index.d.ts.map +1 -0
  24. package/dist/adapters/banal/index.js +56 -0
  25. package/dist/adapters/banal/index.js.map +1 -0
  26. package/dist/adapters/banal/install.d.ts +26 -0
  27. package/dist/adapters/banal/install.d.ts.map +1 -0
  28. package/dist/adapters/banal/install.js +15 -0
  29. package/dist/adapters/banal/install.js.map +1 -0
  30. package/dist/adapters/banal/invocation.d.ts +48 -0
  31. package/dist/adapters/banal/invocation.d.ts.map +1 -0
  32. package/dist/adapters/banal/invocation.js +43 -0
  33. package/dist/adapters/banal/invocation.js.map +1 -0
  34. package/dist/adapters/banal/locate.d.ts +50 -0
  35. package/dist/adapters/banal/locate.d.ts.map +1 -0
  36. package/dist/adapters/banal/locate.js +34 -0
  37. package/dist/adapters/banal/locate.js.map +1 -0
  38. package/dist/adapters/banal/output.d.ts +27 -0
  39. package/dist/adapters/banal/output.d.ts.map +1 -0
  40. package/dist/adapters/banal/output.js +112 -0
  41. package/dist/adapters/banal/output.js.map +1 -0
  42. package/dist/adapters/banal/pin.d.ts +19 -0
  43. package/dist/adapters/banal/pin.d.ts.map +1 -0
  44. package/dist/adapters/banal/pin.js +15 -0
  45. package/dist/adapters/banal/pin.js.map +1 -0
  46. package/dist/adapters/banal/probe.d.ts +12 -0
  47. package/dist/adapters/banal/probe.d.ts.map +1 -0
  48. package/dist/adapters/banal/probe.js +27 -0
  49. package/dist/adapters/banal/probe.js.map +1 -0
  50. package/dist/adapters/banal/run.d.ts +89 -0
  51. package/dist/adapters/banal/run.d.ts.map +1 -0
  52. package/dist/adapters/banal/run.js +104 -0
  53. package/dist/adapters/banal/run.js.map +1 -0
  54. package/dist/adapters/banal/settings.d.ts +18 -0
  55. package/dist/adapters/banal/settings.d.ts.map +1 -0
  56. package/dist/adapters/banal/settings.js +29 -0
  57. package/dist/adapters/banal/settings.js.map +1 -0
  58. package/dist/adapters/banal/xml.d.ts +48 -0
  59. package/dist/adapters/banal/xml.d.ts.map +1 -0
  60. package/dist/adapters/banal/xml.js +67 -0
  61. package/dist/adapters/banal/xml.js.map +1 -0
  62. package/dist/adapters/curl/download.io.d.ts +14 -0
  63. package/dist/adapters/curl/download.io.d.ts.map +1 -0
  64. package/dist/adapters/curl/download.io.js +69 -0
  65. package/dist/adapters/curl/download.io.js.map +1 -0
  66. package/dist/adapters/curl/index.d.ts +6 -0
  67. package/dist/adapters/curl/index.d.ts.map +1 -0
  68. package/dist/adapters/curl/index.js +6 -0
  69. package/dist/adapters/curl/index.js.map +1 -0
  70. package/dist/adapters/memory/index.d.ts +43 -0
  71. package/dist/adapters/memory/index.d.ts.map +1 -0
  72. package/dist/adapters/memory/index.js +79 -0
  73. package/dist/adapters/memory/index.js.map +1 -0
  74. package/dist/adapters/node/files.io.d.ts +3 -0
  75. package/dist/adapters/node/files.io.d.ts.map +1 -0
  76. package/dist/adapters/node/files.io.js +31 -0
  77. package/dist/adapters/node/files.io.js.map +1 -0
  78. package/dist/adapters/node/host.io.d.ts +3 -0
  79. package/dist/adapters/node/host.io.d.ts.map +1 -0
  80. package/dist/adapters/node/host.io.js +14 -0
  81. package/dist/adapters/node/host.io.js.map +1 -0
  82. package/dist/adapters/node/index.d.ts +25 -0
  83. package/dist/adapters/node/index.d.ts.map +1 -0
  84. package/dist/adapters/node/index.js +14 -0
  85. package/dist/adapters/node/index.js.map +1 -0
  86. package/dist/adapters/node/process.io.d.ts +14 -0
  87. package/dist/adapters/node/process.io.d.ts.map +1 -0
  88. package/dist/adapters/node/process.io.js +41 -0
  89. package/dist/adapters/node/process.io.js.map +1 -0
  90. package/dist/adapters/node/workspace.io.d.ts +4 -0
  91. package/dist/adapters/node/workspace.io.d.ts.map +1 -0
  92. package/dist/adapters/node/workspace.io.js +33 -0
  93. package/dist/adapters/node/workspace.io.js.map +1 -0
  94. package/dist/adapters/pdfjs/fill.d.ts +42 -0
  95. package/dist/adapters/pdfjs/fill.d.ts.map +1 -0
  96. package/dist/adapters/pdfjs/fill.js +91 -0
  97. package/dist/adapters/pdfjs/fill.js.map +1 -0
  98. package/dist/build-engine.d.ts +48 -0
  99. package/dist/build-engine.d.ts.map +1 -0
  100. package/dist/build-engine.js +148 -0
  101. package/dist/build-engine.js.map +1 -0
  102. package/dist/build.d.ts +163 -0
  103. package/dist/build.d.ts.map +1 -0
  104. package/dist/build.js +575 -0
  105. package/dist/build.js.map +1 -0
  106. package/dist/cli.d.ts +151 -0
  107. package/dist/cli.d.ts.map +1 -0
  108. package/dist/cli.js +951 -0
  109. package/dist/cli.js.map +1 -0
  110. package/dist/doctor.d.ts +42 -0
  111. package/dist/doctor.d.ts.map +1 -0
  112. package/dist/doctor.js +280 -0
  113. package/dist/doctor.js.map +1 -0
  114. package/dist/domain/geometry.d.ts +71 -0
  115. package/dist/domain/geometry.d.ts.map +1 -0
  116. package/dist/domain/geometry.js +35 -0
  117. package/dist/domain/geometry.js.map +1 -0
  118. package/dist/domain/host.d.ts +16 -0
  119. package/dist/domain/host.d.ts.map +1 -0
  120. package/dist/domain/host.js +8 -0
  121. package/dist/domain/host.js.map +1 -0
  122. package/dist/domain/page-layout.d.ts +34 -0
  123. package/dist/domain/page-layout.d.ts.map +1 -0
  124. package/dist/domain/page-layout.js +8 -0
  125. package/dist/domain/page-layout.js.map +1 -0
  126. package/dist/domain/paths.d.ts +5 -0
  127. package/dist/domain/paths.d.ts.map +1 -0
  128. package/dist/domain/paths.js +2 -0
  129. package/dist/domain/paths.js.map +1 -0
  130. package/dist/domain/result.d.ts +23 -0
  131. package/dist/domain/result.d.ts.map +1 -0
  132. package/dist/domain/result.js +10 -0
  133. package/dist/domain/result.js.map +1 -0
  134. package/dist/domain/sha256.d.ts +7 -0
  135. package/dist/domain/sha256.d.ts.map +1 -0
  136. package/dist/domain/sha256.js +14 -0
  137. package/dist/domain/sha256.js.map +1 -0
  138. package/dist/domain/text.d.ts +6 -0
  139. package/dist/domain/text.d.ts.map +1 -0
  140. package/dist/domain/text.js +7 -0
  141. package/dist/domain/text.js.map +1 -0
  142. package/dist/engine.d.ts +93 -0
  143. package/dist/engine.d.ts.map +1 -0
  144. package/dist/engine.js +119 -0
  145. package/dist/engine.js.map +1 -0
  146. package/dist/exit-code.d.ts +22 -0
  147. package/dist/exit-code.d.ts.map +1 -0
  148. package/dist/exit-code.js +10 -0
  149. package/dist/exit-code.js.map +1 -0
  150. package/dist/facts-file.d.ts +96 -0
  151. package/dist/facts-file.d.ts.map +1 -0
  152. package/dist/facts-file.js +134 -0
  153. package/dist/facts-file.js.map +1 -0
  154. package/dist/hooks-settings.d.ts +141 -0
  155. package/dist/hooks-settings.d.ts.map +1 -0
  156. package/dist/hooks-settings.js +306 -0
  157. package/dist/hooks-settings.js.map +1 -0
  158. package/dist/init.d.ts +201 -0
  159. package/dist/init.d.ts.map +1 -0
  160. package/dist/init.js +579 -0
  161. package/dist/init.js.map +1 -0
  162. package/dist/latex-log.d.ts +80 -0
  163. package/dist/latex-log.d.ts.map +1 -0
  164. package/dist/latex-log.js +187 -0
  165. package/dist/latex-log.js.map +1 -0
  166. package/dist/latex-loop.d.ts +129 -0
  167. package/dist/latex-loop.d.ts.map +1 -0
  168. package/dist/latex-loop.js +113 -0
  169. package/dist/latex-loop.js.map +1 -0
  170. package/dist/link-skills.d.ts +51 -0
  171. package/dist/link-skills.d.ts.map +1 -0
  172. package/dist/link-skills.js +199 -0
  173. package/dist/link-skills.js.map +1 -0
  174. package/dist/new-paper.d.ts +48 -0
  175. package/dist/new-paper.d.ts.map +1 -0
  176. package/dist/new-paper.js +110 -0
  177. package/dist/new-paper.js.map +1 -0
  178. package/dist/pdf-facts.d.ts +44 -0
  179. package/dist/pdf-facts.d.ts.map +1 -0
  180. package/dist/pdf-facts.js +239 -0
  181. package/dist/pdf-facts.js.map +1 -0
  182. package/dist/pdf-geometry.d.ts +170 -0
  183. package/dist/pdf-geometry.d.ts.map +1 -0
  184. package/dist/pdf-geometry.js +158 -0
  185. package/dist/pdf-geometry.js.map +1 -0
  186. package/dist/ports/download.d.ts +9 -0
  187. package/dist/ports/download.d.ts.map +1 -0
  188. package/dist/ports/download.js +2 -0
  189. package/dist/ports/download.js.map +1 -0
  190. package/dist/ports/files.d.ts +11 -0
  191. package/dist/ports/files.d.ts.map +1 -0
  192. package/dist/ports/files.js +2 -0
  193. package/dist/ports/files.js.map +1 -0
  194. package/dist/ports/measure-geometry.d.ts +8 -0
  195. package/dist/ports/measure-geometry.d.ts.map +1 -0
  196. package/dist/ports/measure-geometry.js +2 -0
  197. package/dist/ports/measure-geometry.js.map +1 -0
  198. package/dist/ports/process.d.ts +45 -0
  199. package/dist/ports/process.d.ts.map +1 -0
  200. package/dist/ports/process.js +2 -0
  201. package/dist/ports/process.js.map +1 -0
  202. package/dist/ports/tool-installer.d.ts +29 -0
  203. package/dist/ports/tool-installer.d.ts.map +1 -0
  204. package/dist/ports/tool-installer.js +2 -0
  205. package/dist/ports/tool-installer.js.map +1 -0
  206. package/dist/ports/workspace.d.ts +18 -0
  207. package/dist/ports/workspace.d.ts.map +1 -0
  208. package/dist/ports/workspace.js +2 -0
  209. package/dist/ports/workspace.js.map +1 -0
  210. package/dist/rules-config.d.ts +34 -0
  211. package/dist/rules-config.d.ts.map +1 -0
  212. package/dist/rules-config.js +132 -0
  213. package/dist/rules-config.js.map +1 -0
  214. package/dist/structure.d.ts +34 -0
  215. package/dist/structure.d.ts.map +1 -0
  216. package/dist/structure.js +149 -0
  217. package/dist/structure.js.map +1 -0
  218. package/dist/tex-requirements.d.ts +43 -0
  219. package/dist/tex-requirements.d.ts.map +1 -0
  220. package/dist/tex-requirements.js +127 -0
  221. package/dist/tex-requirements.js.map +1 -0
  222. package/dist/toolchain.d.ts +159 -0
  223. package/dist/toolchain.d.ts.map +1 -0
  224. package/dist/toolchain.js +542 -0
  225. package/dist/toolchain.js.map +1 -0
  226. package/dist/types.d.ts +110 -0
  227. package/dist/types.d.ts.map +1 -0
  228. package/dist/types.js +2 -0
  229. package/dist/types.js.map +1 -0
  230. package/docs/configuration.md +235 -0
  231. package/docs/e2e.md +152 -0
  232. package/docs/incidents.md +59 -0
  233. package/docs/install.md +170 -0
  234. package/docs/optional-rules.md +107 -0
  235. package/docs/package-shape-options.md +262 -0
  236. package/docs/prior-art/README.md +76 -0
  237. package/docs/prior-art/blocking-vs-advisory.md +83 -0
  238. package/docs/prior-art/content-delivery.md +124 -0
  239. package/docs/prior-art/multi-mode-tools.md +106 -0
  240. package/docs/prior-art/nondeterministic-checks.md +99 -0
  241. package/docs/prior-art/package-location.md +422 -0
  242. package/docs/prior-art/paper-folder-scaffolding.md +538 -0
  243. package/docs/prior-art/readme-structure.md +69 -0
  244. package/docs/prior-art/repro/README.md +92 -0
  245. package/docs/prior-art/repro/claim1-allowedtools.mjs +66 -0
  246. package/docs/prior-art/repro/claim1-at2.mjs +40 -0
  247. package/docs/prior-art/repro/claim1-crosschannel.mjs +54 -0
  248. package/docs/prior-art/repro/claim1-frontmatter.mjs +76 -0
  249. package/docs/prior-art/repro/claim1-hook-payload-reporter.mjs +10 -0
  250. package/docs/prior-art/repro/claim1-plugin-frontmatter.mjs +27 -0
  251. package/docs/prior-art/repro/claim1-plugin-skill.mjs +52 -0
  252. package/docs/prior-art/repro/claim1-project-skill.mjs +81 -0
  253. package/docs/prior-art/repro/claim2-marketplace-flat-asclaimed.json +1 -0
  254. package/docs/prior-art/repro/claim2-marketplace-negative-control.json +1 -0
  255. package/docs/prior-art/repro/claim2-marketplace-nested-exact.json +9 -0
  256. package/docs/prior-art/repro/claim2-marketplace-nested-noversion.json +9 -0
  257. package/docs/prior-art/repro/claim2-marketplace-nested-range.json +1 -0
  258. package/docs/prior-art/repro/claim3-imports.mjs +50 -0
  259. package/docs/prior-art/repro/claim4-find-package-json.mjs +8 -0
  260. package/docs/prior-art/repro/claim4-package-dir.mjs +39 -0
  261. package/docs/prior-art/repro/claim4-parent-arg.mjs +17 -0
  262. package/docs/prior-art/repro/claim4-resolve-apis.mjs +21 -0
  263. package/docs/prior-art/repro/claim4-setup-consumers.mjs +45 -0
  264. package/docs/prior-art/repro/claim4-yarn-pnp.mjs +70 -0
  265. package/docs/prior-art/repro/claim5-bin-launch.mjs +39 -0
  266. package/docs/prior-art/repro/claim5-exports-mutation.mjs +57 -0
  267. package/docs/prior-art/repro/claim5-resolved-location-and-bin.mjs +33 -0
  268. package/docs/prior-art/repro/claim6-candidate-ambiguity.mjs +17 -0
  269. package/docs/prior-art/repro/claim6-doc-path-candidates.mjs +27 -0
  270. package/docs/prior-art/test-tooling.md +131 -0
  271. package/docs/rules.md +58 -0
  272. package/docs/texlive-install-decision.md +230 -0
  273. package/docs/toolchain.md +152 -0
  274. package/eslint-rules/doc-fields.harness.mjs +336 -0
  275. package/eslint-rules/doc-fields.mjs +186 -0
  276. package/eslint-rules/doc-fields.mutations.mjs +96 -0
  277. package/eslint-rules/install-path-literals.harness.mjs +121 -0
  278. package/eslint-rules/install-path-literals.mjs +108 -0
  279. package/eslint-rules/install-path-literals.mutations.mjs +62 -0
  280. package/eslint-rules/latex-language.harness.mjs +599 -0
  281. package/eslint-rules/latex-language.mjs +591 -0
  282. package/eslint-rules/latex-language.mutations.mjs +196 -0
  283. package/eslint-rules/paper-research-question.harness.mjs +146 -0
  284. package/eslint-rules/paper-research-question.mjs +180 -0
  285. package/eslint-rules/paper-research-question.mutations.mjs +127 -0
  286. package/eslint-rules/paper-stages.harness.mjs +356 -0
  287. package/eslint-rules/paper-stages.mjs +455 -0
  288. package/eslint-rules/paper-stages.mutations.mjs +157 -0
  289. package/eslint-rules/paper-typography.harness.mjs +291 -0
  290. package/eslint-rules/paper-typography.mjs +313 -0
  291. package/eslint-rules/paper-typography.mutations.mjs +131 -0
  292. package/eslint-rules/papers.harness.mjs +259 -0
  293. package/eslint-rules/papers.mjs +166 -0
  294. package/eslint-rules/papers.mutations.mjs +186 -0
  295. package/eslint-rules/pdf-last-page-balance.harness.mjs +206 -0
  296. package/eslint-rules/pdf-last-page-balance.mjs +208 -0
  297. package/eslint-rules/review-findings-cause.harness.mjs +228 -0
  298. package/eslint-rules/review-findings-cause.mjs +135 -0
  299. package/eslint-rules/review-findings-cause.mutations.mjs +72 -0
  300. package/eslint-rules/temp-root-realpath.harness.mjs +176 -0
  301. package/eslint-rules/temp-root-realpath.mjs +129 -0
  302. package/eslint-rules/temp-root-realpath.mutations.mjs +99 -0
  303. package/eslint-rules/tex-build.harness.mjs +753 -0
  304. package/eslint-rules/tex-build.mjs +322 -0
  305. package/eslint-rules/tex-build.mutations.mjs +258 -0
  306. package/eslint.config.mjs +521 -0
  307. package/fixtures/build-e2e/acmart/PIPELINE-STATUS.md +3 -0
  308. package/fixtures/build-e2e/acmart/paper.tex +11 -0
  309. package/fixtures/build-e2e/acmart/venue.json +1 -0
  310. package/fixtures/build-e2e/broken/PIPELINE-STATUS.md +3 -0
  311. package/fixtures/build-e2e/broken/paper.tex +7 -0
  312. package/fixtures/build-e2e/cite/PIPELINE-STATUS.md +3 -0
  313. package/fixtures/build-e2e/cite/build.sh +5 -0
  314. package/fixtures/build-e2e/cite/paper.tex +10 -0
  315. package/fixtures/build-e2e/cite/refs.bib +9 -0
  316. package/fixtures/build-e2e/empty/PIPELINE-STATUS.md +3 -0
  317. package/fixtures/build-e2e/empty/paper.tex +6 -0
  318. package/fixtures/build-e2e/fallback/PIPELINE-STATUS.md +3 -0
  319. package/fixtures/build-e2e/fallback/paper.tex +11 -0
  320. package/fixtures/build-e2e/guards/PIPELINE-STATUS.md +3 -0
  321. package/fixtures/build-e2e/guards/paper.tex +10 -0
  322. package/fixtures/build-e2e/no-source/PIPELINE-STATUS.md +3 -0
  323. package/fixtures/build-e2e/unbalanced/PIPELINE-STATUS.md +3 -0
  324. package/fixtures/build-e2e/unbalanced/paper.tex +28 -0
  325. package/fixtures/build-e2e/unbalanced/refs.bib +269 -0
  326. package/fixtures/install-path-literals/clean.fixture.mjs +3 -0
  327. package/fixtures/install-path-literals/clean.md +15 -0
  328. package/fixtures/install-path-literals/defect.fixture.mjs +3 -0
  329. package/fixtures/install-path-literals/defect.md +14 -0
  330. package/fixtures/latex-language/clean.tex +50 -0
  331. package/fixtures/latex-language/defect.tex +52 -0
  332. package/fixtures/paper-research-question/comment-only/PIPELINE-STATUS.md +9 -0
  333. package/fixtures/paper-research-question/comment-only/paper.tex +7 -0
  334. package/fixtures/paper-research-question/declared-not-in-paper/PIPELINE-STATUS.md +10 -0
  335. package/fixtures/paper-research-question/declared-not-in-paper/paper.tex +6 -0
  336. package/fixtures/paper-research-question/draft/PIPELINE-STATUS.md +6 -0
  337. package/fixtures/paper-research-question/draft/paper.tex +2 -0
  338. package/fixtures/paper-research-question/markdown-no-rq/PIPELINE-STATUS.md +9 -0
  339. package/fixtures/paper-research-question/markdown-no-rq/paper.md +4 -0
  340. package/fixtures/paper-research-question/shipped-no-rq/PIPELINE-STATUS.md +12 -0
  341. package/fixtures/paper-research-question/shipped-no-rq/paper.tex +3 -0
  342. package/fixtures/paper-research-question/shipped-with-rq/PIPELINE-STATUS.md +10 -0
  343. package/fixtures/paper-research-question/shipped-with-rq/paper.tex +2 -0
  344. package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +16 -0
  345. package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +17 -0
  346. package/fixtures/paper-stages/nofile/PIPELINE-STATUS.md +8 -0
  347. package/fixtures/paper-stages/noheader/PIPELINE-STATUS.md +1 -0
  348. package/fixtures/paper-stages/noheader/versions/2026-07-22-submitted.pdf +0 -0
  349. package/fixtures/paper-stages/nothing/PIPELINE-STATUS.md +3 -0
  350. package/fixtures/paper-stages/ok/PIPELINE-STATUS.md +9 -0
  351. package/fixtures/paper-stages/ok/versions/2026-07-22-submitted.pdf +0 -0
  352. package/fixtures/paper-stages/stale/PIPELINE-STATUS.md +1 -0
  353. package/fixtures/paper-stages/stale/versions/2026-07-22-submitted.STALE-WRONG-FILE.pdf +0 -0
  354. package/fixtures/paper-stages/twice/PIPELINE-STATUS.md +14 -0
  355. package/fixtures/paper-stages/twice/versions/2026-08-06-submitted.pdf +0 -0
  356. package/fixtures/paper-stages/twice/versions/2026-10-24-submitted.pdf +0 -0
  357. package/fixtures/paper-stages/undeclared/PIPELINE-STATUS.md +8 -0
  358. package/fixtures/paper-stages/undeclared/versions/2026-07-22-submitted.pdf +0 -0
  359. package/fixtures/paper-stages/undeclared/versions/2026-08-29-camera-ready.pdf +0 -0
  360. package/fixtures/paper-stages/wrongsize/PIPELINE-STATUS.md +8 -0
  361. package/fixtures/paper-stages/wrongsize/versions/2026-07-22-submitted.pdf +0 -0
  362. package/fixtures/paper-typography/clean-paper/paper.tex +29 -0
  363. package/fixtures/paper-typography/messy-paper/paper.tex +27 -0
  364. package/fixtures/pdf-facts/README.md +22 -0
  365. package/fixtures/pdf-facts/corrupt-font.pdf +0 -0
  366. package/fixtures/pdf-facts/encrypted.pdf +0 -0
  367. package/fixtures/pdf-facts/hidden-text.pdf +0 -0
  368. package/fixtures/pdf-facts/hidden-text.tex +28 -0
  369. package/fixtures/pdf-facts/t3-all.pdf +0 -0
  370. package/fixtures/pdf-facts/t3-all.tex +8 -0
  371. package/fixtures/pdf-facts/t3-mixed.pdf +0 -0
  372. package/fixtures/pdf-facts/t3-mixed.tex +9 -0
  373. package/fixtures/pdf-facts/ttf.pdf +2240 -1
  374. package/fixtures/pdf-facts/ttf.tex +6 -0
  375. package/fixtures/real-markdown-paper/baseline.json +24 -0
  376. package/fixtures/real-markdown-paper/baseline.mjs +48 -0
  377. package/fixtures/render-paper/build-clean.sh +25 -0
  378. package/fixtures/render-paper/build-defect.sh +15 -0
  379. package/fixtures/review-findings-cause/clean.md +17 -0
  380. package/fixtures/review-findings-cause/defect.md +14 -0
  381. package/fixtures/review-findings-cause/old-debt.md +14 -0
  382. package/fixtures/review-findings-cause/quiet-in-fence.md +16 -0
  383. package/fixtures/tex-build/clean.tex +21 -0
  384. package/fixtures/tex-build/defect.tex +24 -0
  385. package/fixtures/tex-build/frontmatter-clean.tex +25 -0
  386. package/fixtures/tex-build/frontmatter-defect.tex +23 -0
  387. package/fixtures/toolchain-mirror/catalog.txt +5 -0
  388. package/fixtures/toolchain-mirror/install-tl +27 -0
  389. package/fixtures/toolchain-mirror/release-texlive.txt +3 -0
  390. package/fixtures/toolchain-mirror/release-year +1 -0
  391. package/fixtures/toolchain-mirror/stub-kpsewhich +8 -0
  392. package/fixtures/toolchain-mirror/stub-pdflatex +3 -0
  393. package/fixtures/toolchain-mirror/stub-tlmgr +44 -0
  394. package/hooks/hooks.harness.mjs +713 -0
  395. package/hooks/hooks.mutations.mjs +337 -0
  396. package/hooks/paper-edit-guard.hook.d.mts +13 -0
  397. package/hooks/paper-edit-guard.hook.mjs +457 -0
  398. package/hooks/paper-skills-nudge.hook.mjs +136 -0
  399. package/hooks/paper-status-gates.hook.mjs +156 -0
  400. package/hooks/paper-status-gates.sh +91 -0
  401. package/lib/agent-cli-version.harness.mjs +165 -0
  402. package/lib/agent-cli-version.mjs +106 -0
  403. package/lib/agent-cli-version.mutations.mjs +109 -0
  404. package/lib/markdown.mjs +386 -0
  405. package/lib/mutation-driver.harness.mjs +227 -0
  406. package/lib/mutation-driver.mjs +397 -0
  407. package/lib/mutation-driver.mutations.mjs +68 -0
  408. package/lib/paper-config.d.mts +34 -0
  409. package/lib/paper-config.harness.mjs +286 -0
  410. package/lib/paper-config.mjs +142 -0
  411. package/lib/paper-config.mutations.mjs +143 -0
  412. package/lib/skill-checks.mjs +701 -0
  413. package/lib/skill-corpus.mjs +403 -0
  414. package/lib/skill-eval-fixture.mjs +63 -0
  415. package/lib/skill-eval-kit.mjs +257 -0
  416. package/lib/skill-trigger-cases.harness.mjs +170 -0
  417. package/lib/skill-trigger-cases.mjs +446 -0
  418. package/lib/skill-trigger-cases.mutations.mjs +65 -0
  419. package/lib/trigger-ledger.mjs +215 -0
  420. package/package.json +97 -0
  421. package/plugin/.claude-plugin/plugin.json +8 -0
  422. package/plugin/hooks/hooks.json +30 -0
  423. package/scripts/check.harness.mjs +177 -0
  424. package/scripts/check.mjs +239 -0
  425. package/scripts/check.mutations.mjs +110 -0
  426. package/scripts/eslint-report-guard.mjs +82 -0
  427. package/scripts/exclusive.mjs +138 -0
  428. package/scripts/harness-api.frozen.json +76 -0
  429. package/scripts/harness-api.test.ts +175 -0
  430. package/scripts/layer-legacy-frozen.d.mts +28 -0
  431. package/scripts/layer-legacy-frozen.mjs +152 -0
  432. package/scripts/layer-legacy-frozen.test.ts +115 -0
  433. package/scripts/layer-legacy.frozen.json +50 -0
  434. package/scripts/mutation-batteries-frozen.harness.mjs +204 -0
  435. package/scripts/mutation-batteries-frozen.mjs +238 -0
  436. package/scripts/mutation-batteries.frozen.json +117 -0
  437. package/scripts/release-config.test.ts +90 -0
  438. package/scripts/rules-are-content-only.harness.mjs +113 -0
  439. package/scripts/rules-are-content-only.mjs +138 -0
  440. package/scripts/rules-are-content-only.mutations.mjs +81 -0
  441. package/scripts/rules-see-files.harness.mjs +115 -0
  442. package/scripts/rules-see-files.mjs +99 -0
  443. package/scripts/rules-see-files.mutations.mjs +131 -0
  444. package/scripts/run-mutations.mjs +100 -0
  445. package/scripts/semantic-release-plugins.d.ts +16 -0
  446. package/skills/README.md +15 -0
  447. package/skills/analyze-sibling-paper/SKILL.md +170 -0
  448. package/skills/analyze-sibling-paper/SKILL.md.spec.ts +186 -0
  449. package/skills/analyze-sibling-paper/analyze-sibling-paper.eval.mjs +19 -0
  450. package/skills/analyze-sibling-paper/analyze-sibling-paper.harness.mjs +23 -0
  451. package/skills/argument-arc/SKILL.md +177 -0
  452. package/skills/argument-arc/SKILL.md.spec.ts +192 -0
  453. package/skills/argument-arc/argument-arc.eval.mjs +19 -0
  454. package/skills/argument-arc/argument-arc.harness.mjs +23 -0
  455. package/skills/build-benchmark/SKILL.md +213 -0
  456. package/skills/build-benchmark/SKILL.md.spec.ts +220 -0
  457. package/skills/build-benchmark/build-benchmark.eval.mjs +19 -0
  458. package/skills/build-benchmark/build-benchmark.harness.mjs +23 -0
  459. package/skills/build-benchmark/references/adversarial-cold-repro.md +68 -0
  460. package/skills/camera-ready/SKILL.md +148 -0
  461. package/skills/camera-ready/SKILL.md.spec.ts +164 -0
  462. package/skills/camera-ready/camera-ready.eval.mjs +19 -0
  463. package/skills/camera-ready/camera-ready.harness.mjs +23 -0
  464. package/skills/cold-read-diff/SKILL.md +160 -0
  465. package/skills/cold-read-diff/SKILL.md.spec.ts +166 -0
  466. package/skills/cold-read-diff/cold-read-diff.eval.mjs +19 -0
  467. package/skills/cold-read-diff/cold-read-diff.harness.mjs +23 -0
  468. package/skills/draft-paper/SKILL.md +152 -0
  469. package/skills/draft-paper/SKILL.md.spec.ts +169 -0
  470. package/skills/draft-paper/draft-paper.eval.mjs +19 -0
  471. package/skills/draft-paper/draft-paper.harness.mjs +23 -0
  472. package/skills/extend-paper/SKILL.md +99 -0
  473. package/skills/extend-paper/SKILL.md.spec.ts +116 -0
  474. package/skills/extend-paper/extend-paper.eval.mjs +19 -0
  475. package/skills/extend-paper/extend-paper.harness.mjs +23 -0
  476. package/skills/find-venue/SKILL.md +128 -0
  477. package/skills/find-venue/SKILL.md.spec.ts +145 -0
  478. package/skills/find-venue/find-venue.eval.mjs +19 -0
  479. package/skills/find-venue/find-venue.harness.mjs +23 -0
  480. package/skills/grade-paper-writing/SKILL.md +436 -0
  481. package/skills/grade-paper-writing/SKILL.md.spec.ts +453 -0
  482. package/skills/grade-paper-writing/fixtures/control_gopen.txt +1 -0
  483. package/skills/grade-paper-writing/fixtures/control_human_paper.txt +1 -0
  484. package/skills/grade-paper-writing/fixtures/rewrite.txt +1 -0
  485. package/skills/grade-paper-writing/fixtures/specimen.txt +1 -0
  486. package/skills/grade-paper-writing/fixtures/structure-checks.md +22 -0
  487. package/skills/grade-paper-writing/grade-paper-writing.eval.mjs +19 -0
  488. package/skills/grade-paper-writing/grade-paper-writing.harness.mjs +23 -0
  489. package/skills/grade-paper-writing/prose-lint.mjs +713 -0
  490. package/skills/harden-paper/SKILL.md +318 -0
  491. package/skills/harden-paper/SKILL.md.spec.ts +336 -0
  492. package/skills/harden-paper/check-numbers.sh +33 -0
  493. package/skills/harden-paper/check-release-claims.sh +35 -0
  494. package/skills/harden-paper/fixtures/uncited-assertions-sample.md +43 -0
  495. package/skills/harden-paper/fixtures/uncited-assertions-sample.tex +77 -0
  496. package/skills/harden-paper/harden-paper.eval.mjs +19 -0
  497. package/skills/harden-paper/harden-paper.harness.mjs +23 -0
  498. package/skills/map-prior-work/SKILL.md +211 -0
  499. package/skills/map-prior-work/SKILL.md.spec.ts +227 -0
  500. package/skills/map-prior-work/map-prior-work.eval.mjs +19 -0
  501. package/skills/map-prior-work/map-prior-work.harness.mjs +23 -0
  502. package/skills/osf-artifact-upload/SKILL.md +52 -0
  503. package/skills/osf-artifact-upload/SKILL.md.spec.ts +59 -0
  504. package/skills/osf-artifact-upload/osf-artifact-upload.eval.mjs +22 -0
  505. package/skills/osf-artifact-upload/osf-artifact-upload.harness.mjs +103 -0
  506. package/skills/paper-adversarial-review/SKILL.md +126 -0
  507. package/skills/paper-adversarial-review/SKILL.md.spec.ts +142 -0
  508. package/skills/paper-adversarial-review/paper-adversarial-review.eval.mjs +19 -0
  509. package/skills/paper-adversarial-review/paper-adversarial-review.harness.mjs +23 -0
  510. package/skills/paper-pipeline/PIPELINE-MAP.md +371 -0
  511. package/skills/paper-pipeline/SKILL.md +499 -0
  512. package/skills/paper-pipeline/SKILL.md.spec.ts +517 -0
  513. package/skills/paper-pipeline/description-language.eval.mjs +347 -0
  514. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +891 -0
  515. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +1254 -0
  516. package/skills/paper-pipeline/paper-pipeline.eval.mjs +22 -0
  517. package/skills/paper-pipeline/paper-pipeline.harness.mjs +143 -0
  518. package/skills/paper-pipeline/pipeline-firing.baseline.json +270 -0
  519. package/skills/paper-pipeline/pipeline-firing.eval.mjs +664 -0
  520. package/skills/paper-pipeline/pipeline-language.eval.mjs +672 -0
  521. package/skills/paper-pipeline/references/acceptance-gate.md +329 -0
  522. package/skills/paper-pipeline/references/acl-venue-rules.md +142 -0
  523. package/skills/paper-pipeline/references/anonymization.md +68 -0
  524. package/skills/paper-pipeline/references/artifact-checklist.md +93 -0
  525. package/skills/paper-pipeline/references/body-vs-appendix.md +97 -0
  526. package/skills/paper-pipeline/references/credit-criteria.md +69 -0
  527. package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/README.md +35 -0
  528. package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/run_retext.mjs +24 -0
  529. package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/sentences.txt +11 -0
  530. package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/test_sentences.py +25 -0
  531. package/skills/paper-pipeline/references/occupancy-2026-08-06-prose-checkers.md +538 -0
  532. package/skills/paper-pipeline/references/occupancy-2026-08-06-reproducible-tooling.md +431 -0
  533. package/skills/paper-pipeline/references/occupancy-2026-08-06-staleness-and-orchestration.md +592 -0
  534. package/skills/paper-pipeline/references/pipeline-status-template.md +162 -0
  535. package/skills/paper-pipeline/references/review-ratchet.md +36 -0
  536. package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +585 -0
  537. package/skills/paper-pipeline/references/writing-craft.md +448 -0
  538. package/skills/paper-pipeline/repro/2026-08-07-description-language-control.log +63 -0
  539. package/skills/paper-pipeline/repro/2026-08-07-fork-check.log +52 -0
  540. package/skills/paper-pipeline/repro/2026-08-07-fork-check2.log +33 -0
  541. package/skills/paper-pipeline/repro/2026-08-07-language-eval-pilot.log +33 -0
  542. package/skills/paper-pipeline/repro/2026-08-07-language-eval-raw.log +166 -0
  543. package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-oracle.json +338 -0
  544. package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-oracle.log +118 -0
  545. package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-raw.log +245 -0
  546. package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary.json +776 -0
  547. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-A6-oracle.log +53 -0
  548. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-A6-raw.log +89 -0
  549. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-oracle.json +450 -0
  550. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-oracle.log +136 -0
  551. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-raw.log +242 -0
  552. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-setupdiff.log +59 -0
  553. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation.json +1032 -0
  554. package/skills/paper-pipeline/repro/2026-08-08-parent-replication-gpw.json +139 -0
  555. package/skills/paper-pipeline/repro/2026-08-08-parent-replication-gpw.log +98 -0
  556. package/skills/paper-pipeline/repro/2026-08-08-parent-replication.mjs +92 -0
  557. package/skills/paper-pipeline/repro/README.md +129 -0
  558. package/skills/paper-pipeline/repro/analyze-language-eval.py +116 -0
  559. package/skills/paper-pipeline/scripts/README.md +344 -0
  560. package/skills/paper-pipeline/scripts/announce.mjs +67 -0
  561. package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +496 -0
  562. package/skills/paper-pipeline/scripts/artifact-coverage.mjs +397 -0
  563. package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +218 -0
  564. package/skills/paper-pipeline/scripts/check-provenance.mjs +184 -0
  565. package/skills/paper-pipeline/scripts/consumer.d.mts +32 -0
  566. package/skills/paper-pipeline/scripts/consumer.harness.mjs +562 -0
  567. package/skills/paper-pipeline/scripts/consumer.mjs +535 -0
  568. package/skills/paper-pipeline/scripts/consumer.mutations.mjs +190 -0
  569. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +457 -0
  570. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +656 -0
  571. package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +54 -0
  572. package/skills/paper-pipeline/scripts/fixtures/clean/PIPELINE-STATUS.md +51 -0
  573. package/skills/paper-pipeline/scripts/fixtures/dirty/PIPELINE-STATUS.md +52 -0
  574. package/skills/paper-pipeline/scripts/fixtures/dirty/paper.md +6 -0
  575. package/skills/paper-pipeline/scripts/fixtures/real-bib/refs.bib +153 -0
  576. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +466 -0
  577. package/skills/paper-pipeline/scripts/generated-code.mjs +338 -0
  578. package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +254 -0
  579. package/skills/paper-pipeline/scripts/ledger.mjs +623 -0
  580. package/skills/paper-pipeline/scripts/ledger.selftest.mjs +286 -0
  581. package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +389 -0
  582. package/skills/paper-pipeline/scripts/pipeline-check.mjs +737 -0
  583. package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +54 -0
  584. package/skills/paper-pipeline/scripts/pipeline-edges.mjs +169 -0
  585. package/skills/paper-pipeline/scripts/population-map.harness.mjs +178 -0
  586. package/skills/paper-pipeline/scripts/population-map.mjs +181 -0
  587. package/skills/paper-pipeline/scripts/population-map.mutations.mjs +65 -0
  588. package/skills/paper-pipeline/scripts/population-map.selftest.mjs +122 -0
  589. package/skills/paper-pipeline/scripts/provenance.harness.mjs +240 -0
  590. package/skills/paper-pipeline/scripts/provenance.mutations.mjs +59 -0
  591. package/skills/paper-pipeline/scripts/round-diff.harness.mjs +881 -0
  592. package/skills/paper-pipeline/scripts/round-diff.mjs +576 -0
  593. package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +276 -0
  594. package/skills/paper-pipeline/scripts/run-mechanical.mjs +633 -0
  595. package/skills/paper-pipeline/scripts/status.mjs +295 -0
  596. package/skills/paper-status/SKILL.md +183 -0
  597. package/skills/paper-status/SKILL.md.spec.ts +190 -0
  598. package/skills/paper-status/paper-status.eval.mjs +22 -0
  599. package/skills/paper-status/paper-status.harness.mjs +25 -0
  600. package/skills/pc-panel-review/SKILL.md +263 -0
  601. package/skills/pc-panel-review/SKILL.md.spec.ts +280 -0
  602. package/skills/pc-panel-review/pc-panel-review.eval.mjs +19 -0
  603. package/skills/pc-panel-review/pc-panel-review.harness.mjs +23 -0
  604. package/skills/plan-paper-timeline/SKILL.md +182 -0
  605. package/skills/plan-paper-timeline/SKILL.md.spec.ts +200 -0
  606. package/skills/plan-paper-timeline/fixtures/fake-google-calendar.mjs +239 -0
  607. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +431 -0
  608. package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +65 -0
  609. package/skills/plan-paper-timeline/plan-paper-timeline.eval.mjs +19 -0
  610. package/skills/plan-paper-timeline/plan-paper-timeline.harness.mjs +23 -0
  611. package/skills/render-paper/SKILL.md +159 -0
  612. package/skills/render-paper/SKILL.md.spec.ts +166 -0
  613. package/skills/render-paper/check-render.sh +419 -0
  614. package/skills/render-paper/checkers-requirements.txt +55 -0
  615. package/skills/render-paper/ensure-checkers.sh +69 -0
  616. package/skills/render-paper/extract-pdf-facts.harness.mjs +166 -0
  617. package/skills/render-paper/extract-pdf-facts.mjs +144 -0
  618. package/skills/render-paper/render-paper.eval.mjs +19 -0
  619. package/skills/render-paper/render-paper.harness.mjs +339 -0
  620. package/skills/research-ideate/SKILL.md +136 -0
  621. package/skills/research-ideate/SKILL.md.spec.ts +152 -0
  622. package/skills/research-ideate/research-ideate.eval.mjs +19 -0
  623. package/skills/research-ideate/research-ideate.harness.mjs +23 -0
  624. package/skills/skill-contract.mutations.mjs +179 -0
  625. package/skills/study-accepted-papers/SKILL.md +206 -0
  626. package/skills/study-accepted-papers/SKILL.md.spec.ts +223 -0
  627. package/skills/study-accepted-papers/study-accepted-papers.eval.mjs +19 -0
  628. package/skills/study-accepted-papers/study-accepted-papers.harness.mjs +23 -0
  629. package/skills/submit-paper/SKILL.md +182 -0
  630. package/skills/submit-paper/SKILL.md.spec.ts +199 -0
  631. package/skills/submit-paper/check-deanon.sh +149 -0
  632. package/skills/submit-paper/references/publishers/acm.md +92 -0
  633. package/skills/submit-paper/references/venues/agenticdev.jsonc +108 -0
  634. package/skills/submit-paper/references/venues/agenticdev.md +139 -0
  635. package/skills/submit-paper/references/venues/agenticdev.tex +19 -0
  636. package/skills/submit-paper/references/venues/aisec.jsonc +101 -0
  637. package/skills/submit-paper/references/venues/aisec.md +105 -0
  638. package/skills/submit-paper/references/venues/paper-guards.tex +41 -0
  639. package/skills/submit-paper/references/venues/realm.jsonc +81 -0
  640. package/skills/submit-paper/references/venues/realm.md +155 -0
  641. package/skills/submit-paper/references/venues/tex-base.jsonc +50 -0
  642. package/skills/submit-paper/references/venues/venue-profile.schema.json +74 -0
  643. package/skills/submit-paper/submit-paper.eval.mjs +19 -0
  644. package/skills/submit-paper/submit-paper.harness.mjs +23 -0
  645. package/skills/sweep-design-space/SKILL.md +269 -0
  646. package/skills/sweep-design-space/SKILL.md.spec.ts +285 -0
  647. package/skills/sweep-design-space/sweep-design-space.eval.mjs +19 -0
  648. package/skills/sweep-design-space/sweep-design-space.harness.mjs +23 -0
  649. package/skills/tighten-paper/SKILL.md +368 -0
  650. package/skills/tighten-paper/SKILL.md.spec.ts +384 -0
  651. package/skills/tighten-paper/structure.mjs +371 -0
  652. package/skills/tighten-paper/tighten-paper.eval.mjs +19 -0
  653. package/skills/tighten-paper/tighten-paper.harness.mjs +23 -0
  654. package/skills/verify-citations/SKILL.md +328 -0
  655. package/skills/verify-citations/SKILL.md.spec.ts +345 -0
  656. package/skills/verify-citations/scripts/bib-authors.mjs +479 -0
  657. package/skills/verify-citations/scripts/bib-authors.test.mjs +175 -0
  658. package/skills/verify-citations/scripts/verify-cites.mjs +1108 -0
  659. package/skills/verify-citations/scripts/verify-cites.test.mjs +735 -0
  660. package/skills/verify-citations/verify-citations.eval.mjs +19 -0
  661. package/skills/verify-citations/verify-citations.harness.mjs +23 -0
  662. package/src/CLAUDE.md +51 -0
  663. package/src/action-ref.test.ts +26 -0
  664. package/src/action-ref.ts +15 -0
  665. package/src/adapters/banal/failure.test.ts +63 -0
  666. package/src/adapters/banal/failure.ts +118 -0
  667. package/src/adapters/banal/index.test.ts +119 -0
  668. package/src/adapters/banal/index.ts +100 -0
  669. package/src/adapters/banal/install.test.ts +20 -0
  670. package/src/adapters/banal/install.ts +41 -0
  671. package/src/adapters/banal/invocation.test.ts +74 -0
  672. package/src/adapters/banal/invocation.ts +95 -0
  673. package/src/adapters/banal/locate.test.ts +52 -0
  674. package/src/adapters/banal/locate.ts +84 -0
  675. package/src/adapters/banal/output.test.ts +140 -0
  676. package/src/adapters/banal/output.ts +141 -0
  677. package/src/adapters/banal/pin.ts +30 -0
  678. package/src/adapters/banal/probe.ts +35 -0
  679. package/src/adapters/banal/run.test.ts +191 -0
  680. package/src/adapters/banal/run.ts +244 -0
  681. package/src/adapters/banal/settings.test.ts +31 -0
  682. package/src/adapters/banal/settings.ts +55 -0
  683. package/src/adapters/banal/xml.test.ts +111 -0
  684. package/src/adapters/banal/xml.ts +112 -0
  685. package/src/adapters/curl/download.io.ts +73 -0
  686. package/src/adapters/curl/download.test.ts +55 -0
  687. package/src/adapters/curl/index.ts +5 -0
  688. package/src/adapters/memory/index.ts +131 -0
  689. package/src/adapters/node/files.io.ts +39 -0
  690. package/src/adapters/node/files.test.ts +28 -0
  691. package/src/adapters/node/host.io.ts +15 -0
  692. package/src/adapters/node/index.ts +36 -0
  693. package/src/adapters/node/process.io.ts +49 -0
  694. package/src/adapters/node/process.test.ts +46 -0
  695. package/src/adapters/node/workspace.io.ts +40 -0
  696. package/src/adapters/node/workspace.test.ts +58 -0
  697. package/src/adapters/pdfjs/fill.test.ts +111 -0
  698. package/src/adapters/pdfjs/fill.ts +141 -0
  699. package/src/build-engine.harness.mjs +314 -0
  700. package/src/build-engine.ts +219 -0
  701. package/src/build.harness.mjs +631 -0
  702. package/src/build.mutations.mjs +195 -0
  703. package/src/build.ts +793 -0
  704. package/src/cli.harness.mjs +2007 -0
  705. package/src/cli.mutations.mjs +448 -0
  706. package/src/cli.ts +1189 -0
  707. package/src/doctor.harness.mjs +396 -0
  708. package/src/doctor.mutations.mjs +175 -0
  709. package/src/doctor.ts +356 -0
  710. package/src/domain/geometry.ts +108 -0
  711. package/src/domain/host.ts +23 -0
  712. package/src/domain/page-layout.ts +32 -0
  713. package/src/domain/paths.ts +5 -0
  714. package/src/domain/result.test.ts +26 -0
  715. package/src/domain/result.ts +29 -0
  716. package/src/domain/sha256.test.ts +12 -0
  717. package/src/domain/sha256.ts +21 -0
  718. package/src/domain/text.ts +11 -0
  719. package/src/engine.harness.mjs +252 -0
  720. package/src/engine.ts +176 -0
  721. package/src/exit-code.test.ts +21 -0
  722. package/src/exit-code.ts +38 -0
  723. package/src/facts-file.test.ts +240 -0
  724. package/src/facts-file.ts +241 -0
  725. package/src/hooks-settings.harness.mjs +386 -0
  726. package/src/hooks-settings.mutations.mjs +116 -0
  727. package/src/hooks-settings.ts +434 -0
  728. package/src/init.ts +900 -0
  729. package/src/latex-log.harness.mjs +226 -0
  730. package/src/latex-log.ts +234 -0
  731. package/src/latex-loop.harness.mjs +449 -0
  732. package/src/latex-loop.ts +211 -0
  733. package/src/link-skills.harness.mjs +273 -0
  734. package/src/link-skills.mutations.mjs +136 -0
  735. package/src/link-skills.ts +258 -0
  736. package/src/new-paper.harness.mjs +216 -0
  737. package/src/new-paper.mutations.mjs +79 -0
  738. package/src/new-paper.ts +158 -0
  739. package/src/pdf-facts.harness.mjs +188 -0
  740. package/src/pdf-facts.ts +327 -0
  741. package/src/pdf-geometry.harness.mjs +254 -0
  742. package/src/pdf-geometry.ts +300 -0
  743. package/src/ports/download.ts +10 -0
  744. package/src/ports/files.ts +11 -0
  745. package/src/ports/measure-geometry.ts +8 -0
  746. package/src/ports/process.ts +46 -0
  747. package/src/ports/tool-installer.ts +33 -0
  748. package/src/ports/workspace.ts +20 -0
  749. package/src/rules-config.harness.mjs +114 -0
  750. package/src/rules-config.ts +178 -0
  751. package/src/structure.harness.mjs +179 -0
  752. package/src/structure.mutations.mjs +83 -0
  753. package/src/structure.ts +166 -0
  754. package/src/tex-requirements.harness.mjs +238 -0
  755. package/src/tex-requirements.ts +181 -0
  756. package/src/toolchain.harness.mjs +651 -0
  757. package/src/toolchain.ts +755 -0
  758. package/src/types.ts +106 -0
  759. package/templates/paper/PIPELINE-STATUS.md +72 -0
  760. package/templates/paper/paper.md +4 -0
  761. package/templates/paper/paper.tex +8 -0
  762. package/tsconfig.json +23 -0
@@ -0,0 +1,327 @@
1
+ /**
2
+ * READ A FINISHED PDF with pdf.js — the shell around `pdf-geometry.ts` and `adapters/banal/xml.ts`. Page
3
+ * count, the fonts the pages draw text with, the last page's words, and every page's text boxes
4
+ * (the input banal measures page geometry from, written as pdftohtml XML by `adapters/banal/xml.ts`).
5
+ *
6
+ * pdf.js arrives as the npm package `unpdf` (a zero-dependency build of Mozilla's pdf.js), so
7
+ * reading a PDF needs nothing installed on the system. It replaced three poppler programs
8
+ * (`pdfinfo`, `pdffonts`, `pdftotext -bbox`), which every consumer had to install by hand; the
9
+ * equivalence was measured before the switch (issue #61, see `pdf-geometry.ts`).
10
+ *
11
+ * ── THE RESULT IS A UNION, AND A FAILED READ CANNOT LOOK LIKE A CLEAN ONE ─────
12
+ * `readPdf` never throws for a property of the file: an unreadable or encrypted PDF, and a PDF whose
13
+ * pages draw text while pdf.js resolves no font for it, come back as `ok: false` with a reason. The
14
+ * last case is the one that matters: on Node 20 pdf.js returns zero fonts and no error, and an empty
15
+ * font list reads as "no Type 3, nothing unembedded" — a clean verdict over nothing.
16
+ *
17
+ * ── LAZY ────────────────────────────────────────────────────────────────────────
18
+ * `unpdf` is imported on the first read, not when this module loads: `paperlint lint` imports the build
19
+ * and must not pay pdf.js's start-up for a command that never opens a PDF.
20
+ */
21
+ // eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
22
+ import { readFileSync } from "node:fs";
23
+ import {
24
+ DEFAULT_ASCENT,
25
+ DEFAULT_DESCENT,
26
+ fontsOf,
27
+ wordsOf,
28
+ type Fonts,
29
+ type PageText,
30
+ type RawFont,
31
+ type RawPage,
32
+ type TextRun,
33
+ } from "./pdf-geometry.ts";
34
+ import type { PageLayout, TextBox } from "./domain/page-layout.ts";
35
+ // eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
36
+ import { fillsOf, isUpright } from "./adapters/pdfjs/fill.ts";
37
+
38
+ type PdfJs = Awaited<ReturnType<typeof import("unpdf").getResolvedPDFJS>>;
39
+ type Doc = Awaited<ReturnType<PdfJs["getDocument"]>["promise"]>;
40
+ type Page = Awaited<ReturnType<Doc["getPage"]>>;
41
+ type Item = Awaited<ReturnType<Page["getTextContent"]>>["items"][number];
42
+ type TextItem = Extract<Item, { str: string }>;
43
+
44
+ /**
45
+ * What `readPdf` measured. `last` is the last page's words; `classifyLastPage` reads it. `layout` is
46
+ * every page's text boxes, for banal (`adapters/banal/xml.ts` writes them as the XML banal reads).
47
+ */
48
+ export interface PdfFacts {
49
+ readonly pages: number;
50
+ readonly fonts: Fonts;
51
+ readonly last: PageText;
52
+ readonly layout: readonly PageLayout[];
53
+ }
54
+
55
+ export type PdfReadFailure =
56
+ "unreadable" | "encrypted" | "zero-fonts-on-text" | "empty";
57
+
58
+ export type PdfRead =
59
+ | { readonly ok: true; readonly facts: PdfFacts }
60
+ | {
61
+ readonly ok: false;
62
+ readonly reason: PdfReadFailure;
63
+ readonly detail: string;
64
+ };
65
+
66
+ /** The port `paperlint build` and the facts writer take, so a test can hand them any outcome. */
67
+ export type PdfReader = (path: string) => Promise<PdfRead>;
68
+
69
+ /**
70
+ * The options every read uses, exactly as measured in the spike. `fontExtraProperties` is
71
+ * load-bearing: without it pdf.js exports no `type`/`subtype`, and every font's program is unknown.
72
+ * `disableFontFace` keeps pdf.js from trying to install fonts into a page that does not exist here.
73
+ */
74
+ export const PDFJS_OPTIONS = {
75
+ disableFontFace: true,
76
+ fontExtraProperties: true,
77
+ verbosity: 0,
78
+ useSystemFonts: false,
79
+ isEvalSupported: false,
80
+ } as const;
81
+
82
+ let pdfjs: Promise<PdfJs> | null = null;
83
+ function loadPdfJs(): Promise<PdfJs> {
84
+ // eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
85
+ pdfjs ??= import("unpdf").then((m) => m.getResolvedPDFJS());
86
+ return pdfjs;
87
+ }
88
+
89
+ const fail = (reason: PdfReadFailure, detail: string): PdfRead => ({
90
+ ok: false,
91
+ reason,
92
+ detail,
93
+ });
94
+
95
+ /** A font object pdf.js left in `commonObjs`, read into a `RawFont`, or null when it is not one. */
96
+ export function rawFontOf(id: string, obj: unknown): RawFont | null {
97
+ if (typeof obj !== "object" || obj === null) return null;
98
+ const o = obj as Record<string, unknown>;
99
+ if (!("loadedName" in o) && !("isType3Font" in o)) return null;
100
+ const str = (v: unknown): string | undefined =>
101
+ typeof v === "string" ? v : undefined;
102
+ return {
103
+ id,
104
+ name: str(o["name"]) ?? "",
105
+ type: str(o["type"]),
106
+ subtype: str(o["subtype"]),
107
+ isType3Font: o["isType3Font"] === true,
108
+ missingFile: o["missingFile"] === true,
109
+ };
110
+ }
111
+
112
+ const isText = (it: Item): it is TextItem =>
113
+ "str" in it && it.str.trim() !== "";
114
+
115
+ /** The font pdf.js resolved for `id`, if it did. `get` throws on an unresolved id; `has` does not. */
116
+ function resolved(page: Page, id: string): unknown {
117
+ return page.commonObjs.has(id) ? page.commonObjs.get(id) : null;
118
+ }
119
+
120
+ /**
121
+ * The fonts pdf.js has loaded for the document so far, by id. `commonObjs` is shared by every page
122
+ * and holds fonts alongside other objects; `rawFontOf` keeps only the fonts.
123
+ */
124
+ function loadedFonts(page: Page): Map<string, RawFont> {
125
+ const out = new Map<string, RawFont>();
126
+ for (const [id, obj] of page.commonObjs as Iterable<[string, unknown]>) {
127
+ const f = rawFontOf(id, obj);
128
+ if (f) out.set(id, f);
129
+ }
130
+ return out;
131
+ }
132
+
133
+ /**
134
+ * One page: its text items, and the fonts it drew with — the fonts its text items name, plus every
135
+ * font first loaded while its operator list ran. The second half is not redundant: pdf.js can
136
+ * render a run in a font that no text item names (measured on an all-Type-3 page: four fonts
137
+ * loaded, two named by items; poppler lists four). A page that reuses an earlier page's fonts loads
138
+ * none, and its items name them.
139
+ */
140
+ async function readPage(
141
+ page: Page,
142
+ seen: ReadonlySet<string>,
143
+ lib: PdfJs,
144
+ ): Promise<{ raw: RawPage; items: TextItem[]; layout: PageLayout }> {
145
+ // The operator list is what makes pdf.js load a page's fonts into `commonObjs`.
146
+ const ops = await page.getOperatorList();
147
+ const items = (await page.getTextContent()).items.filter(isText);
148
+ const loaded = loadedFonts(page);
149
+ const named = new Set(items.map((it) => it.fontName));
150
+ const fonts = [...loaded.values()].filter(
151
+ (f) => named.has(f.id) || !seen.has(f.id),
152
+ );
153
+ return {
154
+ raw: { hasText: items.length > 0, fonts },
155
+ items,
156
+ layout: layoutOf(page, items, ops, lib),
157
+ };
158
+ }
159
+
160
+ /**
161
+ * A font's ascent and descent as fractions of its size, or the defaults when pdf.js has none.
162
+ *
163
+ * ⚠️ Type 3 fonts get the defaults: pdf.js exports no ascent for them. The obvious substitute,
164
+ * FontBBox scaled by FontMatrix (what poppler uses), was measured on `fixtures/pdf-facts/t3-all.pdf`
165
+ * and came out FURTHER from poppler than the defaults: 8.5 pt off on the left column against 7.2.
166
+ * Both are far below any balance tolerance, so the simpler rule stays.
167
+ */
168
+ function metricsOf(font: unknown): { ascent: number; descent: number } {
169
+ const f = (font ?? {}) as Record<string, unknown>;
170
+ const ascent = f["ascent"];
171
+ if (typeof ascent !== "number" || !(ascent > 0))
172
+ return { ascent: DEFAULT_ASCENT, descent: DEFAULT_DESCENT };
173
+ const d = f["descent"];
174
+ return { ascent, descent: typeof d === "number" ? d : DEFAULT_DESCENT };
175
+ }
176
+
177
+ /** The last page's text runs in top-down coordinates, and its size. */
178
+ function pageText(page: Page, items: readonly TextItem[]): PageText {
179
+ const vp = page.getViewport({ scale: 1 });
180
+ const runs: TextRun[] = items.map((it) => {
181
+ const [a = 0, b = 0, , d = 0, e = 0, f = 0] = it.transform as number[];
182
+ const [x, baseline] = vp.convertToViewportPoint(e, f) as [number, number];
183
+ const size = Math.hypot(a, b) || it.height || Math.abs(d);
184
+ return {
185
+ text: it.str,
186
+ x,
187
+ baseline,
188
+ width: it.width,
189
+ size,
190
+ ...metricsOf(resolved(page, it.fontName)),
191
+ };
192
+ });
193
+ return {
194
+ widthPt: vp.width,
195
+ heightPt: vp.height,
196
+ words: runs.flatMap(wordsOf),
197
+ };
198
+ }
199
+
200
+ /** The font name pdf.js resolved for an item, else its internal id. */
201
+ function fontNameOf(font: unknown, id: string): string {
202
+ const name = (font as { name?: unknown } | null)?.name;
203
+ return typeof name === "string" && name ? name : id;
204
+ }
205
+
206
+ /**
207
+ * One text item as a box, in points, top-down. The size is the item's vertical scale, which is what
208
+ * pdftohtml reports as the font size; upright is judged in VIEWER space (the page's /Rotate applied),
209
+ * because that is where pdftohtml draws.
210
+ */
211
+ function boxOf(
212
+ page: Page,
213
+ vp: ReturnType<Page["getViewport"]>,
214
+ it: TextItem,
215
+ lib: PdfJs,
216
+ ): Omit<TextBox, "fill"> {
217
+ const [a = 0, b = 0, c = 0, d = 0, e = 0, f = 0] = it.transform as number[];
218
+ const [x, baseline] = vp.convertToViewportPoint(e, f) as [number, number];
219
+ const size = Math.hypot(c, d) || Math.hypot(a, b) || it.height;
220
+ const font = resolved(page, it.fontName);
221
+ const m = metricsOf(font);
222
+ return {
223
+ top: baseline - m.ascent * size,
224
+ left: x,
225
+ width: it.width,
226
+ height: (m.ascent - m.descent) * size,
227
+ size,
228
+ font: fontNameOf(font, it.fontName),
229
+ text: it.str,
230
+ upright: isUpright(
231
+ lib.Util.transform(vp.transform, it.transform) as number[],
232
+ ),
233
+ };
234
+ }
235
+
236
+ /** Every text item of a page as a box with its fill — the input of `pdf2xml`. */
237
+ function layoutOf(
238
+ page: Page,
239
+ items: readonly TextItem[],
240
+ ops: Awaited<ReturnType<Page["getOperatorList"]>>,
241
+ lib: PdfJs,
242
+ ): PageLayout {
243
+ const vp = page.getViewport({ scale: 1 });
244
+ const fills = fillsOf(
245
+ lib.OPS,
246
+ ops,
247
+ items.map((it) => it.str),
248
+ );
249
+ const boxes = items.map((it, k): TextBox => ({
250
+ ...boxOf(page, vp, it, lib),
251
+ fill: fills[k] ?? { kind: "unknown" },
252
+ }));
253
+ return { widthPt: vp.width, heightPt: vp.height, boxes };
254
+ }
255
+
256
+ /** Every page's fonts, and the last page's words. */
257
+ async function factsOf(doc: Doc, lib: PdfJs): Promise<PdfRead> {
258
+ const read: {
259
+ page: Page;
260
+ raw: RawPage;
261
+ items: TextItem[];
262
+ layout: PageLayout;
263
+ }[] = [];
264
+ const seen = new Set<string>();
265
+ for (let i = 1; i <= doc.numPages; i++) {
266
+ const page = await doc.getPage(i);
267
+ const r = await readPage(page, seen, lib);
268
+ for (const f of r.raw.fonts) seen.add(f.id);
269
+ read.push({ page, ...r });
270
+ }
271
+ const [last] = read.slice(-1);
272
+ if (!last) return fail("empty", "the PDF has no pages");
273
+ const fonts = fontsOf(read.map((r) => r.raw));
274
+ if (fonts.kind === "zero-fonts-on-text")
275
+ return fail(
276
+ "zero-fonts-on-text",
277
+ `page(s) ${fonts.pages.join(", ")} draw text, but pdf.js resolved no font for it ` +
278
+ // eslint-disable-next-line no-restricted-globals -- legacy I/O, moves behind a port in #76
279
+ `(Node ${process.version}; paperlint needs Node >= 22.13, where pdf.js reports fonts)`,
280
+ );
281
+ return {
282
+ ok: true,
283
+ facts: {
284
+ pages: doc.numPages,
285
+ fonts,
286
+ last: pageText(last.page, last.items),
287
+ layout: read.map((r) => r.layout),
288
+ },
289
+ };
290
+ }
291
+
292
+ /** pdf.js's own exception names for the two conditions a caller can act on. */
293
+ function failureOf(e: unknown): PdfRead {
294
+ const err = e as { name?: string; message?: string };
295
+ const detail = `${err.name ?? "Error"}: ${err.message ?? String(e)}`;
296
+ return fail(
297
+ err.name === "PasswordException" ? "encrypted" : "unreadable",
298
+ detail,
299
+ );
300
+ }
301
+
302
+ /** Read a PDF from disk. Never throws for a property of the file; see the module header. */
303
+ export const readPdf: PdfReader = async (path) => {
304
+ let data: Uint8Array;
305
+ try {
306
+ data = new Uint8Array(readFileSync(path));
307
+ } catch (e) {
308
+ return fail("unreadable", (e as Error).message);
309
+ }
310
+ const lib = await loadPdfJs();
311
+ const task = lib.getDocument({ data, ...PDFJS_OPTIONS });
312
+ try {
313
+ return await factsOf(await task.promise, lib);
314
+ } catch (e) {
315
+ return failureOf(e);
316
+ } finally {
317
+ await task.destroy();
318
+ }
319
+ };
320
+
321
+ /** The one wording of a failed read, as a build step or a CLI prints it. */
322
+ export function describeFailure(
323
+ r: Extract<PdfRead, { ok: false }>,
324
+ pdf: string,
325
+ ): string {
326
+ return `could not read ${pdf} with pdf.js (${r.reason}): ${r.detail}`;
327
+ }
@@ -0,0 +1,254 @@
1
+ /**
2
+ * `pdf-geometry.ts` — the pure half of reading a PDF: the last page's columns, and the fonts.
3
+ *
4
+ * Every page here is written by hand as words, because the cases that matter are pages the fixture
5
+ * corpus does not contain. Each column case reproduces an error the poppler-based measurement made
6
+ * on 2026-08-29 (the midpoint cut, the review build, years mistaken for line numbers), ported from
7
+ * the harness of the function this replaces.
8
+ */
9
+ import assert from "node:assert/strict";
10
+ import { dirname, join } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const HERE = dirname(fileURLToPath(import.meta.url));
14
+ const {
15
+ classifyLastPage,
16
+ wordsOf,
17
+ fontsOf,
18
+ fontFact,
19
+ programOf,
20
+ popplerType,
21
+ STUB_WORDS,
22
+ } = await import(join(HERE, "pdf-geometry.ts"));
23
+
24
+ let n = 0;
25
+ const check = (label, cond) => {
26
+ assert.ok(cond, label);
27
+ n++;
28
+ };
29
+
30
+ const W = 612; // US letter, points
31
+
32
+ /** Words from rows `{x, top, n, step, text}`: `n` words stacked downwards, each 10 pt tall. */
33
+ const words = (rows) =>
34
+ rows.flatMap(({ x, top, n: count = 1, step = 10, text = "word" }) =>
35
+ Array.from({ length: count }, (_, i) => ({
36
+ x0: x,
37
+ y0: top + i * step,
38
+ x1: x + 20,
39
+ y1: top + i * step + 10,
40
+ text,
41
+ })),
42
+ );
43
+ const page = (rows, widthPt = W) => ({
44
+ widthPt,
45
+ heightPt: 792,
46
+ words: words(rows),
47
+ });
48
+
49
+ // ── the last page's columns ─────────────────────────────────────────────────────────────
50
+ {
51
+ const p = classifyLastPage(
52
+ page([
53
+ { x: 54, top: 60, n: 40 },
54
+ { x: 320, top: 60, n: 40 },
55
+ ]),
56
+ );
57
+ check(
58
+ "balanced: two identical columns measure identical heights",
59
+ p.kind === "measured" && p.columns[0] === 400 && p.columns[1] === 400,
60
+ );
61
+ }
62
+ {
63
+ const p = classifyLastPage(
64
+ page([
65
+ { x: 54, top: 60, n: 60 },
66
+ { x: 320, top: 60, n: 30 },
67
+ ]),
68
+ );
69
+ check(
70
+ "unbalanced: the 300 pt difference shows up as heights 600 / 300",
71
+ p.kind === "measured" && p.columns[0] === 600 && p.columns[1] === 300,
72
+ );
73
+ }
74
+ {
75
+ // 🔴 Split at the PAGE's middle. Only the left column is filled, lines span 54…300, and a page
76
+ // number sits centred at the bottom: the words' own midpoint would fall inside the left column.
77
+ const rows = [];
78
+ for (let i = 0; i < 20; i++)
79
+ for (const x of [54, 110, 170, 230, 280])
80
+ rows.push({ x, top: 60 + i * 10 });
81
+ rows.push({ x: 300, top: 700, text: "24" });
82
+ const p = classifyLastPage(page(rows));
83
+ check(
84
+ "midpoint: an empty right column measures 0 — the split is the page's middle, not the words'",
85
+ p.kind === "measured" && p.columns[1] === 0 && p.columns[0] > 600,
86
+ );
87
+ }
88
+ {
89
+ const body = [
90
+ { x: 54, top: 60, n: 60 },
91
+ { x: 320, top: 60, n: 10 },
92
+ ];
93
+ const left = classifyLastPage(
94
+ page([...body, { x: 20, top: 60, n: 60, text: "101" }]),
95
+ );
96
+ check(
97
+ "review (left margin): numbered lines make a review build, which is not measured",
98
+ left.kind === "review" && left.lineNumbers === 60,
99
+ );
100
+ const right = classifyLastPage(
101
+ page([...body, { x: 585, top: 60, n: 60, text: "2167" }]),
102
+ );
103
+ check(
104
+ "review (right margin): numbers in the right margin count too",
105
+ right.kind === "review",
106
+ );
107
+ check(
108
+ "review: without the line numbers the same page IS measured",
109
+ classifyLastPage(page(body)).kind === "measured",
110
+ );
111
+ const four = classifyLastPage(
112
+ page([...body, { x: 20, top: 60, n: 4, text: "7" }]),
113
+ );
114
+ check(
115
+ "review: four stray margin numbers are not a review build — five are",
116
+ four.kind === "measured" &&
117
+ classifyLastPage(page([...body, { x: 20, top: 60, n: 5, text: "7" }]))
118
+ .kind === "review",
119
+ );
120
+ }
121
+ {
122
+ const p = classifyLastPage(
123
+ page([
124
+ { x: 54, top: 60, n: 40, text: "2024" },
125
+ { x: 320, top: 60, n: 40, text: "2025" },
126
+ ]),
127
+ );
128
+ check(
129
+ "years in the body are not line numbers — a bibliography stays measured",
130
+ p.kind === "measured",
131
+ );
132
+ }
133
+ {
134
+ const stub = classifyLastPage(page([{ x: 54, top: 60, n: STUB_WORDS - 1 }]));
135
+ check(
136
+ "stub: a page with fewer than 60 words is a stub, with its word count",
137
+ stub.kind === "stub" && stub.words === STUB_WORDS - 1,
138
+ );
139
+ check(
140
+ "stub: at exactly 60 words the page is measured",
141
+ classifyLastPage(page([{ x: 54, top: 60, n: STUB_WORDS }])).kind ===
142
+ "measured",
143
+ );
144
+ }
145
+ check(
146
+ "a page with no width is a caller's defect: it throws, it is not a measurement",
147
+ (() => {
148
+ try {
149
+ classifyLastPage(page([{ x: 54, top: 60, n: 70 }], 0));
150
+ return false;
151
+ } catch (e) {
152
+ return e instanceof RangeError;
153
+ }
154
+ })(),
155
+ );
156
+
157
+ // ── words from a text run ───────────────────────────────────────────────────────────────
158
+ {
159
+ const ws = wordsOf({
160
+ text: "ab cd",
161
+ x: 100,
162
+ baseline: 200,
163
+ width: 60,
164
+ size: 10,
165
+ ascent: 0.8,
166
+ descent: -0.2,
167
+ });
168
+ check(
169
+ "a run splits at whitespace into words with proportional x",
170
+ ws.length === 2 &&
171
+ ws[0].text === "ab" &&
172
+ ws[0].x0 === 100 &&
173
+ ws[0].x1 === 120 &&
174
+ ws[1].x0 === 140,
175
+ );
176
+ check(
177
+ "a word spans ascent above the baseline to descent below it",
178
+ ws[0].y0 === 192 && ws[0].y1 === 202,
179
+ );
180
+ }
181
+
182
+ // ── fonts ───────────────────────────────────────────────────────────────────────────────
183
+ const font = (over) => ({
184
+ id: "f1",
185
+ name: "ABCDEF+LinLibertineT",
186
+ type: "Type1",
187
+ subtype: "Type1C",
188
+ isType3Font: false,
189
+ missingFile: false,
190
+ ...over,
191
+ });
192
+ check(
193
+ "an embedded font: the subset tag is dropped, the program named",
194
+ JSON.stringify(fontFact(font({}))) ===
195
+ JSON.stringify({
196
+ kind: "embedded",
197
+ name: "LinLibertineT",
198
+ program: "Type1C",
199
+ }),
200
+ );
201
+ check(
202
+ "🔴 a font with no usable program is NOT embedded — whatever else the PDF says about it",
203
+ fontFact(font({ name: "Times-Roman", subtype: undefined, missingFile: true }))
204
+ .kind === "not-embedded",
205
+ );
206
+ check(
207
+ "a Type 3 font is its own kind, named Type3",
208
+ JSON.stringify(fontFact(font({ isType3Font: true, name: "" }))) ===
209
+ JSON.stringify({ kind: "type3", name: "Type3" }),
210
+ );
211
+ check(
212
+ "programs: composite TrueType, CFF-in-CID, OpenType, and an unknown one kept verbatim",
213
+ programOf("CIDFontType2", undefined) === "CIDTrueType" &&
214
+ programOf("CIDFontType0", "CIDFontType0C") === "CIDType0C" &&
215
+ programOf("CIDFontType0", undefined) === "CIDType0" &&
216
+ programOf("TrueType", "OpenType") === "OpenType" &&
217
+ programOf("MMType1", undefined) === "MMType1" &&
218
+ programOf(undefined, undefined) === "unknown:undefined/undefined",
219
+ );
220
+ check(
221
+ "the type column keeps poppler's spelling, which consumers match on",
222
+ popplerType({ kind: "type3", name: "Type3" }) === "Type 3" &&
223
+ popplerType(fontFact(font({}))) === "Type 1C" &&
224
+ popplerType({ kind: "embedded", name: "x", program: "unknown:Foo/Bar" }) ===
225
+ "unknown:Foo/Bar",
226
+ );
227
+ {
228
+ const r = fontsOf([
229
+ { hasText: true, fonts: [font({ id: "a" }), font({ id: "b", name: "B" })] },
230
+ { hasText: true, fonts: [font({ id: "a" })] },
231
+ ]);
232
+ check(
233
+ "fonts: each font once, in the order pages first drew them",
234
+ r.kind === "drawn" && r.list.length === 2 && r.list[1].name === "B",
235
+ );
236
+ }
237
+ {
238
+ const r = fontsOf([
239
+ { hasText: true, fonts: [font({ id: "a" })] },
240
+ { hasText: true, fonts: [] },
241
+ ]);
242
+ check(
243
+ "🔴 text on a page with no font is a FAILED READ, naming the page — not an empty list",
244
+ r.kind === "zero-fonts-on-text" && r.pages.join() === "2",
245
+ );
246
+ }
247
+ check(
248
+ "no page draws text and there are no fonts: that is `no-text`, not a failure",
249
+ fontsOf([{ hasText: false, fonts: [] }]).kind === "no-text",
250
+ );
251
+
252
+ console.log(
253
+ `✓ ${String(n)} assertions passed — pdf-geometry: columns, stub/review, words from runs, fonts`,
254
+ );