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,735 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * verify-cites.test.mjs — offline, deterministic tests for the PURE reducer.
4
+ *
5
+ * These exercise reduceVerdict / classifyResolver / checkNvd / checkCommit with
6
+ * injected fixtures — NO network. They pin the narrowed-false semantics:
7
+ * - a resolvable id that fails, or a DOI on the wrong paper → false
8
+ * - a title you simply can't find → unresolvable (NOT false)
9
+ *
10
+ * Run: npx vitest run skills/verify-citations/scripts/verify-cites.test.mjs
11
+ */
12
+
13
+ import assert from "node:assert/strict";
14
+ import { test } from "vitest";
15
+ import {
16
+ levenshtein,
17
+ titleSimilarity,
18
+ titlesMatch,
19
+ yearMatch,
20
+ titleRelation,
21
+ bestTitleRelation,
22
+ candidateTitles,
23
+ classifyResolver,
24
+ classifyDoiAuthority,
25
+ checkNvd,
26
+ checkCommit,
27
+ reduceVerdict,
28
+ parseBib,
29
+ parseArxivFeed,
30
+ normalizeDoi,
31
+ normalizeArxiv,
32
+ normalizeIdentifiers,
33
+ } from "./verify-cites.mjs";
34
+
35
+ /** One vitest case per check, named by it. */
36
+ function ok(cond, name) {
37
+ test(name, () => {
38
+ assert.ok(cond, name);
39
+ });
40
+ }
41
+ function eq(actual, expected, name) {
42
+ ok(actual === expected, `${name} (got ${JSON.stringify(actual)})`);
43
+ }
44
+
45
+ // ── string helpers ──────────────────────────────────────────────────────────
46
+ console.log("string helpers:");
47
+ eq(levenshtein("kitten", "sitting"), 3, "levenshtein kitten/sitting = 3");
48
+ eq(levenshtein("abc", "abc"), 0, "levenshtein identical = 0");
49
+ ok(
50
+ titleSimilarity("Attention Is All You Need", "attention is all you need") >
51
+ 0.99,
52
+ "case/normalize identical titles",
53
+ );
54
+ ok(
55
+ titlesMatch(
56
+ "Deep Residual Learning for Image Recognition",
57
+ "Deep Residual Learning for Image Recognitionn",
58
+ ),
59
+ "one typo still ≥0.70",
60
+ );
61
+ ok(
62
+ !titlesMatch(
63
+ "Attention Is All You Need",
64
+ "A Survey of Reinforcement Learning",
65
+ ),
66
+ "unrelated titles < 0.70",
67
+ );
68
+ ok(yearMatch(2020, 2021), "year ±1 ok");
69
+ ok(!yearMatch(2015, 2020), "year off by 5 fails");
70
+ ok(yearMatch(undefined, 2020), "missing claimed year is not disqualifying");
71
+
72
+ // ── (1) real DOI that matches → true ─────────────────────────────────────────
73
+ console.log("\n(1) DOI resolves to the matching paper → true:");
74
+ {
75
+ const cite = {
76
+ id: "vaswani2017",
77
+ doi: "10.5555/3295222.3295349",
78
+ title: "Attention Is All You Need",
79
+ year: 2017,
80
+ };
81
+ const crossref = {
82
+ db: "crossref",
83
+ transport: "ok",
84
+ query: "doi",
85
+ record: { title: "Attention is all you need", year: 2017 },
86
+ };
87
+ const ev = classifyResolver(cite, crossref);
88
+ eq(ev.status, "matched", "classify → matched");
89
+ const v = reduceVerdict(cite, [ev], checkCommit(cite));
90
+ eq(v.verdict, "true", "verdict true");
91
+ eq(v.matched_db, "crossref", "matched_db = crossref");
92
+ }
93
+
94
+ // ── (2) DOI resolves to an UNRELATED title → false (DOI_MISMATCH) ─────────────
95
+ console.log(
96
+ "\n(2) DOI resolves but to an unrelated paper → false (DOI_MISMATCH):",
97
+ );
98
+ {
99
+ const cite = {
100
+ id: "ghost2021",
101
+ doi: "10.1000/realbutwrong",
102
+ title: "A Formal Verification Framework for Autonomous Agents",
103
+ year: 2021,
104
+ };
105
+ const openalex = {
106
+ db: "openalex",
107
+ transport: "ok",
108
+ query: "doi",
109
+ record: { title: "Photosynthesis in Deep-Sea Bacteria", year: 2009 },
110
+ };
111
+ const ev = classifyResolver(cite, openalex);
112
+ eq(ev.status, "doi_mismatch", "classify → doi_mismatch");
113
+ const v = reduceVerdict(cite, [ev], checkCommit(cite));
114
+ eq(v.verdict, "false", "verdict false");
115
+ ok(/DOI_MISMATCH/.test(v.reason), "reason names DOI_MISMATCH");
116
+ }
117
+
118
+ // ── (3) made-up arXiv id that resolves to nothing → false ────────────────────
119
+ console.log(
120
+ "\n(3) provided arXiv id resolves to nothing → false (RESOLVED_TO_NOTHING):",
121
+ );
122
+ {
123
+ const cite = {
124
+ id: "fake2023",
125
+ arxiv: "2399.99999",
126
+ title: "Neural Fabrication Networks",
127
+ year: 2023,
128
+ };
129
+ // Every resolver either can't find the id or is a title miss; the arXiv id-keyed
130
+ // miss is the fabrication evidence.
131
+ const arxiv = { db: "arxiv", transport: "ok", query: "arxiv", record: null };
132
+ const s2 = {
133
+ db: "semantic_scholar",
134
+ transport: "ok",
135
+ query: "arxiv",
136
+ record: null,
137
+ };
138
+ const crossref = {
139
+ db: "crossref",
140
+ transport: "ok",
141
+ query: "title",
142
+ records: [],
143
+ };
144
+ const evs = [arxiv, s2, crossref].map((r) => classifyResolver(cite, r));
145
+ eq(evs[0].status, "id_unmatched", "arxiv classify → id_unmatched");
146
+ const v = reduceVerdict(cite, evs, checkCommit(cite));
147
+ eq(v.verdict, "false", "verdict false");
148
+ ok(/RESOLVED_TO_NOTHING/.test(v.reason), "reason names RESOLVED_TO_NOTHING");
149
+ }
150
+
151
+ // ── (4) title not found anywhere, nothing else → unresolvable (NOT false) ─────
152
+ console.log(
153
+ "\n(4) title-only, found nowhere → unresolvable (the narrowed-false rule):",
154
+ );
155
+ {
156
+ const cite = {
157
+ id: "regional1998",
158
+ title: "Water Resource Management of the Balkhash Region",
159
+ year: 1998,
160
+ };
161
+ // Title searches across all four DBs come back empty (unindexed regional work).
162
+ const responses = ["crossref", "openalex", "semantic_scholar", "arxiv"].map(
163
+ (db) => ({
164
+ db,
165
+ transport: "ok",
166
+ query: "title",
167
+ records: [],
168
+ }),
169
+ );
170
+ const evs = responses.map((r) => classifyResolver(cite, r));
171
+ ok(
172
+ evs.every((e) => e.status === "title_miss"),
173
+ "all classify → title_miss",
174
+ );
175
+ const v = reduceVerdict(cite, evs, checkCommit(cite));
176
+ eq(v.verdict, "unresolvable", "verdict unresolvable — NOT false");
177
+ ok(
178
+ !/fabricat/i.test(v.reason.split("NOT fabrication")[0]),
179
+ "reason does not accuse fabrication",
180
+ );
181
+ }
182
+
183
+ // ── (5) CVE verified vs a mocked NVD hit → true ──────────────────────────────
184
+ console.log("\n(5) CVE present, mocked NVD hit → true:");
185
+ {
186
+ const cite = { id: "log4shell", cve: "CVE-2021-44228" };
187
+ const nvd = { transport: "ok", found: true };
188
+ const ev = checkNvd(cite, nvd);
189
+ eq(ev.status, "matched", "checkNvd → matched");
190
+ const v = reduceVerdict(cite, [ev], checkCommit(cite));
191
+ eq(v.verdict, "true", "verdict true");
192
+ eq(v.matched_db, "nvd", "matched_db = nvd");
193
+ }
194
+ console.log(" …and a mocked NVD miss on a well-formed CVE → false:");
195
+ {
196
+ const cite = { id: "notacve", cve: "CVE-2099-00001" };
197
+ const nvd = { transport: "ok", found: false };
198
+ const ev = checkNvd(cite, nvd);
199
+ eq(ev.status, "id_unmatched", "checkNvd miss → id_unmatched");
200
+ const v = reduceVerdict(cite, [ev], checkCommit(cite));
201
+ eq(v.verdict, "false", "verdict false");
202
+ }
203
+ console.log(" …and an NVD outage → unresolvable, never false:");
204
+ {
205
+ const cite = { id: "outage", cve: "CVE-2021-44228" };
206
+ const ev = checkNvd(cite, { transport: "error" });
207
+ const v = reduceVerdict(cite, [ev], checkCommit(cite));
208
+ eq(v.verdict, "unresolvable", "NVD unreachable degrades to unresolvable");
209
+ }
210
+
211
+ // ── (6) short/truncated commit SHA → flagged ─────────────────────────────────
212
+ console.log("\n(6) short commit SHA → flagged (hygiene, advisory):");
213
+ {
214
+ const cite = {
215
+ id: "poc-exploit",
216
+ title: "Attention Is All You Need",
217
+ year: 2017,
218
+ commit: "a1b2c3d", // short (7 chars)
219
+ };
220
+ const flags = checkCommit(cite);
221
+ ok(flags.length === 1 && /short SHA/.test(flags[0]), "short SHA flagged");
222
+ // A hygiene flag must NOT by itself turn a good cite into a fabrication.
223
+ const matchedEv = {
224
+ db: "arxiv",
225
+ status: "matched",
226
+ matchedTitle: "Attention Is All You Need",
227
+ matchedYear: 2017,
228
+ };
229
+ const v = reduceVerdict(cite, [matchedEv], flags);
230
+ eq(v.verdict, "true", "verdict still true despite flag");
231
+ ok(v.flags && v.flags.length === 1, "flag surfaced on output");
232
+
233
+ const full = checkCommit({ id: "x", commit: "a".repeat(40) });
234
+ ok(full.length === 0, "full 40-char SHA not flagged");
235
+ const nonhex = checkCommit({ id: "y", commit: "zzz-not-a-sha" });
236
+ ok(
237
+ nonhex.length === 1 && /not a hex/.test(nonhex[0]),
238
+ "non-hex commit flagged",
239
+ );
240
+ }
241
+
242
+ // ── degrade-never-to-false: all resolvers unreachable → unresolvable ──────────
243
+ console.log(
244
+ "\n(extra) all resolvers unreachable → unresolvable (network ≠ fabrication):",
245
+ );
246
+ {
247
+ const cite = {
248
+ id: "netfail",
249
+ doi: "10.1145/3372297.3417231",
250
+ title: "Some Real Paper",
251
+ year: 2020,
252
+ };
253
+ const evs = ["crossref", "openalex", "semantic_scholar", "arxiv"].map((db) =>
254
+ classifyResolver(cite, { db, transport: "error" }),
255
+ );
256
+ const v = reduceVerdict(cite, evs, []);
257
+ eq(
258
+ v.verdict,
259
+ "unresolvable",
260
+ "network failure across the board → unresolvable",
261
+ );
262
+ }
263
+
264
+ // ── matched WINS even alongside an id miss on another DB ──────────────────────
265
+ console.log("\n(extra) a match on one DB WINS over an id-miss on another:");
266
+ {
267
+ const cite = {
268
+ id: "mixed",
269
+ doi: "10.1/x",
270
+ title: "Real Paper Title",
271
+ year: 2020,
272
+ };
273
+ const matched = {
274
+ db: "crossref",
275
+ transport: "ok",
276
+ query: "doi",
277
+ record: { title: "Real Paper Title", year: 2020 },
278
+ };
279
+ const miss = {
280
+ db: "semantic_scholar",
281
+ transport: "ok",
282
+ query: "doi",
283
+ record: null,
284
+ };
285
+ const evs = [classifyResolver(cite, matched), classifyResolver(cite, miss)];
286
+ const v = reduceVerdict(cite, evs, []);
287
+ eq(v.verdict, "true", "matched wins");
288
+ }
289
+
290
+ // ── .bib parser smoke ─────────────────────────────────────────────────────────
291
+ console.log("\n(extra) .bib parser best-effort extraction:");
292
+ {
293
+ const bib = `
294
+ @article{vaswani2017,
295
+ title = {Attention Is All You Need},
296
+ author = {Vaswani, Ashish and others},
297
+ year = {2017},
298
+ doi = {10.5555/3295222.3295349}
299
+ }
300
+ @inproceedings{he2016,
301
+ title="Deep Residual Learning",
302
+ year="2016",
303
+ archiveprefix={arXiv},
304
+ eprint={1512.03385}
305
+ }
306
+ `;
307
+ const cites = parseBib(bib);
308
+ eq(cites.length, 2, "parsed 2 entries");
309
+ eq(cites[0].doi, "10.5555/3295222.3295349", "entry 1 doi");
310
+ eq(cites[1].arxiv, "1512.03385", "entry 2 arxiv eprint");
311
+ }
312
+
313
+ // ═════════════════════════════════════════════════════════════════════════════
314
+ // REGRESSION TESTS for the adversarial-review fixes (M1, M2, M3, S1–S4).
315
+ // Each pins a bug that must never come back: the gate must NEVER call a REAL
316
+ // citation `false`.
317
+ // ═════════════════════════════════════════════════════════════════════════════
318
+
319
+ // ── M1: registry-404 + doi.org says EXISTS → unresolvable, NEVER false ────────
320
+ console.log(
321
+ "\n[M1] DOI 404s at every content registry but doi.org confirms it exists → unresolvable:",
322
+ );
323
+ {
324
+ // A DataCite/Zenodo (or fresh-2026) DOI: Crossref/OpenAlex/S2 have no metadata,
325
+ // yet doi.org handle responseCode 1 = the DOI genuinely resolves.
326
+ const cite = {
327
+ id: "zenodoArtifact",
328
+ doi: "10.5281/zenodo.21511018",
329
+ title: "My Paper's Reproduction Artifact",
330
+ year: 2026,
331
+ };
332
+ const registryMisses = [
333
+ { db: "crossref", transport: "ok", query: "doi", record: null },
334
+ { db: "openalex", transport: "ok", query: "doi", record: null },
335
+ { db: "semantic_scholar", transport: "ok", query: "doi", record: null },
336
+ ].map((r) => classifyResolver(cite, r));
337
+ const authority = classifyDoiAuthority({ transport: "ok", responseCode: 1 });
338
+ eq(
339
+ authority.status,
340
+ "authority_present",
341
+ "doi.org responseCode 1 → authority_present",
342
+ );
343
+ const v = reduceVerdict(cite, [...registryMisses, authority], []);
344
+ eq(
345
+ v.verdict,
346
+ "unresolvable",
347
+ "registry-404 + doi.org-exists → unresolvable, NOT false",
348
+ );
349
+ ok(
350
+ /doi\.org/i.test(v.reason) &&
351
+ !/fabricat/i.test(v.reason.split("NOT fabrication")[0]),
352
+ "reason: exists, metadata unindexed",
353
+ );
354
+ }
355
+ console.log(
356
+ " …and doi.org itself says NOT FOUND (responseCode 100) → false:",
357
+ );
358
+ {
359
+ const cite = {
360
+ id: "fabricatedDoi",
361
+ doi: "10.1000/definitelynotarealdoi99999",
362
+ title: "A Fabricated Paper",
363
+ year: 2023,
364
+ };
365
+ const registryMisses = [
366
+ { db: "crossref", transport: "ok", query: "doi", record: null },
367
+ { db: "openalex", transport: "ok", query: "doi", record: null },
368
+ ].map((r) => classifyResolver(cite, r));
369
+ const authority = classifyDoiAuthority({
370
+ transport: "ok",
371
+ responseCode: 100,
372
+ });
373
+ eq(
374
+ authority.status,
375
+ "authority_absent",
376
+ "doi.org responseCode 100 → authority_absent",
377
+ );
378
+ const v = reduceVerdict(cite, [...registryMisses, authority], []);
379
+ eq(
380
+ v.verdict,
381
+ "false",
382
+ "doi.org authority not-found → false (RESOLVED_TO_NOTHING)",
383
+ );
384
+ ok(/RESOLVED_TO_NOTHING/.test(v.reason), "reason names RESOLVED_TO_NOTHING");
385
+ }
386
+ console.log(
387
+ " …and doi.org UNREACHABLE with registry-404s → unresolvable (no disproof):",
388
+ );
389
+ {
390
+ const cite = {
391
+ id: "unknownAuthority",
392
+ doi: "10.1234/maybe.real",
393
+ title: "Possibly Real Paper",
394
+ year: 2025,
395
+ };
396
+ const registryMisses = [
397
+ { db: "crossref", transport: "ok", query: "doi", record: null },
398
+ { db: "openalex", transport: "ok", query: "doi", record: null },
399
+ ].map((r) => classifyResolver(cite, r));
400
+ const authority = classifyDoiAuthority({ transport: "error" });
401
+ const v = reduceVerdict(cite, [...registryMisses, authority], []);
402
+ eq(
403
+ v.verdict,
404
+ "unresolvable",
405
+ "registry-404 + doi.org unreachable → unresolvable, never false",
406
+ );
407
+ }
408
+
409
+ // ── M2: DOI_MISMATCH false positives from a brittle title test → now match ────
410
+ console.log(
411
+ "\n[M2] correct DOIs no longer flagged DOI_MISMATCH (subtitle / abbrev / drift):",
412
+ );
413
+ {
414
+ // (a) Crossref splits a subtitle into its own field; claimed title carries it.
415
+ const cite = {
416
+ id: "pokemon",
417
+ doi: "10.1/x",
418
+ title: "Gotta Catch 'Em All: A Multistage Framework for Honeypots",
419
+ year: 2020,
420
+ };
421
+ const rec = {
422
+ db: "crossref",
423
+ transport: "ok",
424
+ query: "doi",
425
+ record: {
426
+ title: "Gotta Catch 'Em All",
427
+ subtitle: "A Multistage Framework for Honeypots",
428
+ year: 2020,
429
+ },
430
+ };
431
+ const ev = classifyResolver(cite, rec);
432
+ eq(ev.status, "matched", "subtitle-split title → matched (not doi_mismatch)");
433
+ eq(reduceVerdict(cite, [ev], []).verdict, "true", "verdict true");
434
+ }
435
+ {
436
+ // subtitle test the OTHER way: record has only the main title, claimed has subtitle.
437
+ const cite = {
438
+ id: "sub2",
439
+ doi: "10.1/y",
440
+ title: "Attention Is All You Need: A Transformer Study",
441
+ year: 2017,
442
+ };
443
+ const rec = {
444
+ db: "openalex",
445
+ transport: "ok",
446
+ query: "doi",
447
+ record: { title: "Attention Is All You Need", year: 2017 },
448
+ };
449
+ eq(
450
+ classifyResolver(cite, rec).status,
451
+ "matched",
452
+ "claimed-has-subtitle, record-plain → matched via containment",
453
+ );
454
+ }
455
+ {
456
+ // (a') abbreviation / acronym expansion: "Deep CNNs" vs the expansion.
457
+ const cite = {
458
+ id: "cnn",
459
+ doi: "10.1/z",
460
+ title: "Deep CNNs for Image Recognition",
461
+ year: 2016,
462
+ };
463
+ const rec = {
464
+ db: "crossref",
465
+ transport: "ok",
466
+ query: "doi",
467
+ record: {
468
+ title: "Deep Convolutional Neural Networks for Image Recognition",
469
+ year: 2016,
470
+ },
471
+ };
472
+ eq(
473
+ classifyResolver(cite, rec).status,
474
+ "matched",
475
+ "acronym expansion (CNNs↔Conv.Neural Networks) → matched",
476
+ );
477
+ eq(
478
+ titleRelation(
479
+ "Deep CNNs for Image Recognition",
480
+ "Deep Convolutional Neural Networks for Image Recognition",
481
+ ),
482
+ "match",
483
+ "titleRelation acronym → match",
484
+ );
485
+ }
486
+ {
487
+ // preprint→published drift adds a subtitle clause.
488
+ const cite = {
489
+ id: "drift",
490
+ doi: "10.1/d",
491
+ title: "Prune the Timeline",
492
+ year: 2026,
493
+ };
494
+ const rec = {
495
+ db: "crossref",
496
+ transport: "ok",
497
+ query: "doi",
498
+ record: {
499
+ title: "Prune the Timeline: Fighting Combinatorial Explosion in LLM Code",
500
+ year: 2026,
501
+ },
502
+ };
503
+ eq(
504
+ classifyResolver(cite, rec).status,
505
+ "matched",
506
+ "preprint→published drift → matched",
507
+ );
508
+ }
509
+ {
510
+ // …but a GENUINELY different paper still trips DOI_MISMATCH → false (invariant holds).
511
+ const cite = {
512
+ id: "wrong",
513
+ doi: "10.1/w",
514
+ title: "A Formal Verification Framework for Autonomous Agents",
515
+ year: 2021,
516
+ };
517
+ const rec = {
518
+ db: "openalex",
519
+ transport: "ok",
520
+ query: "doi",
521
+ record: { title: "Photosynthesis in Deep-Sea Bacteria", year: 2009 },
522
+ };
523
+ eq(
524
+ classifyResolver(cite, rec).status,
525
+ "doi_mismatch",
526
+ "confidently-different titles → doi_mismatch preserved",
527
+ );
528
+ }
529
+
530
+ // ── M3: non-Latin / uncomparable title → NOT doi_mismatch, never false ────────
531
+ console.log(
532
+ "\n[M3] non-Latin (uncomparable) title against a Latin record → NOT a mismatch:",
533
+ );
534
+ {
535
+ const cite = {
536
+ id: "cyrillicDoi",
537
+ doi: "10.1/ru",
538
+ title: "Управление водными ресурсами",
539
+ year: 2019,
540
+ };
541
+ const rec = {
542
+ db: "crossref",
543
+ transport: "ok",
544
+ query: "doi",
545
+ record: { title: "Water Resource Management", year: 2019 },
546
+ };
547
+ const ev = classifyResolver(cite, rec);
548
+ eq(
549
+ ev.status,
550
+ "matched",
551
+ "Cyrillic-vs-Latin → incomparable → matched (id resolves), NOT doi_mismatch",
552
+ );
553
+ eq(
554
+ reduceVerdict(cite, [ev], []).verdict,
555
+ "true",
556
+ "verdict true — never false on an uncomparable title",
557
+ );
558
+ eq(
559
+ bestTitleRelation(
560
+ "Управление водными ресурсами",
561
+ candidateTitles(rec.record),
562
+ ),
563
+ "incomparable",
564
+ "bestTitleRelation → incomparable",
565
+ );
566
+ }
567
+
568
+ // ── S1: normalization preserves unicode letters → two DIFFERENT non-Latin ─────
569
+ // titles do NOT score 1.0 (a fabricated non-Latin cite can't pass as true).
570
+ console.log(
571
+ "\n[S1] different Cyrillic titles score LOW (was 1.0 via empty-normalization):",
572
+ );
573
+ {
574
+ const a = "Управление водными ресурсами Прибалхашья";
575
+ const b = "Совершенно другая научная работа о климате";
576
+ ok(
577
+ titleSimilarity(a, b) < 0.5,
578
+ `different Cyrillic titles < 0.5 (got ${titleSimilarity(a, b).toFixed(2)})`,
579
+ );
580
+ ok(titleSimilarity(a, a) > 0.99, "identical Cyrillic titles still ~1.0");
581
+ ok(
582
+ titleSimilarity("北京大学的研究", "上海的天气报告") < 0.6,
583
+ "different CJK titles score low",
584
+ );
585
+ eq(
586
+ titleRelation(a, b),
587
+ "different",
588
+ "different Cyrillic → 'different' (comparable now)",
589
+ );
590
+ // a title with NO letters/digits normalizes to "" → incomparable, NEVER 1.0
591
+ eq(
592
+ titleSimilarity("★★★ !!!", "▲▲▲ ???"),
593
+ 0,
594
+ "punctuation-only titles → 0, not 1.0",
595
+ );
596
+ }
597
+
598
+ // ── S2: arXiv "Error" sentinel entry is detected, not treated as a real paper ─
599
+ console.log(
600
+ "\n[S2] arXiv error-sentinel feed → no record (not an 'unrelated paper'):",
601
+ );
602
+ {
603
+ const errorFeed = `<feed><entry>
604
+ <id>http://arxiv.org/api/errors#incorrect_id_format_for_2399.99999</id>
605
+ <title>Error</title>
606
+ <summary>incorrect id format for 2399.99999</summary>
607
+ </entry></feed>`;
608
+ const recs = parseArxivFeed(errorFeed);
609
+ eq(
610
+ recs.length,
611
+ 0,
612
+ "error sentinel skipped → 0 records (id reads as not-found)",
613
+ );
614
+ // and a REAL feed still parses
615
+ const realFeed = `<feed><entry>
616
+ <id>http://arxiv.org/abs/1706.03762v5</id>
617
+ <title>Attention Is All You Need</title>
618
+ <published>2017-06-12T00:00:00Z</published>
619
+ </entry></feed>`;
620
+ const real = parseArxivFeed(realFeed);
621
+ eq(real.length, 1, "a real entry still parses");
622
+ eq(real[0].title, "Attention Is All You Need", "real title extracted");
623
+ }
624
+
625
+ // ── S3: commit SHA must be EXACTLY 40 hex (was: >40 hex accepted) ─────────────
626
+ console.log("\n[S3] commit SHA flagged unless exactly 40 hex chars:");
627
+ {
628
+ ok(
629
+ checkCommit({ id: "x", commit: "a".repeat(40) }).length === 0,
630
+ "exactly 40 hex → OK",
631
+ );
632
+ const long = checkCommit({ id: "x", commit: "a".repeat(41) });
633
+ ok(
634
+ long.length === 1 && />40|not a valid/.test(long[0]),
635
+ "41 hex chars → flagged (was wrongly accepted)",
636
+ );
637
+ const longer = checkCommit({ id: "x", commit: "a".repeat(64) });
638
+ ok(longer.length === 1, "64 hex chars → flagged");
639
+ ok(
640
+ checkCommit({ id: "x", commit: "a1b2c3d" }).length === 1,
641
+ "short SHA still flagged",
642
+ );
643
+ }
644
+
645
+ // ── S4: .bib parser — booktitle/title, brace-protected acronyms, one-line & indented ─
646
+ console.log("\n[S4] .bib parser edge cases:");
647
+ {
648
+ // (a) booktitle appears BEFORE title — field('title') must not grab booktitle.
649
+ const bibA = `@inproceedings{k1,
650
+ booktitle = {Proceedings of the Booktitle Conference},
651
+ title = {The Real Paper Title},
652
+ year = {2021}
653
+ }`;
654
+ const a = parseBib(bibA)[0];
655
+ eq(a.title, "The Real Paper Title", "title anchored — not booktitle");
656
+
657
+ // (b) brace-protected acronym must not truncate at the inner brace.
658
+ const bibB = `@article{k2,
659
+ title = {Evaluating {LLM}-based Systems for Code Review},
660
+ year = {2026}
661
+ }`;
662
+ eq(
663
+ parseBib(bibB)[0].title,
664
+ "Evaluating LLM-based Systems for Code Review",
665
+ "brace-protected acronym kept whole",
666
+ );
667
+
668
+ // (c1) one-line entry (no \n} at column 0).
669
+ const bibC1 = `@article{k3, title = {A One Line Entry}, year = {2020}, doi = {10.1/abc}}`;
670
+ const c1 = parseBib(bibC1)[0];
671
+ eq(c1.title, "A One Line Entry", "one-line entry title parsed");
672
+ eq(c1.doi, "10.1/abc", "one-line entry doi parsed");
673
+
674
+ // (c2) indented closing brace `\n }`.
675
+ const bibC2 = `@article{k4,
676
+ title = {Indented Closing Brace},
677
+ year = {2019}
678
+ }`;
679
+ eq(
680
+ parseBib(bibC2)[0].title,
681
+ "Indented Closing Brace",
682
+ "indented-closing-brace entry parsed",
683
+ );
684
+
685
+ // all four parse together, in sequence (balanced-brace scan resumes correctly).
686
+ eq(
687
+ parseBib(bibA + "\n" + bibB + "\n" + bibC1 + "\n" + bibC2).length,
688
+ 4,
689
+ "all 4 entries parsed in sequence",
690
+ );
691
+ }
692
+
693
+ // ── identifier normalization (parser must not manufacture a false `false`) ──────
694
+ // Fable re-check MUST-FIX: doi:/arXiv: prefixes + a bib-swallowed trailing period
695
+ // were reaching the authority gate verbatim and flipping REAL works to `false`.
696
+ {
697
+ eq(
698
+ normalizeDoi("10.1145/3576915.3623218."),
699
+ "10.1145/3576915.3623218",
700
+ "DOI trailing period stripped",
701
+ );
702
+ eq(
703
+ normalizeDoi("doi:10.1145/1234.5678"),
704
+ "10.1145/1234.5678",
705
+ "DOI doi: prefix stripped",
706
+ );
707
+ eq(
708
+ normalizeDoi("https://doi.org/10.1/x"),
709
+ "10.1/x",
710
+ "DOI resolver-URL prefix stripped",
711
+ );
712
+ eq(normalizeDoi("10.1/x"), "10.1/x", "clean DOI untouched");
713
+ eq(
714
+ normalizeArxiv("arXiv:1706.03762"),
715
+ "1706.03762",
716
+ "arXiv: prefix stripped",
717
+ );
718
+ eq(normalizeArxiv("1706.03762"), "1706.03762", "clean arXiv id untouched");
719
+ const n = normalizeIdentifiers({
720
+ id: "x",
721
+ doi: "doi:10.1/y.",
722
+ arxiv: "arXiv:1234.5678",
723
+ });
724
+ eq(n.doi, "10.1/y", "normalizeIdentifiers cleans doi");
725
+ eq(n.arxiv, "1234.5678", "normalizeIdentifiers cleans arxiv");
726
+ // the exact live-reproduced break: a \bibitem DOI with a swallowed sentence period.
727
+ const bib = `\\begin{thebibliography}{9}
728
+ \\bibitem{ccs23} A. Author. Title. In Proc. CCS. doi:10.1145/3576915.3623218. 2023.
729
+ \\end{thebibliography}`;
730
+ eq(
731
+ parseBib(bib)[0].doi,
732
+ "10.1145/3576915.3623218",
733
+ "bibitem DOI trailing period not swallowed",
734
+ );
735
+ }