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,190 @@
1
+ /**
2
+ * consumer.mutations.mjs — plant each defect `consumer.harness.mjs` claims to catch, and require
3
+ * it to die BY ITS OWN assertion.
4
+ *
5
+ * Run: `node skills/paper-pipeline/scripts/consumer.mutations.mjs` (it rewrites `consumer.mjs` and
6
+ * restores it, so it is not part of the plain test run).
7
+ *
8
+ * 🔴 WHY THIS ONE MATTERS MORE THAN MOST. Every resolution in `consumer.mjs` fails toward SILENCE:
9
+ * a ledger under `node_modules` still accepts appends, a main guard that never fires still exits 0,
10
+ * a root that does not exist still hashes to a stable value. That is exactly the shape a harness
11
+ * cannot be seen catching — so the four cases below are the only evidence that its assertions can
12
+ * fail at all. Each `expect` is the harness's OWN message for that defect, not a generic failure
13
+ * string: a case killed by somebody else's assertion says nothing about the defect it planted.
14
+ */
15
+ import { dirname, join } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import { runMutations } from "../../../lib/mutation-driver.mjs";
18
+ import { consumerRoot } from "./consumer.mjs";
19
+
20
+ const HERE = dirname(fileURLToPath(import.meta.url));
21
+ const SRC = join(HERE, "consumer.mjs");
22
+ const HARNESS = join(HERE, "consumer.harness.mjs");
23
+
24
+ const MUTATIONS = [
25
+ [
26
+ "a declared `null` is read as an absence",
27
+ [
28
+ [
29
+ SRC,
30
+ "if (declared !== undefined) {",
31
+ "if (declared !== undefined && declared !== null) {",
32
+ ],
33
+ ],
34
+ 'a `"ledger": null` was accepted',
35
+ "the absence/keystroke distinction — a typed null silently picks a different file",
36
+ ],
37
+ [
38
+ "`node_modules` is detected by substring instead of by path segment",
39
+ [
40
+ [
41
+ SRC,
42
+ 'return dir.split(sep).includes("node_modules");',
43
+ 'return dir.includes("node_modules");',
44
+ ],
45
+ ],
46
+ "naming coincidence",
47
+ "a consumer whose own directory merely contains the word is refused its ledger",
48
+ ],
49
+ [
50
+ "the ledger default inside node_modules is no longer refused",
51
+ [
52
+ [
53
+ SRC,
54
+ " if (insideNodeModules(hereDir))",
55
+ " if (false && insideNodeModules(hereDir))",
56
+ ],
57
+ ],
58
+ "ledgerPath RETURNED A PATH INSIDE node_modules",
59
+ "the one guard against silent data loss: appends succeed until `npm ci` deletes them",
60
+ ],
61
+ [
62
+ "the main guard stops resolving the entry point's realpath",
63
+ [[SRC, " real = realpathSync(entry);", " real = entry;"]],
64
+ "isMain is false through a symlink",
65
+ "every CLI in this directory when a consumer runs it at the path its skills document",
66
+ ],
67
+
68
+ // ── the fourth carrier. Its defects are worse than the three above: the value is a PREFIX a
69
+ // caller filters prose with, so a wrong one matches nothing and every check built on it
70
+ // reports zero findings — the green zero this package has a dedicated guard against.
71
+ [
72
+ "a declared `null` scripts path is read as an absence",
73
+ [
74
+ [
75
+ SRC,
76
+ "const rel = declared === undefined ? DEFAULT_SCRIPTS_ROOT : declared;",
77
+ "const rel = declared ?? DEFAULT_SCRIPTS_ROOT;",
78
+ ],
79
+ ],
80
+ 'a `"scripts": null` was accepted',
81
+ "the absence/keystroke distinction — a typed null is ignored and the default silently wins",
82
+ ],
83
+ [
84
+ "a scripts path inside node_modules is no longer refused",
85
+ [
86
+ [
87
+ SRC,
88
+ " if (insideNodeModules(abs))",
89
+ " if (false && insideNodeModules(abs))",
90
+ ],
91
+ ],
92
+ "scriptsRoot ACCEPTED a path inside node_modules",
93
+ "the guard against naming a path `npm ci` deletes and git does not track",
94
+ ],
95
+ [
96
+ "a scripts path that is not on disk is no longer refused",
97
+ [[SRC, " if (!existsSync(abs))", " if (false && !existsSync(abs))"]],
98
+ "scriptsRoot accepted a path that does not exist",
99
+ "the only net under the wrong prefix, whose symptom is zero findings and exit 0",
100
+ ],
101
+ [
102
+ "the scripts root is returned resolved instead of as declared",
103
+ [[SRC, " return rel;", " return abs;"]],
104
+ "scriptsRoot returned an ABSOLUTE path",
105
+ "prose comparison — every `startsWith` against a SKILL.md becomes false",
106
+ ],
107
+ [
108
+ "a declared trailing slash is no longer normalised",
109
+ [[SRC, 'const base = root.replace(/\\/+$/, "");', "const base = root;"]],
110
+ "a declared trailing slash doubled the separator",
111
+ "the one input a consumer types by habit, whose doubled prefix matches nothing",
112
+ ],
113
+ [
114
+ "the prefix loses its trailing separator",
115
+ [[SRC, " prefix: `${base}/`,", " prefix: `${base}`,"]],
116
+ "the prefix matched a sibling directory sharing the root's name",
117
+ "the boundary between the pipeline's scripts and a directory merely named like them",
118
+ ],
119
+
120
+ // ── `installedSkills` (rpp#62). Its defects fail toward a WRONG ROSTER, not an error: a reader
121
+ // that does not follow links sees zero skills in every consumer; one that skips a dangling link
122
+ // reports a broken install as "not installed"; one that counts it hands out an unreadable skill.
123
+ [
124
+ "installed skills are listed by `Dirent.isDirectory()` again, which does not follow links",
125
+ [
126
+ [
127
+ SRC,
128
+ " for (const name of readdirSync(dir)) {",
129
+ " for (const dirent of readdirSync(dir, { withFileTypes: true })) {\n const name = dirent.name;",
130
+ ],
131
+ [
132
+ SRC,
133
+ ' if (st.isDirectory() && existsSync(join(entry, "SKILL.md")))',
134
+ ' if (dirent.isDirectory() && existsSync(join(entry, "SKILL.md")))',
135
+ ],
136
+ ],
137
+ "a SYMLINKED skill was not counted as installed",
138
+ "every installed skill in every consumer — `paperlint init` makes links, and the evals refuse to start",
139
+ ],
140
+ [
141
+ "a dangling skill link is skipped silently",
142
+ [
143
+ [
144
+ SRC,
145
+ ' dangling.push({\n name,\n target: readlinkOr(entry),\n cause: e.code ?? "error",\n });',
146
+ "",
147
+ ],
148
+ ],
149
+ "a DANGLING skill link was silently accepted",
150
+ "the report of a broken install — a retired skill's stale link reads as one never installed",
151
+ ],
152
+ [
153
+ "a dangling skill link is counted as installed",
154
+ [
155
+ [
156
+ SRC,
157
+ ' dangling.push({\n name,\n target: readlinkOr(entry),\n cause: e.code ?? "error",\n });',
158
+ " names.push(name);",
159
+ ],
160
+ ],
161
+ "a DANGLING skill link was silently accepted",
162
+ "the guarantee that every name returned has a SKILL.md a caller can open",
163
+ ],
164
+ [
165
+ "any directory counts as an installed skill",
166
+ [
167
+ [
168
+ SRC,
169
+ ' if (st.isDirectory() && existsSync(join(entry, "SKILL.md")))',
170
+ " if (st.isDirectory())",
171
+ ],
172
+ ],
173
+ "installedSkills must return exactly the entries that lead to a directory holding SKILL.md",
174
+ "the definition of a skill — a helper directory beside the skills joins the roster",
175
+ ],
176
+ ];
177
+
178
+ process.exit(
179
+ runMutations({
180
+ root: consumerRoot(),
181
+ runner: "vigiles",
182
+ cases: MUTATIONS.map(([name, edits, expect, disables]) => ({
183
+ name,
184
+ disables,
185
+ edits,
186
+ harness: HARNESS,
187
+ expect,
188
+ })),
189
+ }),
190
+ );
@@ -0,0 +1,457 @@
1
+ /**
2
+ * Colocated test of the `extract-ref-facts.mjs` extractor.
3
+ * Run: `npx vigiles test .claude/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs`
4
+ *
5
+ * ORDER IS LOAD-BEARING: first we prove that on CORRECT input the extractor returns the facts it
6
+ * promises, and only then — that on broken input it refuses to write. An extractor that silently
7
+ * writes empty facts is scarier than a missing one: empty bibliography reads as clean.
8
+ *
9
+ * 🔴 THREE MEASUREMENTS HERE — THESE ARE THREE DEFECTS THAT LIVED THE ENTIRE LIFE OF THE PREDECESSOR. Each
10
+ * is pinned by an assertion on a REAL repository file, not a fixture, because all three
11
+ * held exactly because nobody ever passed the real file:
12
+ * 1. `refs.bib` was not in the list of sources (`resolveSource`);
13
+ * 2. the regex `.bib` required `}` on a separate line and gave 0 entries on both our files;
14
+ * 3. in `.bib` an author is written "LastName, FirstName", but comparison took the last token.
15
+ *
16
+ * 🔴 NO NETWORK. Registry answers — fixtures in a cache file, read by the same code that writes
17
+ * a real request. A test that needs the internet is skipped in CI, and a skipped check
18
+ * is indistinguishable from one that passed.
19
+ *
20
+ * 🔴 Assertions — AT MODULE TOP LEVEL: `vigiles test` imports the file and counts "did not throw"
21
+ * as success, so an exported test object would report ✓ without running anything.
22
+ */
23
+ import assert from "node:assert/strict";
24
+ import { PAPERS_DIR_FIELD } from "../../../lib/paper-config.mjs";
25
+ import {
26
+ existsSync,
27
+ mkdirSync,
28
+ mkdtempSync,
29
+ readFileSync,
30
+ readdirSync,
31
+ rmSync,
32
+ writeFileSync,
33
+ realpathSync,
34
+ } from "node:fs";
35
+ import { tmpdir } from "node:os";
36
+ import { dirname, join } from "node:path";
37
+ import { fileURLToPath } from "node:url";
38
+ import { consumerRoot } from "./consumer.mjs";
39
+
40
+ const HERE = dirname(fileURLToPath(import.meta.url));
41
+ const ROOT = consumerRoot();
42
+ const X = await import(join(HERE, "extract-ref-facts.mjs"));
43
+
44
+ // 🔴 PAPERS ROOT — FROM THE DECLARATION, NOT BY THE FIRST CONSUMER'S DIRECTORY NAME (12.09.2026).
45
+ // `package.json` → `paperlint.papersDir`, default `papers`. The same key is read by
46
+ // ESLint config, hook and skills — the mechanism was moved into the package at step 2, and a second way to know
47
+ // the same thing would be a second truth.
48
+ //
49
+ // Real consumer papers — ADDITION to fixture, "if they exist — check against them too".
50
+ // Absence is lawful and silently skipped; the test itself is carried by the fixture, and its absence is caught
51
+ // with a loud zero below.
52
+ //
53
+ // 🔴 READ FROM DISK, NOT LISTED BY NAME (12.09.2026). There used to be a list of two
54
+ // paper names of the first consumer. A list of names in the PACKAGE is wrong twice: a different consumer has no such
55
+ // directories, meaning the loop makes no iteration and prints success having checked zero
56
+ // files; and it names others' unpublished works in a repository everyone reads.
57
+ // A directory on disk answers the same question without knowing any name in advance.
58
+ const PAPERS_ROOT = join(
59
+ ROOT,
60
+ JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"))["paperlint"]?.[
61
+ PAPERS_DIR_FIELD
62
+ ] ?? "papers",
63
+ );
64
+ const REAL_PAPERS = existsSync(PAPERS_ROOT)
65
+ ? readdirSync(PAPERS_ROOT, { withFileTypes: true })
66
+ .filter((e) => e.isDirectory())
67
+ .map((e) => e.name)
68
+ : [];
69
+
70
+ const TMP = realpathSync(mkdtempSync(join(tmpdir(), "extract-ref-facts-")));
71
+ // Cleanup is attached IMMEDIATELY: assertions throw, and "rmSync at end of file" does not execute precisely in those
72
+ // runs that are red.
73
+ process.on("exit", () => rmSync(TMP, { recursive: true, force: true }));
74
+
75
+ // ── 1. DEFECT #1: `refs.bib` must be in the list of sources ─────────────────
76
+ {
77
+ assert.ok(
78
+ X.SOURCE_ORDER.includes("refs.bib"),
79
+ `refs.bib missing from SOURCE_ORDER — this is the defect that kept the .bib leg from ever opening: ${X.SOURCE_ORDER.join(", ")}`,
80
+ );
81
+ // 🔴 SUBJECT — FIXTURE, real papers ADDITION (12.09.2026). There used to be only a loop over
82
+ // our two papers with `continue` if file doesn't exist. In this repo they exist, and test is green;
83
+ // in the extracted pipeline their directories WON'T EXIST — both loops make no iteration, and
84
+ // harness prints success having checked zero files. "Green over void", a class this base has paid for thrice. Fixture makes an empty run INEXPRESSIBLE: it always lies nearby.
85
+ //
86
+ // ⚠️ Directory, not file: `resolveSource` returns the path itself if it IS A FILE, and only for
87
+ // a directory descends the `SOURCE_ORDER`. A fixture-file would check the wrong branch.
88
+ {
89
+ const dir = join(HERE, "fixtures", "real-bib");
90
+ assert.equal(
91
+ X.resolveSource(dir),
92
+ join(dir, "refs.bib"),
93
+ "resolveSource cannot find the frozen real .bib — .bib leg is dead again",
94
+ );
95
+ }
96
+ for (const paper of REAL_PAPERS) {
97
+ const dir = join(PAPERS_ROOT, paper);
98
+ if (!existsSync(join(dir, "refs.bib"))) continue;
99
+ assert.equal(
100
+ X.resolveSource(dir),
101
+ join(dir, "refs.bib"),
102
+ `resolveSource cannot find real refs.bib for ${paper} — .bib leg is dead again`,
103
+ );
104
+ }
105
+ }
106
+
107
+ // ── 2. DEFECT #2: real .bib parses, and entry count matches ───────
108
+ //
109
+ // REAL files, not fixture: the old parser failed on the form `}}` at the end of the last
110
+ // field, and a hand-written fixture almost certainly would close the entry on its own line — that is,
111
+ // the test would pass but the corpus would not. The expected count comes from the file itself (`^@`), not
112
+ // hardcoded: a constant grows stale on the first added entry.
113
+ {
114
+ // Counter of parsed sources, checked after the loop: a check that prints "passed"
115
+ // but says nothing about HOW MANY files it opened — this is a counter that counts what it ignores.
116
+ let parsed = 0;
117
+ const sources = [
118
+ join(HERE, "fixtures", "real-bib", "refs.bib"), // always lies nearby
119
+ ...REAL_PAPERS.map((p) => join(PAPERS_ROOT, p, "refs.bib")),
120
+ ];
121
+ for (const f of sources) {
122
+ const paper = f.includes("fixtures")
123
+ ? "fixtures/real-bib"
124
+ : f.split("/").slice(-2)[0];
125
+ if (!existsSync(f)) continue;
126
+ parsed++;
127
+ const text = readFileSync(f, "utf8");
128
+ const want = (text.match(/^@\w+\{/gm) ?? []).length;
129
+ const got = await X.parseBib(text);
130
+ assert.ok(
131
+ want > 0,
132
+ `${paper}/refs.bib: no @… entries in file — fixture is broken`,
133
+ );
134
+ assert.equal(
135
+ got.length,
136
+ want,
137
+ `${paper}/refs.bib: parsed ${got.length} entries out of ${want}`,
138
+ );
139
+ for (const e of got) {
140
+ assert.ok(e.title, `${paper}: entry ${e.key} has no title`);
141
+ assert.ok(
142
+ e.line > 0,
143
+ `${paper}: entry ${e.key} has no source line — nowhere to report the finding`,
144
+ );
145
+ }
146
+ }
147
+ // Loud zero. Fixture makes it unreachable today — and precisely why the assertion stands: it
148
+ // catches not absence of papers (that is lawful), but disappearance of the fixture itself, after which the loop
149
+ // will start printing success without opening any file.
150
+ assert.ok(
151
+ parsed > 0,
152
+ "parsed ZERO .bib files. This is not 'nothing to check', but 'check did not find its subject': " +
153
+ "the frozen fixture fixtures/real-bib/refs.bib must always lie in the repository.",
154
+ );
155
+ }
156
+
157
+ // ── 3b. INSTITUTION DOES NOT DISAPPEAR ──────────────────────────────────────────────
158
+ //
159
+ // `author={{Adversa AI}}` — double brace means "one name whole, do not parse". Parser
160
+ // returns such as `{name}`, WITHOUT `lastName`, and this is correct: an organization has no surname.
161
+ // Measurement 17.09 on real aisec-2026: eleven entries out of fifty-one came back
162
+ // with `authors = []`, because joinName didn't know this form. Failure toward SILENCE — checking against
163
+ // such entries found nothing and looked like passing.
164
+ {
165
+ const bib = `@misc{inst,
166
+ author={{Adversa AI}},
167
+ title={A Report}, year={2026}}
168
+ @misc{inst2,
169
+ author={{sh-guard contributors}},
170
+ title={Another}, year={2026}}`;
171
+ const es = await X.parseBib(bib);
172
+ assert.deepEqual(
173
+ es.map((e) => e.authors),
174
+ [["Adversa AI"], ["sh-guard contributors"]],
175
+ `institution lost or torn apart: ${JSON.stringify(es.map((e) => e.authors))}`,
176
+ );
177
+ assert.equal(es[0].truncated, false, "institution is not `and others`");
178
+ }
179
+
180
+ // ── 3c. …AND SINGLE BRACE STILL PARSES AS PERSON ─────────────
181
+ //
182
+ // Second half. Without it the fix above is indistinguishable from "stopped parsing names at all":
183
+ // single brace — ordinary author, and order "LastName, FirstName" must be unwound.
184
+ {
185
+ const bib = `@misc{human, author={Adversa, Alice}, title={T}, year={2026}}`;
186
+ const [e] = await X.parseBib(bib);
187
+ assert.deepEqual(
188
+ e.authors,
189
+ ["Alice Adversa"],
190
+ `single brace must parse as person: ${JSON.stringify(e.authors)}`,
191
+ );
192
+ }
193
+
194
+ // ── 3. DEFECT #3: name from .bib is converted to order "FirstName LastName" ─────────────
195
+ //
196
+ // In BibTeX they write `Jimenez, Carlos E.`, and the "last alphabetic token" of such a string is `e`.
197
+ // The rule compares exactly the last token, so order must be unwound HERE.
198
+ {
199
+ const bib = `@inproceedings{k1,
200
+ author={Jimenez, Carlos E. and Di Penta, Massimiliano and others},
201
+ title={A Title}, booktitle={ICLR}, year={2024}, note={arXiv:2310.06770}}`;
202
+ const [e] = await X.parseBib(bib);
203
+ assert.deepEqual(
204
+ e.authors,
205
+ ["Carlos E. Jimenez", "Massimiliano Di Penta"],
206
+ `name order not unwound: ${JSON.stringify(e.authors)}`,
207
+ );
208
+ assert.equal(
209
+ e.truncated,
210
+ true,
211
+ "`and others` is et al. in BibTeX format, and it must be marked as truncation",
212
+ );
213
+ assert.deepEqual(X.extractIds(e), { arxiv: ["2310.06770"], doi: [] });
214
+ }
215
+
216
+ // ── 4. arXiv-DOI goes to arXiv, NOT to CrossRef ───────────────────────────────
217
+ //
218
+ // Measurement 26.08: `api.crossref.org/works/10.48550%2FarXiv.2107.03374` → HTTP 404 "Resource not
219
+ // found" on a completely real DOI in <paper-b>/refs.bib. Asking CrossRef,
220
+ // gate `id-resolves` would declare the correct entry broken.
221
+ {
222
+ assert.equal(X.routeOf("doi:10.48550/arXiv.2107.03374").registry, "arxiv");
223
+ assert.ok(
224
+ X.routeOf("doi:10.48550/arXiv.2107.03374").url.includes("2107.03374"),
225
+ );
226
+ assert.equal(X.routeOf("doi:10.1145/3597503.3623333").registry, "crossref");
227
+ assert.equal(X.routeOf("arxiv:2310.06770").registry, "arxiv");
228
+ }
229
+
230
+ // ── 5. registry readers work on RAW bodies ─────────────────────────────
231
+ //
232
+ // Cache stores raw responses precisely for this: every fabricated title this base
233
+ // shipped was found by reading the registry's response. A cache of parsed entries would leave readers
234
+ // uncovered, appearing covered.
235
+ {
236
+ const atom = `<?xml version='1.0' encoding='UTF-8'?>
237
+ <feed xmlns:arxiv="http://arxiv.org/schemas/atom" xmlns="http://www.w3.org/2005/Atom">
238
+ <entry>
239
+ <id>http://arxiv.org/abs/0000.00000v1</id>
240
+ <title>A &amp; B: A Study</title>
241
+ <published>2025-03-12T00:00:00Z</published>
242
+ <author><name>Ada Lovelace</name></author>
243
+ <author><name>Grace Hopper</name></author>
244
+ <arxiv:journal_ref>Proc. ICSE 2025</arxiv:journal_ref>
245
+ </entry>
246
+ </feed>`;
247
+ const a = X.readArxiv(atom);
248
+ assert.equal(a.title, "A & B: A Study", "XML entities not decoded");
249
+ assert.deepEqual(a.authors, ["Ada Lovelace", "Grace Hopper"]);
250
+ assert.equal(a.year, "2025");
251
+ assert.equal(a.journalRef, "Proc. ICSE 2025");
252
+ assert.equal(a.error, false);
253
+ assert.equal(
254
+ X.readArxiv(`<feed xmlns="http://www.w3.org/2005/Atom"></feed>`),
255
+ null,
256
+ "feed without <entry> must return null",
257
+ );
258
+
259
+ // CrossRef puts subtitle after colon in SEPARATE field; joining must return it,
260
+ // otherwise correct entry gets similarity 0.69 and is declared fabricated.
261
+ const cr = JSON.stringify({
262
+ message: {
263
+ title: ["Variability-Aware Static Analysis at Scale"],
264
+ subtitle: ["An Empirical Study"],
265
+ author: [{ family: "Liebig" }, { family: "von Rhein" }],
266
+ issued: { "date-parts": [[2018]] },
267
+ "container-title": ["TOSEM"],
268
+ },
269
+ });
270
+ const c = X.readCrossref(cr);
271
+ assert.equal(
272
+ c.title,
273
+ "Variability-Aware Static Analysis at Scale: An Empirical Study",
274
+ );
275
+ assert.equal(c.year, "2018");
276
+ assert.deepEqual(c.authors, ["Liebig", "von Rhein"]);
277
+ }
278
+
279
+ // ── 6. recording facts: registry response → record, and UNparsed response is also a fact ─
280
+ {
281
+ const key = "arxiv:2310.06770";
282
+ const ok = X.recordFrom(key, {
283
+ httpStatus: 200,
284
+ body: `<feed><entry><title>T</title><author><name>A B</name></author><published>2023-01-01T00:00:00Z</published></entry></feed>`,
285
+ });
286
+ assert.equal(ok.found, true);
287
+ assert.equal(ok.registry, "arxiv");
288
+ assert.equal(
289
+ X.recordFrom(key, { httpStatus: 404, body: "" }).found,
290
+ undefined,
291
+ "non-200 must not be declared found",
292
+ );
293
+ assert.equal(
294
+ X.recordFrom(key, undefined).cached,
295
+ false,
296
+ "missing response is a fact 'never asked', not 'no entry'",
297
+ );
298
+ const broken = X.recordFrom("doi:10.1145/x", {
299
+ httpStatus: 200,
300
+ body: "{not json",
301
+ });
302
+ assert.equal(broken.found, false);
303
+ assert.ok(
304
+ broken.parse_error,
305
+ "unreadable response must leave reason, not silently disappear",
306
+ );
307
+ }
308
+
309
+ // ── 7. markdown: reference list, source lines, `et al.`, appendix ───
310
+ {
311
+ const md = [
312
+ "# Paper",
313
+ "",
314
+ "## 1. Intro",
315
+ "",
316
+ "Prose.",
317
+ "",
318
+ "## References",
319
+ "",
320
+ "1. C. Yang, Z. Zhao, L. Zhang. *KNighter: Transforming Static Analysis.* arXiv:2503.09002, 2025.",
321
+ "2. Z. Xiang et al. *Another Work.* ICSE 2024. arXiv:2401.00001",
322
+ "",
323
+ "## Appendix A",
324
+ "",
325
+ "1. Not a reference at all.",
326
+ "",
327
+ ].join("\n");
328
+ const es = X.parseMarkdownRefs(md);
329
+ assert.equal(
330
+ es.length,
331
+ 2,
332
+ `heading must close the reference list, parsed ${es.length}`,
333
+ );
334
+ assert.equal(
335
+ es[0].line,
336
+ 9,
337
+ `line of first entry is ${es[0].line}, but it is ninth in file`,
338
+ );
339
+ assert.equal(es[0].title, "KNighter: Transforming Static Analysis");
340
+ assert.equal(es[1].truncated, true, "`et al.` must be marked as truncation");
341
+ assert.deepEqual(es[1].authors, ["Z. Xiang"]);
342
+ assert.equal(es[1].year, "2024");
343
+ }
344
+
345
+ // ── 8. `## References` inside ```-block does NOT open list ──────────────────
346
+ // Papers in this repo cite their own markup in pieces; the old `split` would open
347
+ // bibliography in the middle of a code example.
348
+ {
349
+ const md = [
350
+ "# P",
351
+ "",
352
+ "## Body",
353
+ "",
354
+ "```md",
355
+ "## References",
356
+ "",
357
+ "1. Fake. *X.* arXiv:1111.11111",
358
+ "```",
359
+ "",
360
+ ].join("\n");
361
+ assert.deepEqual(
362
+ X.parseMarkdownRefs(md),
363
+ [],
364
+ "heading inside ```-block is not a heading",
365
+ );
366
+ }
367
+
368
+ // ── 9. CLI: ZERO ENTRIES — REFUSAL, not empty facts ─────────────────────────
369
+ {
370
+ const { spawnSync } = await import("node:child_process");
371
+ const dir = join(TMP, "empty");
372
+ mkdirSync(dir, { recursive: true });
373
+ writeFileSync(join(dir, "paper.md"), "# P\n\nNo links at all.\n");
374
+ const r = spawnSync(
375
+ "node",
376
+ [join(HERE, "extract-ref-facts.mjs"), dir, "--offline"],
377
+ { encoding: "utf8" },
378
+ );
379
+ assert.equal(
380
+ r.status,
381
+ 1,
382
+ `empty bibliography must return non-zero code, returned ${r.status}`,
383
+ );
384
+ assert.ok(
385
+ /ZERO|0 entries/u.test(r.stderr),
386
+ `no reason in stderr: ${r.stderr}`,
387
+ );
388
+ assert.ok(
389
+ !existsSync(join(dir, "_build", "refs.facts.json")),
390
+ "facts with empty list written to disk — would be read as clean bibliography",
391
+ );
392
+ }
393
+
394
+ // ── 10. CLI: no source — also refusal, not silence ─────────────────────────
395
+ {
396
+ const { spawnSync } = await import("node:child_process");
397
+ const dir = join(TMP, "nosrc");
398
+ mkdirSync(dir, { recursive: true });
399
+ const r = spawnSync(
400
+ "node",
401
+ [join(HERE, "extract-ref-facts.mjs"), dir, "--offline"],
402
+ { encoding: "utf8" },
403
+ );
404
+ assert.equal(r.status, 1);
405
+ assert.ok(
406
+ /refs\.bib/u.test(r.stderr),
407
+ "message must list sources, including refs.bib",
408
+ );
409
+ }
410
+
411
+ // ── 11. end-to-end: real paper → facts, freshness is computed ───────────────
412
+ {
413
+ const { spawnSync } = await import("node:child_process");
414
+ const dir = join(TMP, "e2e");
415
+ mkdirSync(join(dir, "repro"), { recursive: true });
416
+ const paper = [
417
+ "# P",
418
+ "",
419
+ "## References",
420
+ "",
421
+ "1. A. Lovelace, G. Hopper. *A Study.* arXiv:2503.09002, 2025.",
422
+ "",
423
+ ].join("\n");
424
+ writeFileSync(join(dir, "paper.md"), paper);
425
+ writeFileSync(
426
+ join(dir, "repro", "refs-cache.json"),
427
+ JSON.stringify({
428
+ "arxiv:2503.09002": {
429
+ httpStatus: 200,
430
+ body: `<feed><entry><title>A Study</title><author><name>Ada Lovelace</name></author><author><name>Grace Hopper</name></author><published>2025-01-01T00:00:00Z</published></entry></feed>`,
431
+ },
432
+ }),
433
+ );
434
+ const r = spawnSync(
435
+ "node",
436
+ [join(HERE, "extract-ref-facts.mjs"), dir, "--offline"],
437
+ { encoding: "utf8" },
438
+ );
439
+ assert.equal(r.status, 0, r.stderr);
440
+ const facts = JSON.parse(
441
+ readFileSync(join(dir, "_build", "refs.facts.json"), "utf8"),
442
+ );
443
+ assert.equal(facts.schema, X.SCHEMA);
444
+ assert.equal(facts.entries.length, 1);
445
+ assert.equal(facts.records["arxiv:2503.09002"].found, true);
446
+ const { createHash } = await import("node:crypto");
447
+ assert.equal(
448
+ facts.source_sha256,
449
+ createHash("sha256").update(paper).digest("hex"),
450
+ "source sha256 does not match — rule `fresh` could not catch stale facts",
451
+ );
452
+ }
453
+
454
+ console.log(
455
+ "✓ extract-ref-facts: refs.bib in sources, both real .bib parse completely, name order unwound, " +
456
+ "arXiv-DOI does not go to CrossRef, registry readers run on raw bodies, zero entries = refusal",
457
+ );