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,479 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bib-authors.mjs — does our author list match the VERSION we claim to cite?
4
+ *
5
+ * The gap this fills, stated precisely so it does not drift into its neighbour:
6
+ * verify-cites.mjs asks "does this citation exist, and does the DOI point at it?"
7
+ * this script asks "we say @inproceedings{...NeurIPS 2023}; is our author list
8
+ * the NeurIPS one, or did we copy the arXiv preprint's?"
9
+ *
10
+ * 🔴 Why this is its own check (measured 2026-08-24, on a paper a `verify-citations`
11
+ * pass had already reported as "ALL 23 cites verified real"):
12
+ *
13
+ * schick2023toolformer — ours 8 authors. NeurIPS 2023 has NINE. Eric Hambro, a real
14
+ * person, was simply absent from a citation in an archival
15
+ * publication. The arXiv preprint has 8 — that is where ours
16
+ * came from.
17
+ * dehghani2022efficiency — ours: Dehghani, Arnab, Beyer, Vaswani, Tay.
18
+ * ICLR 2022: Dehghani, TAY, Arnab, Beyer, Vaswani.
19
+ * Again the preprint's list, under a proceedings entry.
20
+ *
21
+ * Both are invisible to an existence check: the paper is real, the id resolves, the title
22
+ * matches. The defect lives only in the gap between the venue we NAME and the metadata we
23
+ * CARRY, and nothing in the pipeline was looking there.
24
+ *
25
+ * DBLP is the source because it indexes both records separately and says so —
26
+ * "NeurIPS 2023" and "CoRR 2023" come back as two hits with different author lists. That
27
+ * side-by-side is the cleanest evidence available for this question.
28
+ *
29
+ * ⚠️ Discovery credit and a deliberate NON-dependency: this class was found by running
30
+ * `rebiber` (yuchenlin/rebiber), which rewrites bib entries to their DBLP records. We do
31
+ * NOT depend on it here. Two reasons, both practical: its install needs a Python toolchain
32
+ * that fights modern setuptools (bibtexparser wheel fails with `AttributeError:
33
+ * install_layout`; the workaround is vendoring a tarball), and its output REPLACES entries
34
+ * with DBLP's very long official booktitles, which is wrong for a page-limited paper. What
35
+ * we actually needed was the comparison, and that is one HTTPS call with no dependencies.
36
+ *
37
+ * 🔴 The normalisation below is load-bearing, not tidiness. A first cut of this comparison
38
+ * reported 13 mismatches on our bibliography, of which ELEVEN were "Last, First" vs
39
+ * "First Last" — pure formatting. A checker that cries 13 when 2 are real is read once and
40
+ * then ignored; that exact death is already recorded in this repo (a skill-pointer check
41
+ * that reported 33 findings of which 22 were live files). So: compare SURNAME SEQUENCES,
42
+ * and report nothing else.
43
+ *
44
+ * Usage: node bib-authors.mjs <paper-dir-or-.bib-or-.tex> [--json]
45
+ * Exit: 0 = no author-set/order differences 1 = differences found 2 = usage/IO error
46
+ */
47
+ import { readFileSync, existsSync, readdirSync } from "node:fs";
48
+ import { join, extname } from "node:path";
49
+ import {
50
+ headings as mdHeadings,
51
+ requireMarkdown,
52
+ } from "../../../lib/markdown.mjs";
53
+ import { isMain } from "../../paper-pipeline/scripts/consumer.mjs";
54
+
55
+ const DBLP = "https://dblp.org/search/publ/api";
56
+
57
+ /* ---------- input: a .bib, a .tex with filecontents, or a paper dir ---------- */
58
+
59
+ /**
60
+ * Some papers carry no BibTeX at all. `compile-rules-2026` writes its 67 references as a
61
+ * hand-numbered markdown list that `repro/md2acl.py` turns into ACL format at build time:
62
+ *
63
+ * 12. N. F. Liu, K. Lin, J. Hewitt. *Lost in the Middle: How Language Models Use Long
64
+ * Contexts.* TACL, 2024. arXiv:2307.03172.
65
+ *
66
+ * 🔴 Exempting such a paper would be the wrong call, and measurably so: a hand-written author
67
+ * list has NO machine-checkable source, so it is the MORE exposed of the two formats, not the
68
+ * less. The class this script hunts lives exactly where an entry names a published venue and an
69
+ * arXiv id in the same breath — which is most of this list.
70
+ *
71
+ * Authors are taken only from the text BEFORE the first `*`, on purpose: entries here sometimes
72
+ * append a second work ("— and the reanalysis: S. B. Hossain, ...") after the title, and folding
73
+ * those names in would invent co-authors that the first work does not have.
74
+ */
75
+ export function parseMarkdownRefs(text) {
76
+ // 🔴 The heading comes FROM THE PARSER, not from a regex (the base's rule "parse markdown with a
77
+ // parser"). The previous `text.search(/^#+\s*References\s*$/m)` opened the bibliography on a
78
+ // `## References` line QUOTED inside a ``` fence — and the papers in this repo quote their own
79
+ // markup in chunks. Exactly this assert already stands at a neighbour
80
+ // (`paper-pipeline/scripts/extract-ref-facts.harness.mjs`: "a heading inside a ``` block is not a
81
+ // heading"), that is, the class was known and was being reproduced here again.
82
+ // `requireMarkdown()` — so that a missing markdown-it fails LOUDLY: with an empty parser
83
+ // `headings()` returns [], and the gate would report "no references" instead of refusing. That is
84
+ // the same trade of one silent failure for another that the rule was written against.
85
+ requireMarkdown();
86
+ const h = mdHeadings(text).find((x) => /^References\s*$/u.test(x.text));
87
+ if (!h) return [];
88
+ const body = text.slice(h.offset);
89
+ const out = [];
90
+ // 🔴 `$(?![\s\S])`, NOT `\Z`. JavaScript has no `\Z` anchor — outside a unicode-mode pattern it
91
+ // is an identity escape meaning the LETTER Z, so the first version of this lookahead ended entry
92
+ // 1 in the middle of "J. Zhou", at the Z. Measured: 48 of 67 references parsed, and the missing
93
+ // 19 looked like ordinary gaps rather than a bug. Same family as the `\b`-over-Cyrillic defect
94
+ // already recorded in this repo: an escape that means one thing in Perl/Python and another here.
95
+ const re = /^(\d+)\.[ \t]+([\s\S]*?)(?=^\d+\.[ \t]|^#|$(?![\s\S]))/gm;
96
+ let m;
97
+ while ((m = re.exec(body))) {
98
+ const entry = m[2].replace(/\s*\n\s*/g, " ").trim();
99
+ const t = entry.match(/\*([^*]+)\*/);
100
+ if (!t) {
101
+ // No italic title and no author list — in this corpus these are SOFTWARE references
102
+ // (npm packages, repos). Nothing for DBLP to disagree with. They are surfaced as
103
+ // not-applicable rather than dropped: an entry that silently vanishes between the file
104
+ // and the report is indistinguishable from an entry that passed.
105
+ out.push({
106
+ type: "mdref",
107
+ key: `ref${m[1]}`,
108
+ unparsed: true,
109
+ author: "",
110
+ title: "",
111
+ booktitle: "",
112
+ journal: "",
113
+ });
114
+ continue;
115
+ }
116
+ const authorPart = entry
117
+ .slice(0, entry.indexOf("*"))
118
+ .trim()
119
+ .replace(/[.,;]\s*$/, "");
120
+ const rest = entry.slice(entry.indexOf(t[0]) + t[0].length);
121
+ out.push({
122
+ type: "mdref",
123
+ key: `ref${m[1]}`,
124
+ author: authorPart
125
+ .split(/,\s*|\s+and\s+/)
126
+ .filter(Boolean)
127
+ .join(" and "),
128
+ title: t[1].replace(/\.$/, "").trim(),
129
+ booktitle: rest.trim(),
130
+ journal: "",
131
+ });
132
+ }
133
+ return out;
134
+ }
135
+
136
+ function bibTextFrom(target) {
137
+ let file = target;
138
+ if (!existsSync(file)) die(`no such path: ${file}`);
139
+ if (!extname(file)) {
140
+ const names = readdirSync(file);
141
+ // 🔴 THE CANONICAL NAME FIRST, then the only candidate, and never "whichever
142
+ // turns up first" (review #189). It used to be `names.find(f => f.endsWith(".tex"))`,
143
+ // that is, the first in directory order. Measured: one of the papers in the corpus
144
+ // has FIVE drafts lying around (paper-CONSTRUCTIVE-…, paper-FOLDED-…, paper-SAFE-…),
145
+ // while PIPELINE-STATUS.md names `paper.tex` as the submitted source. The author
146
+ // check went off into a stale draft and printed a verdict about the WRONG
147
+ // bibliography — a gate that checks the wrong file is worse than a missing one,
148
+ // because it says "checked".
149
+ const pick = (ext) => {
150
+ const canon = names.find((f) => f === `paper${ext}`);
151
+ if (canon) return canon;
152
+ const all = names.filter((f) => f.endsWith(ext)).sort();
153
+ if (all.length === 1) return all[0];
154
+ if (all.length > 1) {
155
+ // We do not fail: a new check here would set up one more surface, and its
156
+ // home is decided by the ladder in the `CLAUDE.md` next to the papers, not by
157
+ // this script (the `frozen-checks` ratchet is what guards that). It is enough
158
+ // to SAY it out loud and to choose DETERMINISTICALLY — the header below prints
159
+ // which file was checked anyway, so the reader sees the choice.
160
+ console.error(
161
+ `⚠️ ${file} holds several ${ext} files and no canonical paper${ext}: ` +
162
+ `${all.join(", ")} — took ${all[0]}. If that is the wrong one, name the file explicitly.`,
163
+ );
164
+ return all[0];
165
+ }
166
+ return undefined;
167
+ };
168
+ const found =
169
+ pick(".bib") ||
170
+ pick(".tex") ||
171
+ names.find((f) => f === "paper.md" || f === "draft.md");
172
+ if (!found) die(`no .bib, .tex or paper.md in ${file}`);
173
+ file = join(file, found);
174
+ }
175
+ const text = readFileSync(file, "utf-8");
176
+ if (file.endsWith(".md")) return { text, file, markdown: true };
177
+ if (file.endsWith(".bib")) return { text, file };
178
+ // A .tex may carry the bibliography inline via filecontents — that is how our papers do it.
179
+ const m = text.match(
180
+ /\\begin\{filecontents\*?\}(?:\[[^\]]*\])?\{[^}]*\.bib\}\r?\n([\s\S]*?)\\end\{filecontents\*?\}/,
181
+ );
182
+ if (m) return { text: m[1], file };
183
+ const sibling = join(file, "..", "refs.bib");
184
+ if (existsSync(sibling))
185
+ return { text: readFileSync(sibling, "utf-8"), file: sibling };
186
+ die(
187
+ `no bibliography found in ${file} (no filecontents block, no refs.bib beside it)`,
188
+ );
189
+ }
190
+
191
+ /* ---------- a deliberately small bib reader ----------
192
+ * Only three fields are needed (type, key, author, title, booktitle/journal), and a full
193
+ * BibTeX grammar would be a second thing to maintain. Brace-depth counting is enough and
194
+ * is exercised by the colocated test. */
195
+ function parseBib(text) {
196
+ const out = [];
197
+ const re = /@(\w+)\s*\{\s*([^,\s]+)\s*,/g;
198
+ let m;
199
+ while ((m = re.exec(text))) {
200
+ const [, type, key] = m;
201
+ let i = re.lastIndex,
202
+ depth = 1;
203
+ while (i < text.length && depth > 0) {
204
+ if (text[i] === "{") depth++;
205
+ else if (text[i] === "}") depth--;
206
+ i++;
207
+ }
208
+ const body = text.slice(re.lastIndex, i - 1);
209
+ out.push({ type: type.toLowerCase(), key, ...fields(body) });
210
+ }
211
+ return out;
212
+ }
213
+
214
+ function fields(body) {
215
+ const get = (name) => {
216
+ const r = new RegExp(`(?:^|[,\\s])${name}\\s*=\\s*`, "i");
217
+ const at = body.search(r);
218
+ if (at === -1) return "";
219
+ let i = body.indexOf("=", at) + 1;
220
+ while (/\s/.test(body[i])) i++;
221
+ if (body[i] === "{") {
222
+ let depth = 1,
223
+ j = i + 1;
224
+ while (j < body.length && depth > 0) {
225
+ if (body[j] === "{") depth++;
226
+ else if (body[j] === "}") depth--;
227
+ j++;
228
+ }
229
+ return body.slice(i + 1, j - 1);
230
+ }
231
+ if (body[i] === '"') {
232
+ const j = body.indexOf('"', i + 1);
233
+ return body.slice(i + 1, j);
234
+ }
235
+ const j = body.indexOf(",", i);
236
+ return body.slice(i, j === -1 ? undefined : j);
237
+ };
238
+ return {
239
+ author: get("author"),
240
+ title: get("title"),
241
+ booktitle: get("booktitle"),
242
+ journal: get("journal"),
243
+ };
244
+ }
245
+
246
+ /* ---------- normalisation: surnames only, in order ---------- */
247
+
248
+ const DEACCENT = (s) =>
249
+ s
250
+ .normalize("NFD")
251
+ .replace(/[̀-ͯ]/g, "")
252
+ .replace(/[{}\\'`"^~]/g, "");
253
+
254
+ export function surnames(authorField) {
255
+ if (!authorField) return [];
256
+ return DEACCENT(authorField)
257
+ .split(/\s+and\s+/i)
258
+ .map((p) => p.trim())
259
+ .filter(Boolean)
260
+ .map((p) => {
261
+ // Reduce BOTH orders to the final token of the family name.
262
+ // "Berg, Jan van der" -> family part "Berg" -> "berg"
263
+ // "van der Berg, Jan" -> family part "van der Berg"-> "berg"
264
+ // "Jan van der Berg" -> last token -> "berg"
265
+ // 🔴 Taking the whole family part in the comma branch (the first version of this) made
266
+ // the two orders normalise DIFFERENTLY for any particle surname — "van der berg" vs
267
+ // "berg" — so every Dutch/German co-author would have read as a real disagreement
268
+ // against DBLP, which always writes "First Last". The colocated test caught it before
269
+ // this shipped; the assertion pinning it is "a multi-token surname keeps its last token,
270
+ // consistently in both orders".
271
+ const familyPart = p.includes(",") ? p.split(",")[0] : p;
272
+ const last = familyPart.trim().split(/\s+/).slice(-1)[0] || "";
273
+ return last
274
+ .trim()
275
+ .toLowerCase()
276
+ .replace(/[.\s-]+$/, "");
277
+ })
278
+ .filter((s) => s && s !== "others");
279
+ }
280
+
281
+ const truncated = (authorField) => /\band\s+others\b/i.test(authorField || "");
282
+
283
+ /** Is the entry claiming a PUBLISHED venue (as opposed to a preprint)? */
284
+ export function claimsPublished(e) {
285
+ const venue = `${e.booktitle} ${e.journal}`.trim();
286
+ if (!venue) return false;
287
+ return !/^\s*arxiv\b|arxiv preprint|\bcorr\b/i.test(venue);
288
+ }
289
+
290
+ /* ---------- DBLP ---------- */
291
+
292
+ async function dblpHits(title) {
293
+ const url = `${DBLP}/?q=${encodeURIComponent(title)}&format=json&h=6`;
294
+ // Per-request timeout: a check that hangs is indistinguishable from a check that is dead,
295
+ // and this one runs 25+ requests. Measured 2026-08-24: a single unbounded query stalled the
296
+ // whole run past two minutes while a healthy one answers in ~0.6 s.
297
+ const res = await fetch(url, {
298
+ headers: { "User-Agent": "bib-authors/1.0 (paper QA)" },
299
+ signal: AbortSignal.timeout(15_000),
300
+ });
301
+ if (res.status === 429)
302
+ throw Object.assign(new Error("DBLP 429"), { retryable: true });
303
+ if (!res.ok) throw new Error(`DBLP ${res.status}`);
304
+ const hits = (await res.json())?.result?.hits?.hit ?? [];
305
+ return hits.map((h) => {
306
+ const a = h.info?.authors?.author;
307
+ const list = a ? (Array.isArray(a) ? a : [a]) : [];
308
+ return {
309
+ venue: h.info?.venue ?? "",
310
+ year: h.info?.year ?? "",
311
+ type: h.info?.type ?? "",
312
+ title: h.info?.title ?? "",
313
+ // DBLP disambiguates people as "Mostafa Dehghani 0001" — the digits are not a name.
314
+ authors: list.map((p) => String(p.text).replace(/\s+\d{4}$/, "")),
315
+ };
316
+ });
317
+ }
318
+
319
+ const isPreprintRecord = (h) =>
320
+ /^corr$/i.test(h.venue) || /informal/i.test(h.type);
321
+ const looseTitle = (s) =>
322
+ DEACCENT(s)
323
+ .toLowerCase()
324
+ .replace(/[^a-z0-9]+/g, "");
325
+
326
+ /* ---------- the comparison ---------- */
327
+
328
+ export function compare(ourSurnames, theirSurnames) {
329
+ const missing = theirSurnames.filter((x) => !ourSurnames.includes(x));
330
+ const extra = ourSurnames.filter((x) => !theirSurnames.includes(x));
331
+ const orderDiffers =
332
+ missing.length === 0 &&
333
+ extra.length === 0 &&
334
+ ourSurnames.join("|") !== theirSurnames.join("|");
335
+ return { missing, extra, orderDiffers };
336
+ }
337
+
338
+ function die(msg) {
339
+ console.error(`bib-authors: ${msg}`);
340
+ process.exit(2);
341
+ }
342
+
343
+ async function main() {
344
+ const args = process.argv.slice(2).filter((a) => a !== "--json");
345
+ const asJson = process.argv.includes("--json");
346
+ if (!args[0])
347
+ die("usage: bib-authors.mjs <paper-dir|file.bib|file.tex> [--json]");
348
+
349
+ const { text, file, markdown } = bibTextFrom(args[0]);
350
+ const parsed = markdown ? parseMarkdownRefs(text) : parseBib(text);
351
+ const entries = parsed.filter((e) => e.title && e.author);
352
+ const findings = [];
353
+ const skipped = []; // legitimately not applicable
354
+ const unchecked = []; // we FAILED to check — must never be reported as a pass
355
+
356
+ for (const e of parsed.filter((x) => x.unparsed)) {
357
+ skipped.push({
358
+ key: e.key,
359
+ why: "no author/title — a reference to software or a dataset, DBLP does not apply",
360
+ });
361
+ }
362
+
363
+ for (const e of entries) {
364
+ if (!claimsPublished(e)) continue; // a preprint entry is allowed to carry preprint metadata
365
+ if (truncated(e.author)) {
366
+ skipped.push({
367
+ key: e.key,
368
+ why: "author list ends in `and others` — completeness not checkable",
369
+ });
370
+ continue;
371
+ }
372
+ // 🔴 A failed lookup is NOT a skip. Measured 2026-08-24: the first run reported
373
+ // "PASS: no author-list disagreement" while NINE of 27 entries had never been examined —
374
+ // five of them because DBLP answered 429. A verdict printed over unexamined data is the
375
+ // "counter that counts what it ignores" failure this repo already has written down.
376
+ // So: retry transient failures, then record them in a SEPARATE bucket that suppresses PASS.
377
+ let hits = null;
378
+ for (let attempt = 0; attempt < 3 && hits === null; attempt++) {
379
+ try {
380
+ hits = await dblpHits(e.title.replace(/[{}]/g, ""));
381
+ } catch (err) {
382
+ const last = attempt === 2;
383
+ if (last)
384
+ unchecked.push({
385
+ key: e.key,
386
+ why: `DBLP lookup failed: ${err.message}`,
387
+ });
388
+ else
389
+ await new Promise((r) => {
390
+ setTimeout(r, 1500 * (attempt + 1));
391
+ });
392
+ }
393
+ }
394
+ if (hits === null) continue;
395
+ const want = looseTitle(e.title);
396
+ const same = hits.filter((h) => looseTitle(h.title) === want);
397
+ // We claim the published version, so compare against the published record, never CoRR.
398
+ const rec = same.find((h) => !isPreprintRecord(h));
399
+ if (!rec) {
400
+ skipped.push({
401
+ key: e.key,
402
+ why: same.length
403
+ ? "only a preprint record on DBLP"
404
+ : "no DBLP title match",
405
+ });
406
+ continue;
407
+ }
408
+ const ours = surnames(e.author);
409
+ const theirs = surnames(rec.authors.join(" and "));
410
+ const d = compare(ours, theirs);
411
+ if (d.missing.length || d.extra.length || d.orderDiffers) {
412
+ findings.push({
413
+ key: e.key,
414
+ venue: `${rec.venue} ${rec.year}`.trim(),
415
+ ours,
416
+ theirs,
417
+ ...d,
418
+ });
419
+ }
420
+ // A braced body rather than a concise arrow — see `no-promise-executor-return` (2026-08-28).
421
+ await new Promise((r) => {
422
+ setTimeout(r, 900);
423
+ }); // DBLP asks for gentle clients; 350 ms drew 429s
424
+ }
425
+
426
+ if (asJson) {
427
+ console.log(
428
+ JSON.stringify(
429
+ { file, entries: entries.length, findings, skipped, unchecked },
430
+ null,
431
+ 2,
432
+ ),
433
+ );
434
+ } else {
435
+ console.log(`== bib-authors: ${file} ==`);
436
+ for (const f of findings) {
437
+ console.log(`\n 🔴 ${f.key} (DBLP: ${f.venue})`);
438
+ if (f.missing.length)
439
+ console.log(` MISSING from ours : ${f.missing.join(", ")}`);
440
+ if (f.extra.length)
441
+ console.log(` EXTRA in ours : ${f.extra.join(", ")}`);
442
+ if (f.orderDiffers) {
443
+ console.log(` ORDER differs`);
444
+ console.log(` ours : ${f.ours.join(" > ")}`);
445
+ console.log(` DBLP : ${f.theirs.join(" > ")}`);
446
+ }
447
+ }
448
+ for (const s of skipped) console.log(` · n/a ${s.key} — ${s.why}`);
449
+ for (const u of unchecked)
450
+ console.log(` ⚠️ NOT CHECKED ${u.key} — ${u.why}`);
451
+ console.log(
452
+ `\n-- ${entries.length} entries · ${findings.length} difference(s) · ` +
453
+ `${skipped.length} not applicable · ${unchecked.length} NOT CHECKED`,
454
+ );
455
+ console.log(
456
+ findings.length
457
+ ? "FAIL: our author list disagrees with the version we claim to cite."
458
+ : unchecked.length
459
+ ? `PARTIAL: no disagreement among the entries reached, but ${unchecked.length} could not be checked — this is NOT a pass.`
460
+ : "PASS: no author-list disagreement.",
461
+ );
462
+ }
463
+ // 🔴 THE UNCHECKED IS NOT A PASS EITHER (review #189). The exit code depended only
464
+ // on findings, so a run where DBLP did not answer EVEN ONCE (a timeout, a series of
465
+ // 429s) exited 0 — and the caller read an author audit THAT NEVER RAN as passed.
466
+ // The text next to it already said "this is NOT a pass", but a human reads the text
467
+ // while CI reads the exit code. A divergence between what the script SAYS and what it
468
+ // REPORTS to its caller is the same class as "the gate checked the wrong file".
469
+ // Different codes so the caller can tell them apart: 1 — there are disagreements,
470
+ // 2 — the audit is incomplete.
471
+ process.exit(findings.length ? 1 : unchecked.length ? 2 : 0);
472
+ }
473
+
474
+ // 🔴 `isMain`, NOT `import.meta.url === `file://${process.argv[1]}``. Node resolves the entry point
475
+ // to its REAL path for `import.meta.url` but leaves `process.argv[1]` as typed, so through a symlink
476
+ // the two are not equal and the CLI silently does not execute — the process exits 0 having done
477
+ // nothing. The consumer reaches these scripts precisely through a symlink. Observed 14.09 on run
478
+ // 34784079821: `extract-pdf-facts.mjs --strict` returned RC=0 and created no facts file.
479
+ if (isMain(import.meta.url)) await main();
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Unit tests for bib-authors.mjs — the pure half, no network.
4
+ *
5
+ * The network half (DBLP) is deliberately NOT mocked: a mock of DBLP would assert that our
6
+ * mock behaves, which is the "checker that never failed" trap. What is worth pinning here is
7
+ * the NORMALISATION, because that is the part whose failure is silent and expensive:
8
+ *
9
+ * too loose -> real differences vanish (a dropped author reads as a formatting quirk)
10
+ * too strict -> 13 findings of which 11 are "Last, First" vs "First Last", the checker is
11
+ * read once and never again. Measured on our own bibliography 2026-08-24.
12
+ *
13
+ * Run: npx vitest run skills/verify-citations/scripts/bib-authors.test.mjs
14
+ */
15
+ import assert from "node:assert/strict";
16
+ import { test } from "vitest";
17
+ import {
18
+ surnames,
19
+ compare,
20
+ claimsPublished,
21
+ parseMarkdownRefs,
22
+ } from "./bib-authors.mjs";
23
+
24
+ const t = test;
25
+
26
+ console.log("bib-authors — normalisation");
27
+
28
+ t("the two BibTeX name orders normalise to the SAME sequence", () => {
29
+ const a = surnames("Schick, Timo and Dwivedi-Yu, Jane and Scialom, Thomas");
30
+ const b = surnames("Timo Schick and Jane Dwivedi-Yu and Thomas Scialom");
31
+ assert.deepEqual(a, b);
32
+ assert.deepEqual(a, ["schick", "dwivedi-yu", "scialom"]);
33
+ });
34
+
35
+ t("accents and TeX braces do not create a difference", () => {
36
+ assert.deepEqual(surnames("Dess{\\`i}, Roberto"), surnames("Roberto Dessì"));
37
+ });
38
+
39
+ t("`and others` is dropped, not treated as a surname", () => {
40
+ assert.deepEqual(surnames("Zheng, Lianmin and Yin, Liangsheng and others"), [
41
+ "zheng",
42
+ "yin",
43
+ ]);
44
+ });
45
+
46
+ t(
47
+ "a multi-token surname keeps its last token, consistently in both orders",
48
+ () => {
49
+ assert.deepEqual(
50
+ surnames("van der Berg, Jan"),
51
+ surnames("Jan van der Berg"),
52
+ );
53
+ },
54
+ );
55
+
56
+ console.log("bib-authors — comparison");
57
+
58
+ t("identical sequences produce nothing", () => {
59
+ const d = compare(["raji", "denton"], ["raji", "denton"]);
60
+ assert.deepEqual(d, { missing: [], extra: [], orderDiffers: false });
61
+ });
62
+
63
+ t("REGRESSION — Toolformer: a dropped author is reported as MISSING", () => {
64
+ // Ours (arXiv preprint) had 8; NeurIPS 2023 has 9. Eric Hambro was absent from a citation
65
+ // in an archival publication until 2026-08-24. This assert is that exact paper.
66
+ const ours = surnames(
67
+ "Schick, Timo and Dwivedi-Yu, Jane and Dess{\\`i}, Roberto and Raileanu, Roberta and " +
68
+ "Lomeli, Maria and Zettlemoyer, Luke and Cancedda, Nicola and Scialom, Thomas",
69
+ );
70
+ const neurips = surnames(
71
+ "Timo Schick and Jane Dwivedi-Yu and Roberto Dessì and Roberta Raileanu and Maria Lomeli and " +
72
+ "Eric Hambro and Luke Zettlemoyer and Nicola Cancedda and Thomas Scialom",
73
+ );
74
+ const d = compare(ours, neurips);
75
+ assert.deepEqual(d.missing, ["hambro"]);
76
+ assert.deepEqual(d.extra, []);
77
+ });
78
+
79
+ t(
80
+ "REGRESSION — Efficiency Misnomer: same people, preprint ORDER, reported as order",
81
+ () => {
82
+ const ours = surnames(
83
+ "Dehghani, Mostafa and Arnab, Anurag and Beyer, Lucas and Vaswani, Ashish and Tay, Yi",
84
+ );
85
+ const iclr = surnames(
86
+ "Mostafa Dehghani and Yi Tay and Anurag Arnab and Lucas Beyer and Ashish Vaswani",
87
+ );
88
+ const d = compare(ours, iclr);
89
+ assert.equal(
90
+ d.orderDiffers,
91
+ true,
92
+ "an order-only difference must still be a finding",
93
+ );
94
+ assert.deepEqual(d.missing, []);
95
+ assert.deepEqual(d.extra, []);
96
+ },
97
+ );
98
+
99
+ t(
100
+ "REGRESSION — Raji: order-only, found by the script itself after the other two were fixed",
101
+ () => {
102
+ const ours = surnames(
103
+ "Raji, Inioluwa Deborah and Bender, Emily M. and Paullada, Amandalynne and Denton, Emily and Hanna, Alex",
104
+ );
105
+ const neurips = surnames(
106
+ "Inioluwa Deborah Raji and Emily Denton and Emily M. Bender and Alex Hanna and Amandalynne Paullada",
107
+ );
108
+ assert.equal(compare(ours, neurips).orderDiffers, true);
109
+ },
110
+ );
111
+
112
+ t("an author we invented would be reported as EXTRA", () => {
113
+ assert.deepEqual(compare(["raji", "ghost"], ["raji"]).extra, ["ghost"]);
114
+ });
115
+
116
+ console.log("bib-authors — scope");
117
+
118
+ t("a preprint entry is NOT held to a published author list", () => {
119
+ assert.equal(
120
+ claimsPublished({
121
+ booktitle: "",
122
+ journal: "arXiv preprint arXiv:2604.22750",
123
+ }),
124
+ false,
125
+ );
126
+ assert.equal(claimsPublished({ booktitle: "", journal: "CoRR" }), false);
127
+ });
128
+
129
+ t("a proceedings entry IS in scope", () => {
130
+ assert.equal(
131
+ claimsPublished({
132
+ booktitle: "NeurIPS Datasets and Benchmarks Track",
133
+ journal: "",
134
+ }),
135
+ true,
136
+ );
137
+ });
138
+
139
+ console.log("bib-authors — where the bibliography begins");
140
+
141
+ // Both halves. "Silent on a fence" without "finds the real one" is indistinguishable from a broken parse.
142
+ t("a real ## References is parsed", () => {
143
+ const md = [
144
+ "# Paper",
145
+ "text",
146
+ "",
147
+ "## References",
148
+ "",
149
+ "1. A. Author. *A title*. In NeurIPS, 2023.",
150
+ ].join("\n");
151
+ const refs = parseMarkdownRefs(md);
152
+ assert.equal(refs.length, 1);
153
+ assert.match(refs[0].title, /A title/);
154
+ });
155
+
156
+ t("a ## References inside a ``` fence does NOT open the bibliography", () => {
157
+ const md = [
158
+ "# Paper",
159
+ "",
160
+ "The section is written like this:",
161
+ "",
162
+ "```markdown",
163
+ "## References",
164
+ "",
165
+ "1. Z. Ghost. *Not a reference, but a markup example*. In Nowhere, 2020.",
166
+ "```",
167
+ "",
168
+ "The end.",
169
+ ].join("\n");
170
+ assert.deepEqual(
171
+ parseMarkdownRefs(md),
172
+ [],
173
+ "quoted markup is not a bibliography — the ^#+ regex counted it as a heading",
174
+ );
175
+ });