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,656 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * extract-ref-facts.mjs — parse a paper's bibliography, ask the registries and write FACTS to JSON.
4
+ * It judges nothing.
5
+ *
6
+ * Usage: node extract-ref-facts.mjs <paper dir|paper.md|refs.bib> [--offline] [--refresh]
7
+ * [--cache=PATH] [--out=PATH] [--quiet]
8
+ * Exit: 0 — facts written · 1 — nothing to write (no source, no entries).
9
+ *
10
+ * WHY THE SPLIT. Before 2026-08-26 measurement and judgement lived in one script
11
+ * (`verify-refs.mjs`, 20 emit sites, 8 kinds of findings) — that is, on the fifth rung of the
12
+ * ladder in `the consumer's papers CLAUDE.md`. The judgement moved into ESLint rules
13
+ * (`eslint-rules/ref-facts.mjs`): a registry of rules, severity from config, `eslint-disable` with a
14
+ * reason, `file:line:col` positions. What stayed here is the plumbing: parsing markup, parsing
15
+ * `.bib`, the network, parsing the registries' responses.
16
+ *
17
+ * The precedent in this same knowledge base is `render-paper/extract-pdf-facts.mjs` +
18
+ * `eslint-rules/pdf-facts.mjs`. The boundary is exactly the same: the script goes out into the
19
+ * world, the rule passes the judgement.
20
+ *
21
+ * 🔴 WHY THE RULE CANNOT LIVE OVER THE PAPER ITSELF. Of the three papers in the knowledge base one
22
+ * has a `paper.md` (`<paper-a>`); in `<paper-b>` and `<paper-c>` the bibliography is `refs.bib`, and
23
+ * no ESLint language plugin parses `.bib`. The common input that can do both is the facts JSON.
24
+ * The price is known and written down: a finding is addressed into the facts, not into a line of
25
+ * `paper.md`; that is why every entry in the facts carries the `line` of its source, and the rule
26
+ * prints it in the message.
27
+ *
28
+ * 🔴 WHAT THIS MOVE FIXED (both legs were DEAD, measured 2026-08-26):
29
+ *
30
+ * 1. `.bib` WAS NEVER OPENED. `resolveSource` walked `paper.md` → `draft.md` →
31
+ * `build/custom.bib`; `refs.bib` was not on the list, although the error text promised
32
+ * "or .bib". The run: `verify-refs.mjs <papers-root>/<paper-b>` →
33
+ * "no paper.md, draft.md or .bib … nothing to check".
34
+ * 2. THE `.bib` PARSER DID NOT TAKE THE REAL FILES. The regex `/@\w+\{([^,]+),([\s\S]*?)\n\}/g`
35
+ * required a `}` on a line of its own, while both of our `refs.bib` close an entry on the line
36
+ * of the last field (`note={arXiv:2310.06770}}`). A run straight at the file: "parsed 0
37
+ * references out of …/<paper-b>/refs.bib". That is, 40 lines of `parseBib` never ran once.
38
+ * 3. AND A THIRD, hidden behind the first two: in `.bib` an author is written "Surname, First
39
+ * name", while the comparison took the LAST token of the string — for `Jimenez, Carlos E.` that
40
+ * is `e`. Even if the regex had worked, checking authors against `.bib` would have compared
41
+ * initials with surnames.
42
+ *
43
+ * None of this is cured by being careful, but by having `.bib` parsed by a REAL PARSER
44
+ * (`@retorquere/bibtex-parser`), which returns `lastName` as a separate field — class #3 becomes
45
+ * inexpressible. Occupancy was checked by a run over both real files BEFORE the choice:
46
+ * `bibtex-parse`, `@retorquere/bibtex-parser`, `citation-js` and `astrocite-bibtex` — all four give
47
+ * the correct 27 and 51 entries against zero from our regex. retorquere was taken because it is the
48
+ * only one that returns a STRUCTURED name; with any other the surname would have to be cut out by
49
+ * hand, that is, we would be writing ourselves the very half that was broken.
50
+ *
51
+ * 🔴 THE CACHE IS EVIDENCE, NOT A SECOND DECLARATION. `<paper>/repro/refs-cache.json` keeps the RAW
52
+ * BODIES of the registries' responses and is committed (for `<paper-a>` it has been in git since
53
+ * 16.08). It cannot be made up — only refreshed from the registry. The facts in `_build/` are
54
+ * derived from it and from the bibliography, which is why `_build/` is in `.gitignore` while the
55
+ * facts carry `source_sha256`: the rule `refs/fresh` compares it with the file on disk, and stale
56
+ * facts become a finding rather than silence. Exactly the failure that made `api:check` in vigiles
57
+ * print "no drift" while reading a `dist/` from the previous build.
58
+ *
59
+ * 🔴 PARSING THE RESPONSES LIVES HERE, NOT IN THE RULE, AND THAT IS LOAD-BEARING. The cache keeps
60
+ * raw bodies precisely so that the CrossRef-JSON and arXiv-Atom readers are exercised on real
61
+ * responses: every fabricated title this knowledge base has ever shipped was found by reading the
62
+ * registry's response, not by a comparison. If the cache kept parsed entries, those readers would
63
+ * stay uncovered while looking covered.
64
+ */
65
+ import {
66
+ readFileSync,
67
+ writeFileSync,
68
+ existsSync,
69
+ mkdirSync,
70
+ statSync,
71
+ } from "node:fs";
72
+ import { createHash } from "node:crypto";
73
+ import { join, resolve, dirname, basename } from "node:path";
74
+ import {
75
+ headings as mdHeadings,
76
+ requireMarkdown,
77
+ } from "../../../lib/markdown.mjs";
78
+ import { isMain } from "./consumer.mjs";
79
+
80
+ // Markup is parsed with a parser (`CLAUDE.md`, 2026-08-11). We fail rather than degrade: without
81
+ // the parser the reference list would not be found at all, and the facts would come out empty —
82
+ // that is, a clean verdict about a paper that in fact has forty references.
83
+ requireMarkdown();
84
+
85
+ const ROOT = process.env.CLAUDE_PROJECT_DIR || process.cwd();
86
+ const CROSSREF = "https://api.crossref.org/works/";
87
+ const ARXIV = "https://export.arxiv.org/api/query?id_list=";
88
+ // CrossRef asks for a contact in the User-Agent and gives the polite pool in return. No account
89
+ // and no key — it is a courtesy, the anonymous pool is rate-limited.
90
+ const UA =
91
+ "extract-ref-facts/1.0 (https://github.com/; paper-pipeline citation facts)";
92
+
93
+ export const SCHEMA = 1;
94
+
95
+ // ── parsing: the reference list in markdown ──────────────────────────────────
96
+ //
97
+ // The same shape that `repro/md2submission.py` reads, deliberately: that script turns the list into
98
+ // the .bib actually uploaded to the venue, so whatever it counts as an entry is an entry.
99
+ // `^\d{1,2}. ` opens an entry, an indent continues it, a blank line closes it.
100
+
101
+ const REF_OPEN = /^(\d{1,2})\.\s+(.*)$/;
102
+ /** Working notes that never reach the PDF; md2submission strips them, so we do too. */
103
+ const BRACKET_NOTE = /\[(VERIFY|ANONYMIZ)[^\]]*\]/g;
104
+
105
+ export function parseMarkdownRefs(text) {
106
+ // The section is cut by the parser. The former `text.split(/^## References\s*$/m)[1]` opened the
107
+ // reference list on a `## References` quoted inside a ```-block — and in this repo papers quote
108
+ // their own markup in chunks.
109
+ const refsHs = mdHeadings(text).filter(
110
+ (h) => h.depth === 2 && /^References\s*$/u.test(h.text),
111
+ );
112
+ if (refsHs.length === 0) return [];
113
+ const nl = text.indexOf("\n", refsHs[0].offset);
114
+ const body = text.slice(
115
+ nl === -1 ? text.length : nl,
116
+ refsHs[1] ? refsHs[1].offset : text.length,
117
+ );
118
+ // The line where the body starts in the source file — so that an entry has an ADDRESS in
119
+ // `paper.md` and not just a number in a list. The rule prints it in the message: the facts file
120
+ // lives in `_build/`, and without the source line a finding would have to be hunted for by eye.
121
+ const bodyLine = text
122
+ .slice(0, nl === -1 ? text.length : nl)
123
+ .split("\n").length;
124
+ // A heading closes the list — appendices come AFTER the bibliography, and without this their
125
+ // numbered prose would be parsed as references. The line numbers come from the parser:
126
+ // `/^#{1,6}\s/` also counted a hash inside a ```-block as a heading.
127
+ const headingLines = new Set(mdHeadings(body).map((h) => h.line));
128
+ const entries = [];
129
+ let cur = null;
130
+ let buf = [];
131
+ let curLine = 0;
132
+ const flush = () => {
133
+ if (cur !== null)
134
+ entries.push({
135
+ n: cur,
136
+ line: curLine,
137
+ raw: buf.join(" ").replace(BRACKET_NOTE, "").trim(),
138
+ });
139
+ cur = null;
140
+ buf = [];
141
+ };
142
+ const lns = body.split("\n");
143
+ for (let i = 0; i < lns.length; i++) {
144
+ const ln = lns[i];
145
+ if (headingLines.has(i)) break;
146
+ const m = REF_OPEN.exec(ln);
147
+ if (m) {
148
+ flush();
149
+ cur = Number(m[1]);
150
+ curLine = bodyLine + i;
151
+ buf = [m[2].trim()];
152
+ } else if (cur !== null && ln.trim()) buf.push(ln.trim());
153
+ else if (cur !== null) flush();
154
+ }
155
+ flush();
156
+ return entries.map(splitEntry).filter(Boolean);
157
+ }
158
+
159
+ /** `A, B, C. *Title.* rest` → authors / title / remainder — the shape md2submission produces. */
160
+ function splitEntry(e) {
161
+ const base = { n: e.n, line: e.line, key: null, raw: e.raw };
162
+ const mt = /\*(.+?)\*/.exec(e.raw);
163
+ if (!mt)
164
+ return {
165
+ ...base,
166
+ authors: [],
167
+ truncated: false,
168
+ title: null,
169
+ year: yearIn(e.raw),
170
+ venue_text: e.raw,
171
+ };
172
+ const title = mt[1].trim().replace(/\.$/, "");
173
+ const rawAuthors = splitAuthors(e.raw.slice(0, mt.index));
174
+ const note = e.raw
175
+ .slice(mt.index + mt[0].length)
176
+ .trim()
177
+ .replace(/^,\s*/, "");
178
+ const { authors, truncated } = dropEtAl(rawAuthors);
179
+ return {
180
+ ...base,
181
+ authors,
182
+ truncated,
183
+ title,
184
+ year: yearIn(note) ?? yearIn(e.raw),
185
+ venue_text: note,
186
+ };
187
+ }
188
+
189
+ function splitAuthors(a) {
190
+ const s = a
191
+ .trim()
192
+ .replace(/\.\s*$/, "")
193
+ .trim();
194
+ if (!s) return [];
195
+ return s
196
+ .split(",")
197
+ .map((p) => p.trim())
198
+ .filter(Boolean);
199
+ }
200
+
201
+ /**
202
+ * `Z. Xiang et al.` — the entry deliberately gives a PREFIX of the author list. That is a fact about
203
+ * the entry, not a judgement, so the truncation is removed here and the rule receives an honest
204
+ * prefix plus a flag.
205
+ */
206
+ const ET_AL = /\bet\s+al\.?$/i;
207
+ function dropEtAl(authors) {
208
+ if (!authors.length || !ET_AL.test(authors[authors.length - 1].trim()))
209
+ return { authors, truncated: false };
210
+ const out = authors.slice();
211
+ out[out.length - 1] = out[out.length - 1].replace(ET_AL, "").trim();
212
+ return { authors: out.filter(Boolean), truncated: true };
213
+ }
214
+
215
+ const yearIn = (s) => (/\b(19|20)\d{2}\b/.exec(s ?? "") ?? [null])[0];
216
+
217
+ // ── parsing: .bib WITH A REAL PARSER ─────────────────────────────────────────
218
+
219
+ /**
220
+ * The name order is normalised to "First name Surname" — the same shape in which authors are written
221
+ * in markdown. Then the rule has ONE `surname()` function (the last alphabetic token) for both
222
+ * sources instead of two branches. Normalising the format is the parser's job; comparing is the
223
+ * rule's job.
224
+ *
225
+ * 🔴 TWO SHAPES, AND BOTH ARE LISTED EXPLICITLY. The parser returns an institution
226
+ * (`author={{Adversa AI}}` — the double brace means "one name as a whole, do not split") as
227
+ * `{name}`, WITHOUT `lastName`: an organisation has no surname, and that is correct. The first
228
+ * revision of this function did not know the `name` shape, so all four fields were undefined, the
229
+ * string came out empty, and the caller dropped it with its own `.filter(Boolean)` — the author
230
+ * disappeared silently.
231
+ *
232
+ * Measured 2026-09-17 on the real aisec-2026: ELEVEN entries out of fifty-one arrived with
233
+ * `authors = []`, that is, the check over them found nothing and looked like it had passed. Exactly
234
+ * the failure toward silence against which a real parser was taken instead of a regex.
235
+ *
236
+ * ⚠️ An enumeration, not "the first non-empty field": with a list the next shape reads as a missing
237
+ * line, whereas with a `??` chain over arbitrary fields it would dissolve again.
238
+ */
239
+ const joinName = (a) =>
240
+ a.name ??
241
+ [a.firstName, a.prefix, a.lastName, a.suffix]
242
+ .filter(Boolean)
243
+ .join(" ")
244
+ .trim();
245
+
246
+ /**
247
+ * 🔴 THE .bib PARSER IS AN OPTIONAL DEPENDENCY, AND THE FAILURE MUST BE LOUD AND CARRY THE CURE.
248
+ * It weighs 15 MB out of the consumer's 56 MB install (the package itself 9 MB plus the English
249
+ * model `wink-eng-lite-web-model` 4 MB and `unicode2latex` 2 MB) — 27% of the weight for a single
250
+ * call that only whoever extracts bibliography facts needs. The import here is dynamic anyway, so
251
+ * the laziness was already there; the manifest merely stopped lying about it being required.
252
+ *
253
+ * ⚠️ A silent skip here would be the worst of the options: a missing checker and a passing one look
254
+ * the same, and "the bibliography was not checked" reads as "the bibliography is fine". That is why
255
+ * the message names the command, not the fact.
256
+ */
257
+ export async function parseBib(text) {
258
+ let parse;
259
+ try {
260
+ ({ parse } = await import("@retorquere/bibtex-parser"));
261
+ } catch (e) {
262
+ if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e;
263
+ throw new Error(
264
+ "parsing .bib requires @retorquere/bibtex-parser — it is declared OPTIONAL because it " +
265
+ "weighs 15 MB and is needed only for bibliography facts.\n" +
266
+ " Install: npm i -D @retorquere/bibtex-parser\n" +
267
+ " Why not our own regex: measured 26.08 — the regex gave 0 entries on both real " +
268
+ "files, four libraries gave the correct 27 and 51.",
269
+ );
270
+ }
271
+ // `sentenceCase: false` — the title is needed as written. Our normalisation folds the case
272
+ // anyway, but a fact has to be a fact: a paraphrased title cannot be shown to a human.
273
+ const res = parse(text, { sentenceCase: false, verbatimFields: [] });
274
+ // The entry's line in the file — found by its key. The parser gives no positions, and a finding
275
+ // needs an address; a key in `.bib` is unique by definition of the format, so the search is
276
+ // unambiguous.
277
+ const lines = text.split("\n");
278
+ const lineOfKey = (key) => {
279
+ const i = lines.findIndex((l) => l.includes(`{${key},`));
280
+ return i === -1 ? 0 : i + 1;
281
+ };
282
+ return res.entries.map((e, i) => {
283
+ const f = e.fields ?? {};
284
+ const names = Array.isArray(f.author) ? f.author : [];
285
+ // `and others` is BibTeX's `et al.`. The parser returns it as an author with no first name.
286
+ const isOthers = (a) => !a.firstName && /^others$/i.test(a.lastName ?? "");
287
+ const truncated = names.some(isOthers);
288
+ const authors = names
289
+ .filter((a) => !isOthers(a))
290
+ .map(joinName)
291
+ .filter(Boolean);
292
+ const str = (v) =>
293
+ Array.isArray(v)
294
+ ? v.join(" ")
295
+ : typeof v === "string"
296
+ ? v
297
+ : v == null
298
+ ? ""
299
+ : String(v);
300
+ const venueText = [f.booktitle, f.journal, f.note, f.howpublished]
301
+ .map(str)
302
+ .filter(Boolean)
303
+ .join(" ");
304
+ return {
305
+ n: i + 1,
306
+ line: lineOfKey(e.key),
307
+ key: e.key,
308
+ // `raw` is where identifiers are looked for: they hide in `note`, in `journal`
309
+ // (`arXiv preprint arXiv:2107.03374`), and in a separate `doi` field.
310
+ raw: [
311
+ str(f.author && authors.join(" and ")),
312
+ str(f.title),
313
+ venueText,
314
+ str(f.doi),
315
+ str(f.url),
316
+ str(f.year),
317
+ ]
318
+ .filter(Boolean)
319
+ .join(" "),
320
+ authors,
321
+ truncated,
322
+ title: str(f.title).trim() || null,
323
+ year: str(f.year).trim() || null,
324
+ venue_text: [venueText, str(f.doi)].filter(Boolean).join(" "),
325
+ // In BibTeX a DOI is declared as a field rather than hidden in prose — take it directly.
326
+ doi_field: str(f.doi).trim() || null,
327
+ };
328
+ });
329
+ }
330
+
331
+ // ── identifiers ──────────────────────────────────────────────────────────────
332
+
333
+ const ARXIV_RE = /arxiv[:\s]\s*(\d{4}\.\d{4,5})(v\d+)?/i;
334
+ const DOI_RE = /(?:doi[:\s]\s*|doi\.org\/)(10\.\d{4,9}\/[^\s,;)}]+)/i;
335
+ const ARXIV_RE_G = new RegExp(ARXIV_RE.source, "gi");
336
+ const DOI_RE_G = new RegExp(DOI_RE.source, "gi");
337
+
338
+ /**
339
+ * ALL identifiers of an entry, not the first one.
340
+ *
341
+ * 🔴 Entry [16] of the paper this was written against is one numbered entry carrying TWO works
342
+ * (TOGA and its later re-analysis), each with its own arXiv id. A parser that stops at the first
343
+ * checks half the entry and stays silent about the second — exactly the silence all of this exists
344
+ * to remove. There is nothing to compare the extras against (the entry has one title and one author
345
+ * list, and they are about the first work), so they are RESOLVED while the rule says it did not
346
+ * compare them.
347
+ */
348
+ export function extractIds(entry) {
349
+ const hay = `${entry.venue_text ?? ""} ${entry.raw ?? ""}`;
350
+ const uniq = (xs) => [...new Set(xs)];
351
+ const doi = uniq([
352
+ ...(entry.doi_field ? [entry.doi_field] : []),
353
+ // A trailing period belongs to the sentence, not to the DOI. Suffixes legitimately contain
354
+ // periods (10.18653/v1/2020.acl-main.168), so only the LAST one is stripped.
355
+ ...[...hay.matchAll(DOI_RE_G)].map((m) => m[1].replace(/[.,]$/, "")),
356
+ ]);
357
+ return { arxiv: uniq([...hay.matchAll(ARXIV_RE_G)].map((m) => m[1])), doi };
358
+ }
359
+
360
+ /**
361
+ * 🔴 `10.48550/arXiv.NNNN.NNNNN` IS A DOI REGISTERED WITH DATACITE, NOT WITH CROSSREF.
362
+ * Measured 2026-08-26: `api.crossref.org/works/10.48550%2FarXiv.2107.03374` → **HTTP 404**,
363
+ * "Resource not found", while the DOI is perfectly real and sits in `<paper-b>/refs.bib`.
364
+ * Asking CrossRef about it would have produced `unresolvable-id` on a CORRECT entry — that is, an
365
+ * `error`-level rule failing on valid input, which gets switched off the same day.
366
+ * Such a DOI resolves at arXiv by its own number.
367
+ */
368
+ export const ARXIV_DOI = /^10\.48550\/arxiv\.(\d{4}\.\d{4,5})(v\d+)?$/i;
369
+
370
+ /** The record the title/authors/year are judged against: the DOI, if there is one. */
371
+ export const primaryOf = (ids) =>
372
+ ids.doi.length
373
+ ? `doi:${ids.doi[0]}`
374
+ : ids.arxiv.length
375
+ ? `arxiv:${ids.arxiv[0]}`
376
+ : null;
377
+ export const allKeys = (ids) => [
378
+ ...ids.doi.map((d) => `doi:${d}`),
379
+ ...ids.arxiv.map((a) => `arxiv:${a}`),
380
+ ];
381
+
382
+ /** Where to go for a key and with which id. Decided BEFORE the network — parsing depends on it. */
383
+ export function routeOf(key) {
384
+ const kind = key.slice(0, key.indexOf(":"));
385
+ const id = key.slice(key.indexOf(":") + 1);
386
+ if (kind === "arxiv")
387
+ return { registry: "arxiv", url: ARXIV + encodeURIComponent(id) };
388
+ const m = ARXIV_DOI.exec(id);
389
+ if (m) return { registry: "arxiv", url: ARXIV + encodeURIComponent(m[1]) };
390
+ return { registry: "crossref", url: CROSSREF + encodeURIComponent(id) };
391
+ }
392
+
393
+ // ── registry readers ─────────────────────────────────────────────────────────
394
+
395
+ export function readCrossref(body) {
396
+ const j = JSON.parse(body);
397
+ const m = j.message;
398
+ if (!m) return null;
399
+ const dp =
400
+ m.issued?.["date-parts"]?.[0]?.[0] ??
401
+ m["published-print"]?.["date-parts"]?.[0]?.[0] ??
402
+ m["published-online"]?.["date-parts"]?.[0]?.[0] ??
403
+ null;
404
+ // 🔴 CrossRef puts the subtitle after the colon into a SEPARATE field: for TOSEM's
405
+ // "Variability-Aware Static Analysis at Scale: An Empirical Study" it returns
406
+ // title=["Variability-Aware Static Analysis at Scale"], subtitle=["An Empirical Study"].
407
+ // Comparing against `title[0]` alone gave a correct entry 0.69 and declared the title made up.
408
+ // A checker that fires on correct entries gets muted — and then it is not a checker.
409
+ const parts = [
410
+ ...(Array.isArray(m.title) ? m.title : [m.title]),
411
+ ...(Array.isArray(m.subtitle)
412
+ ? m.subtitle
413
+ : m.subtitle
414
+ ? [m.subtitle]
415
+ : []),
416
+ ];
417
+ return {
418
+ title: parts.filter(Boolean).join(": "),
419
+ authors: (m.author ?? []).map((a) => a.family ?? a.name ?? ""),
420
+ year: dp ? String(dp) : null,
421
+ venue: Array.isArray(m["container-title"])
422
+ ? m["container-title"][0]
423
+ : m["container-title"],
424
+ journalRef: null,
425
+ doi: null,
426
+ error: false,
427
+ };
428
+ }
429
+
430
+ /** arXiv answers in Atom. One `<entry>` per id; zero entries means the id does not resolve. */
431
+ export function readArxiv(body) {
432
+ const entry = /<entry>([\s\S]*?)<\/entry>/.exec(body);
433
+ if (!entry) return null;
434
+ const e = entry[1];
435
+ const tag = (name) => {
436
+ const m = new RegExp(`<${name}[^>]*>([\\s\\S]*?)</${name}>`).exec(e);
437
+ return m ? unescapeXml(m[1]).replace(/\s+/g, " ").trim() : null;
438
+ };
439
+ const authors = [
440
+ ...e.matchAll(
441
+ /<author>[\s\S]*?<name>([\s\S]*?)<\/name>[\s\S]*?<\/author>/g,
442
+ ),
443
+ ].map((m) => unescapeXml(m[1]).replace(/\s+/g, " ").trim());
444
+ const published = tag("published");
445
+ return {
446
+ title: tag("title"),
447
+ authors,
448
+ year: published ? published.slice(0, 4) : null,
449
+ venue: null,
450
+ // arXiv's own "this came out somewhere" fields. Either of them means a published version
451
+ // exists.
452
+ journalRef: tag("arxiv:journal_ref"),
453
+ doi: tag("arxiv:doi"),
454
+ // A search for a withdrawn or invented id still returns an <entry> whose id echoes the query;
455
+ // a real entry's title is never "Error".
456
+ error: /^Error$/i.test(tag("title") ?? ""),
457
+ };
458
+ }
459
+
460
+ const unescapeXml = (s) =>
461
+ s
462
+ .replace(/&lt;/g, "<")
463
+ .replace(/&gt;/g, ">")
464
+ .replace(/&quot;/g, '"')
465
+ .replace(/&apos;/g, "'")
466
+ .replace(/&amp;/g, "&");
467
+
468
+ // ── cache and network ────────────────────────────────────────────────────────
469
+
470
+ export function loadCache(path) {
471
+ if (!path || !existsSync(path)) return {};
472
+ try {
473
+ return JSON.parse(readFileSync(path, "utf8"));
474
+ } catch {
475
+ return {};
476
+ }
477
+ }
478
+
479
+ function saveCache(path, cache) {
480
+ if (!path) return;
481
+ mkdirSync(dirname(path), { recursive: true });
482
+ writeFileSync(path, `${JSON.stringify(cache, null, 1)}\n`);
483
+ }
484
+
485
+ async function fetchKey(key) {
486
+ const { url } = routeOf(key);
487
+ const r = await fetch(url, { headers: { "User-Agent": UA } });
488
+ return {
489
+ httpStatus: r.status,
490
+ body: await r.text(),
491
+ fetched: new Date().toISOString().slice(0, 10),
492
+ };
493
+ }
494
+
495
+ /**
496
+ * A raw response → a facts record. Everything that can throw lives HERE: a rule that throws brings
497
+ * down the whole ESLint run, not one finding.
498
+ */
499
+ export function recordFrom(key, cached) {
500
+ if (!cached) return { cached: false };
501
+ const { registry } = routeOf(key);
502
+ const base = {
503
+ cached: true,
504
+ registry,
505
+ httpStatus: cached.httpStatus ?? null,
506
+ fetched: cached.fetched ?? null,
507
+ };
508
+ if (cached.httpStatus !== 200) return base;
509
+ let rec; // no initializer: try assigns it, catch returns (2026-08-28)
510
+ try {
511
+ rec =
512
+ registry === "crossref"
513
+ ? readCrossref(cached.body)
514
+ : readArxiv(cached.body);
515
+ } catch (e) {
516
+ return {
517
+ ...base,
518
+ found: false,
519
+ parse_error: String(e.message).split("\n")[0],
520
+ };
521
+ }
522
+ if (!rec) return { ...base, found: false };
523
+ return { ...base, found: !rec.error, ...rec };
524
+ }
525
+
526
+ // ── assembling the facts ─────────────────────────────────────────────────────
527
+
528
+ /** `paper.md` → `draft.md` → `refs.bib` → `build/custom.bib`. `refs.bib` ADDED 26.08 (defect #1). */
529
+ export const SOURCE_ORDER = [
530
+ "paper.md",
531
+ "draft.md",
532
+ "refs.bib",
533
+ "build/custom.bib",
534
+ ];
535
+
536
+ export function resolveSource(target) {
537
+ const t = resolve(target);
538
+ if (existsSync(t) && statSync(t).isFile()) return t;
539
+ for (const c of SOURCE_ORDER) {
540
+ const p = join(t, c);
541
+ if (existsSync(p)) return p;
542
+ }
543
+ return null;
544
+ }
545
+
546
+ export async function loadEntries(path) {
547
+ const text = readFileSync(path, "utf8");
548
+ return path.endsWith(".bib") ? parseBib(text) : parseMarkdownRefs(text);
549
+ }
550
+
551
+ /** Every registry key a list of entries needs. Computed once so the requests go in one batch. */
552
+ export function keysFor(entries) {
553
+ const keys = new Set();
554
+ for (const e of entries) for (const k of allKeys(extractIds(e))) keys.add(k);
555
+ return [...keys];
556
+ }
557
+
558
+ const rel = (p) => (p.startsWith(ROOT) ? p.slice(ROOT.length + 1) : p);
559
+
560
+ export function buildFacts({ source, text, entries, cache, cachePath }) {
561
+ const withIds = entries.map((e) => {
562
+ const ids = extractIds(e);
563
+ return { ...e, ids, primary: primaryOf(ids), keys: allKeys(ids) };
564
+ });
565
+ const records = {};
566
+ for (const e of withIds)
567
+ for (const k of e.keys)
568
+ if (!(k in records)) records[k] = recordFrom(k, cache[k]);
569
+ return {
570
+ schema: SCHEMA,
571
+ source: rel(source),
572
+ source_kind: source.endsWith(".bib") ? "bibtex" : "markdown",
573
+ source_sha256: createHash("sha256").update(text).digest("hex"),
574
+ cache: cachePath ? rel(cachePath) : null,
575
+ generated: new Date().toISOString().slice(0, 10),
576
+ entries: withIds,
577
+ records,
578
+ };
579
+ }
580
+
581
+ // ── cli ──────────────────────────────────────────────────────────────────────
582
+
583
+ async function main(argv) {
584
+ const args = argv.slice(2);
585
+ const target = args.find((a) => !a.startsWith("--")) ?? ".";
586
+ const offline = args.includes("--offline");
587
+ const refresh = args.includes("--refresh");
588
+ const quiet = args.includes("--quiet");
589
+ const opt = (name) =>
590
+ (args.find((a) => a.startsWith(`--${name}=`)) ?? "")
591
+ .split("=")
592
+ .slice(1)
593
+ .join("=");
594
+
595
+ const src = resolveSource(target);
596
+ if (!src) {
597
+ console.error(
598
+ `🛑 no ${SOURCE_ORDER.join(", ")} under ${resolve(target)} — nowhere to take a bibliography from.`,
599
+ );
600
+ return 1;
601
+ }
602
+ const paperDir = statSync(resolve(target)).isFile()
603
+ ? dirname(src)
604
+ : resolve(target);
605
+ const cachePath = opt("cache")
606
+ ? resolve(opt("cache"))
607
+ : join(paperDir, "repro", "refs-cache.json");
608
+ const out = opt("out")
609
+ ? resolve(opt("out"))
610
+ : join(paperDir, "_build", "refs.facts.json");
611
+
612
+ const text = readFileSync(src, "utf8");
613
+ const entries = await loadEntries(src);
614
+ if (!entries.length) {
615
+ // 🔴 Zero entries is a suspect, not a success. Facts with an empty list would look like a
616
+ // clean bibliography, so we do not write them at all and exit with a non-zero code.
617
+ console.error(
618
+ `🛑 parsed 0 entries out of ${rel(src)}. Silence here would look like a clean bibliography.`,
619
+ );
620
+ return 1;
621
+ }
622
+
623
+ const cache = loadCache(cachePath);
624
+ let fetched = 0;
625
+ if (!offline) {
626
+ for (const k of keysFor(entries)) {
627
+ if (cache[k] && !refresh) continue;
628
+ try {
629
+ cache[k] = await fetchKey(k);
630
+ fetched++;
631
+ } catch (err) {
632
+ // A network failure is NOT a clean result: the key stays missing, and the rule will say
633
+ // the entry was not checked instead of declaring it correct.
634
+ if (!quiet) console.error(` … ${k}: ${err.message}`);
635
+ }
636
+ // A braced body: a concise arrow returns the Timeout out of the promise executor, where
637
+ // nobody reads it (`no-promise-executor-return`, 2026-08-28). The behaviour is the same.
638
+ await new Promise((r) => {
639
+ setTimeout(r, 250);
640
+ }); // both registries ask for a polite pace
641
+ }
642
+ if (fetched) saveCache(cachePath, cache);
643
+ }
644
+
645
+ const facts = buildFacts({ source: src, text, entries, cache, cachePath });
646
+ mkdirSync(dirname(out), { recursive: true });
647
+ writeFileSync(out, `${JSON.stringify(facts, null, 2)}\n`);
648
+ const resolved = Object.values(facts.records).filter((r) => r.found).length;
649
+ console.log(
650
+ `📚 ${basename(src)} → ${rel(out)} (${entries.length} entries, ${Object.keys(facts.records).length} identifiers, ` +
651
+ `${resolved} resolved${offline ? ", offline" : fetched ? `, +${fetched} fetched` : ", all from cache"})`,
652
+ );
653
+ return 0;
654
+ }
655
+
656
+ if (isMain(import.meta.url)) main(process.argv).then((c) => process.exit(c));