paperlint 3.1.0 → 3.1.2

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 (311) hide show
  1. package/.github/workflows/ci.yml +28 -41
  2. package/.github/workflows/dependabot-automerge.yml +1 -1
  3. package/CLAUDE.md +21 -105
  4. package/CONTRIBUTING.md +23 -25
  5. package/README.md +11 -7
  6. package/bin/paperlint.test.mjs +45 -0
  7. package/dist/adapters/node/files.io.d.ts +2 -0
  8. package/dist/adapters/node/files.io.d.ts.map +1 -1
  9. package/dist/adapters/node/files.io.js +3 -2
  10. package/dist/adapters/node/files.io.js.map +1 -1
  11. package/dist/adapters/pdfjs/fill.d.ts +15 -4
  12. package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
  13. package/dist/adapters/pdfjs/fill.js +31 -21
  14. package/dist/adapters/pdfjs/fill.js.map +1 -1
  15. package/dist/adapters/references/clock.io.d.ts +5 -0
  16. package/dist/adapters/references/clock.io.d.ts.map +1 -0
  17. package/dist/adapters/references/clock.io.js +7 -0
  18. package/dist/adapters/references/clock.io.js.map +1 -0
  19. package/dist/adapters/references/index.d.ts +24 -2
  20. package/dist/adapters/references/index.d.ts.map +1 -1
  21. package/dist/adapters/references/index.js +131 -12
  22. package/dist/adapters/references/index.js.map +1 -1
  23. package/dist/adapters/references/pool.d.ts +8 -0
  24. package/dist/adapters/references/pool.d.ts.map +1 -0
  25. package/dist/adapters/references/pool.js +19 -0
  26. package/dist/adapters/references/pool.js.map +1 -0
  27. package/dist/build.d.ts +14 -2
  28. package/dist/build.d.ts.map +1 -1
  29. package/dist/build.js +17 -11
  30. package/dist/build.js.map +1 -1
  31. package/dist/cli.d.ts +27 -1
  32. package/dist/cli.d.ts.map +1 -1
  33. package/dist/cli.js +71 -55
  34. package/dist/cli.js.map +1 -1
  35. package/dist/doctor.d.ts +27 -1
  36. package/dist/doctor.d.ts.map +1 -1
  37. package/dist/doctor.js +4 -3
  38. package/dist/doctor.js.map +1 -1
  39. package/dist/domain/lookup-cache.d.ts +75 -0
  40. package/dist/domain/lookup-cache.d.ts.map +1 -0
  41. package/dist/domain/lookup-cache.js +137 -0
  42. package/dist/domain/lookup-cache.js.map +1 -0
  43. package/dist/domain/result.d.ts +0 -2
  44. package/dist/domain/result.d.ts.map +1 -1
  45. package/dist/domain/result.js +0 -4
  46. package/dist/domain/result.js.map +1 -1
  47. package/dist/hooks-settings.d.ts +8 -5
  48. package/dist/hooks-settings.d.ts.map +1 -1
  49. package/dist/hooks-settings.js +32 -32
  50. package/dist/hooks-settings.js.map +1 -1
  51. package/dist/init.d.ts +8 -0
  52. package/dist/init.d.ts.map +1 -1
  53. package/dist/init.js +3 -3
  54. package/dist/init.js.map +1 -1
  55. package/dist/latex-log.d.ts +0 -2
  56. package/dist/latex-log.d.ts.map +1 -1
  57. package/dist/latex-log.js +7 -13
  58. package/dist/latex-log.js.map +1 -1
  59. package/dist/link-skills.d.ts +15 -2
  60. package/dist/link-skills.d.ts.map +1 -1
  61. package/dist/link-skills.js +36 -20
  62. package/dist/link-skills.js.map +1 -1
  63. package/dist/paper-files.d.ts.map +1 -1
  64. package/dist/paper-files.js +6 -4
  65. package/dist/paper-files.js.map +1 -1
  66. package/dist/pdf-facts.d.ts +11 -0
  67. package/dist/pdf-facts.d.ts.map +1 -1
  68. package/dist/pdf-facts.js +16 -18
  69. package/dist/pdf-facts.js.map +1 -1
  70. package/dist/pdf-geometry.js +1 -1
  71. package/dist/pdf-geometry.js.map +1 -1
  72. package/dist/ports/check-references.d.ts +12 -2
  73. package/dist/ports/check-references.d.ts.map +1 -1
  74. package/dist/ports/check-references.js +0 -5
  75. package/dist/ports/check-references.js.map +1 -1
  76. package/dist/presets.d.ts +2 -0
  77. package/dist/presets.d.ts.map +1 -1
  78. package/dist/presets.js +3 -1
  79. package/dist/presets.js.map +1 -1
  80. package/dist/references.d.ts +11 -3
  81. package/dist/references.d.ts.map +1 -1
  82. package/dist/references.js +63 -15
  83. package/dist/references.js.map +1 -1
  84. package/dist/tex-requirements.d.ts +8 -0
  85. package/dist/tex-requirements.d.ts.map +1 -1
  86. package/dist/tex-requirements.js +6 -5
  87. package/dist/tex-requirements.js.map +1 -1
  88. package/dist/toolchain.d.ts +3 -2
  89. package/dist/toolchain.d.ts.map +1 -1
  90. package/dist/toolchain.js +4 -5
  91. package/dist/toolchain.js.map +1 -1
  92. package/dist/venue-rules.d.ts.map +1 -1
  93. package/dist/venue-rules.js +11 -9
  94. package/dist/venue-rules.js.map +1 -1
  95. package/docs/prior-art/README.md +11 -10
  96. package/docs/prior-art/citation-checkers.md +174 -0
  97. package/docs/prior-art/test-tooling.md +7 -30
  98. package/docs/references.md +126 -0
  99. package/docs/testing.md +118 -0
  100. package/docs/toolchain.md +6 -4
  101. package/eslint-rules/install-path-literals.harness.mjs +17 -1
  102. package/eslint-rules/latex-language.harness.mjs +3 -3
  103. package/eslint-rules/latex-language.mjs +12 -9
  104. package/eslint-rules/latex-language.test.mjs +200 -0
  105. package/eslint-rules/paper-research-question.harness.mjs +73 -7
  106. package/eslint-rules/paper-stages.harness.mjs +9 -14
  107. package/eslint-rules/paper-stages.test.mjs +135 -0
  108. package/eslint-rules/paper-typography.mjs +9 -2
  109. package/eslint-rules/paper-typography.test.mjs +146 -1
  110. package/eslint-rules/pdf-last-page-balance.harness.mjs +63 -7
  111. package/eslint-rules/review-frontmatter.mjs +2 -1
  112. package/eslint-rules/review-frontmatter.test.mjs +37 -1
  113. package/eslint-rules/temp-root-realpath.harness.mjs +49 -0
  114. package/eslint-rules/tex-build.mjs +13 -13
  115. package/eslint.config.mjs +6 -2
  116. package/hooks/hooks.harness.mjs +6 -11
  117. package/hooks/paper-edit-guard.hook.mjs +2 -2
  118. package/hooks/paper-edit-guard.test.mjs +61 -0
  119. package/hooks/paper-skills-nudge.hook.mjs +2 -2
  120. package/hooks/paper-skills-nudge.test.mjs +38 -0
  121. package/hooks/paper-status-gates.test.mjs +42 -0
  122. package/lib/agent-cli-version.harness.mjs +7 -0
  123. package/lib/check.d.mts +17 -0
  124. package/lib/check.mjs +50 -0
  125. package/lib/check.test.ts +67 -0
  126. package/lib/markdown.mjs +16 -69
  127. package/lib/markdown.test.mjs +246 -0
  128. package/lib/paper-config.harness.mjs +28 -9
  129. package/lib/paper-config.mjs +7 -2
  130. package/lib/skill-checks.mjs +18 -46
  131. package/lib/skill-checks.test.mjs +297 -0
  132. package/lib/skill-corpus.mjs +11 -13
  133. package/lib/skill-corpus.test.mjs +54 -0
  134. package/lib/skill-trigger-cases.harness.mjs +57 -4
  135. package/package.json +6 -5
  136. package/scripts/check.harness.mjs +4 -10
  137. package/scripts/check.mjs +45 -42
  138. package/scripts/check.test.mjs +104 -0
  139. package/scripts/coverage-config.test.ts +406 -0
  140. package/scripts/eslint-report-guard.test.mjs +48 -0
  141. package/scripts/harness-api.frozen.json +0 -2
  142. package/scripts/layer-legacy-frozen.d.mts +9 -1
  143. package/scripts/layer-legacy-frozen.mjs +28 -11
  144. package/scripts/layer-legacy-frozen.test.ts +143 -1
  145. package/scripts/rules-are-content-only.harness.mjs +3 -11
  146. package/scripts/rules-are-content-only.mjs +29 -19
  147. package/scripts/rules-are-content-only.test.mjs +61 -0
  148. package/scripts/rules-see-files.mjs +11 -9
  149. package/scripts/rules-see-files.test.mjs +71 -0
  150. package/scripts/test.mjs +44 -0
  151. package/scripts/test.test.mjs +52 -0
  152. package/skills/grade-paper-writing/__snapshots__/prose-lint.test.mjs.snap +57 -0
  153. package/skills/grade-paper-writing/prose-lint.mjs +31 -12
  154. package/skills/grade-paper-writing/prose-lint.test.mjs +153 -0
  155. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +3 -3
  156. package/skills/paper-pipeline/references/artifact-checklist.md +7 -7
  157. package/skills/paper-pipeline/scripts/README.md +13 -132
  158. package/skills/paper-pipeline/scripts/announce.test.mjs +102 -0
  159. package/skills/paper-pipeline/scripts/artifact-coverage.test.mjs +86 -0
  160. package/skills/paper-pipeline/scripts/consumer.d.mts +8 -1
  161. package/skills/paper-pipeline/scripts/consumer.harness.mjs +3 -3
  162. package/skills/paper-pipeline/scripts/consumer.mjs +13 -25
  163. package/skills/paper-pipeline/scripts/consumer.test.mjs +113 -0
  164. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +9 -4
  165. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +380 -0
  166. package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +71 -0
  167. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +4 -4
  168. package/skills/paper-pipeline/scripts/generated-code.mjs +3 -2
  169. package/skills/paper-pipeline/scripts/generated-code.test.mjs +62 -0
  170. package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +302 -0
  171. package/skills/paper-pipeline/scripts/ledger.mjs +1 -1
  172. package/skills/paper-pipeline/scripts/{ledger.selftest.mjs → ledger.test.mjs} +44 -76
  173. package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +474 -0
  174. package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -63
  175. package/skills/paper-pipeline/scripts/pipeline-check.mjs +37 -21
  176. package/skills/paper-pipeline/scripts/pipeline-edges.mjs +2 -2
  177. package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +73 -0
  178. package/skills/paper-pipeline/scripts/population-map.harness.mjs +4 -47
  179. package/skills/paper-pipeline/scripts/population-map.mjs +12 -2
  180. package/skills/paper-pipeline/scripts/population-map.test.mjs +154 -0
  181. package/skills/paper-pipeline/scripts/provenance.harness.mjs +26 -0
  182. package/skills/paper-pipeline/scripts/round-diff.harness.mjs +1 -2
  183. package/skills/paper-pipeline/scripts/round-diff.mjs +13 -6
  184. package/skills/paper-pipeline/scripts/round-diff.test.mjs +159 -0
  185. package/skills/paper-pipeline/scripts/run-mechanical.mjs +30 -6
  186. package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +468 -0
  187. package/skills/paper-pipeline/scripts/status.mjs +2 -2
  188. package/skills/paper-pipeline/scripts/status.test.mjs +232 -0
  189. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +2 -2
  190. package/skills/render-paper/checkers-requirements.txt +30 -19
  191. package/skills/render-paper/ensure-checkers.sh +20 -4
  192. package/skills/render-paper/extract-pdf-facts.harness.mjs +56 -7
  193. package/skills/tighten-paper/__snapshots__/structure.test.mjs.snap +133 -0
  194. package/skills/tighten-paper/structure.test.mjs +116 -0
  195. package/skills/verify-citations/SKILL.md +30 -14
  196. package/skills/verify-citations/SKILL.md.spec.ts +29 -13
  197. package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +253 -0
  198. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +274 -0
  199. package/skills/verify-citations/scripts/bib-authors.cli.test.mjs +178 -0
  200. package/skills/verify-citations/scripts/bib-authors.mjs +6 -6
  201. package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
  202. package/skills/verify-citations/scripts/verify-cites.mjs +360 -215
  203. package/skills/verify-citations/scripts/verify-cites.net.test.mjs +588 -0
  204. package/skills/verify-citations/scripts/verify-cites.test.mjs +151 -5
  205. package/src/adapters/banal/failure.test.ts +70 -1
  206. package/src/adapters/banal/index.test.ts +16 -0
  207. package/src/adapters/banal/output.test.ts +5 -0
  208. package/src/adapters/curl/download.test.ts +43 -1
  209. package/src/adapters/memory/index.test.ts +25 -0
  210. package/src/adapters/node/files.io.ts +3 -2
  211. package/src/adapters/node/files.test.ts +13 -1
  212. package/src/adapters/node/process.test.ts +38 -0
  213. package/src/adapters/pdfjs/fill.test.ts +34 -0
  214. package/src/adapters/pdfjs/fill.ts +44 -25
  215. package/src/adapters/references/clock.io.ts +8 -0
  216. package/src/adapters/references/index.test.ts +560 -0
  217. package/src/adapters/references/index.ts +203 -19
  218. package/src/adapters/references/pool.test.ts +31 -0
  219. package/src/adapters/references/pool.ts +24 -0
  220. package/src/build-engine.harness.mjs +3 -7
  221. package/src/build-engine.test.ts +137 -0
  222. package/src/build.harness.mjs +5 -9
  223. package/src/build.test.ts +362 -0
  224. package/src/build.ts +23 -12
  225. package/src/cli.harness.mjs +10 -17
  226. package/src/cli.test.ts +584 -0
  227. package/src/cli.ts +112 -66
  228. package/src/doctor.harness.mjs +3 -8
  229. package/src/doctor.test.ts +64 -0
  230. package/src/doctor.ts +12 -4
  231. package/src/domain/lookup-cache.test.ts +178 -0
  232. package/src/domain/lookup-cache.ts +202 -0
  233. package/src/domain/result.test.ts +4 -0
  234. package/src/domain/result.ts +0 -5
  235. package/src/engine.harness.mjs +27 -7
  236. package/src/facts-file.test.ts +110 -0
  237. package/src/hooks-settings.harness.mjs +43 -13
  238. package/src/hooks-settings.ts +41 -35
  239. package/src/init.test.ts +308 -5
  240. package/src/init.ts +3 -3
  241. package/src/latex-log.harness.mjs +32 -7
  242. package/src/latex-log.ts +6 -13
  243. package/src/latex-loop.harness.mjs +8 -7
  244. package/src/link-skills.harness.mjs +3 -8
  245. package/src/link-skills.test.ts +105 -0
  246. package/src/link-skills.ts +75 -21
  247. package/src/new-paper.harness.mjs +3 -7
  248. package/src/new-paper.test.ts +31 -1
  249. package/src/paper-files.test.ts +12 -0
  250. package/src/paper-files.ts +9 -5
  251. package/src/paper-settings.test.ts +20 -0
  252. package/src/pdf-facts.harness.mjs +3 -6
  253. package/src/pdf-facts.test.ts +144 -0
  254. package/src/pdf-facts.ts +17 -19
  255. package/src/pdf-geometry.harness.mjs +3 -7
  256. package/src/pdf-geometry.ts +1 -1
  257. package/src/ports/check-references.ts +16 -2
  258. package/src/presets.test.ts +2 -0
  259. package/src/presets.ts +7 -1
  260. package/src/references.test.ts +257 -10
  261. package/src/references.ts +93 -17
  262. package/src/rules-config.harness.mjs +10 -8
  263. package/src/structure.harness.mjs +3 -7
  264. package/src/tex-requirements.harness.mjs +34 -7
  265. package/src/tex-requirements.ts +12 -6
  266. package/src/toolchain.harness.mjs +3 -7
  267. package/src/toolchain.test.ts +280 -0
  268. package/src/toolchain.ts +8 -7
  269. package/src/venue-rules.test.ts +60 -0
  270. package/src/venue-rules.ts +13 -11
  271. package/action.mutations.mjs +0 -162
  272. package/eslint-rules/install-path-literals.mutations.mjs +0 -62
  273. package/eslint-rules/latex-language.mutations.mjs +0 -196
  274. package/eslint-rules/paper-research-question.mutations.mjs +0 -127
  275. package/eslint-rules/paper-stages.mutations.mjs +0 -81
  276. package/eslint-rules/papers.mutations.mjs +0 -186
  277. package/eslint-rules/temp-root-realpath.mutations.mjs +0 -99
  278. package/eslint-rules/tex-build.mutations.mjs +0 -258
  279. package/hooks/hooks.mutations.mjs +0 -337
  280. package/lib/agent-cli-version.mutations.mjs +0 -109
  281. package/lib/mutation-driver.harness.mjs +0 -227
  282. package/lib/mutation-driver.mjs +0 -397
  283. package/lib/mutation-driver.mutations.mjs +0 -68
  284. package/lib/paper-config.mutations.mjs +0 -143
  285. package/lib/skill-trigger-cases.mutations.mjs +0 -65
  286. package/scripts/check.mutations.mjs +0 -110
  287. package/scripts/exclusive.mjs +0 -138
  288. package/scripts/mutation-batteries-frozen.harness.mjs +0 -204
  289. package/scripts/mutation-batteries-frozen.mjs +0 -238
  290. package/scripts/mutation-batteries.frozen.json +0 -108
  291. package/scripts/rules-are-content-only.mutations.mjs +0 -81
  292. package/scripts/rules-see-files.mutations.mjs +0 -131
  293. package/scripts/run-mutations.mjs +0 -100
  294. package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +0 -218
  295. package/skills/paper-pipeline/scripts/consumer.mutations.mjs +0 -190
  296. package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +0 -54
  297. package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +0 -254
  298. package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +0 -54
  299. package/skills/paper-pipeline/scripts/population-map.mutations.mjs +0 -65
  300. package/skills/paper-pipeline/scripts/population-map.selftest.mjs +0 -122
  301. package/skills/paper-pipeline/scripts/provenance.mutations.mjs +0 -59
  302. package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +0 -276
  303. package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +0 -65
  304. package/skills/skill-contract.mutations.mjs +0 -179
  305. package/src/build.mutations.mjs +0 -196
  306. package/src/cli.mutations.mjs +0 -414
  307. package/src/doctor.mutations.mjs +0 -175
  308. package/src/hooks-settings.mutations.mjs +0 -99
  309. package/src/link-skills.mutations.mjs +0 -140
  310. package/src/new-paper.mutations.mjs +0 -65
  311. package/src/structure.mutations.mjs +0 -83
@@ -0,0 +1,116 @@
1
+ /**
2
+ * structure.mjs — the section inventory the tighten-paper skill prints for a human to read. No
3
+ * threshold, no finding: the table IS the product, so the whole printed table is compared.
4
+ */
5
+ import assert from "node:assert/strict";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { expect, test } from "vitest";
9
+ import { runNode, useTempDir, writeTree } from "../../test/support.mjs";
10
+
11
+ const SCRIPT = join(dirname(fileURLToPath(import.meta.url)), "structure.mjs");
12
+ const root = useTempDir("structure-");
13
+ const words = (n) => Array.from({ length: n }, (_, i) => `w${i}`).join(" ");
14
+
15
+ writeTree(root, {
16
+ // Every row shape: a sub-section before any section, carries notes with a score and a verdict,
17
+ // with only one of them, with neither, a numbered section with no note, and a free half — after
18
+ // Limitations, between References and the Appendix — larger than the body.
19
+ "full.md": `---
20
+ title: x
21
+ ---
22
+ Preamble before any heading.
23
+
24
+ ### Orphan sub
25
+ ${words(3)}
26
+
27
+ <!-- carries: the claim · score: 7/10 · verdict: keep -->
28
+ ## 1 Introduction
29
+ ${words(10)}
30
+
31
+ <!-- carries: a sub-point -->
32
+ ### 1.1 Detail
33
+ ${words(4)}
34
+
35
+ ### 1.2 Bare sub
36
+ ${words(2)}
37
+
38
+ ## 2 Method
39
+ ${words(6)}
40
+
41
+ <!-- carries: only a verdict · verdict: CUT -->
42
+ ## 3 Results
43
+ ${words(5)}
44
+
45
+ <!-- carries: score only · score: 4 -->
46
+ ## Discussion
47
+ ${words(3)}
48
+
49
+ ## Limitations
50
+ ${words(40)}
51
+
52
+ <!-- carries: why the scope note -->
53
+ ### Scope
54
+ ${words(5)}
55
+
56
+ ## References
57
+ [1] Not counted.
58
+
59
+ <!-- carries: the raw tables -->
60
+ ## Appendix A
61
+ ${words(30)}
62
+ `,
63
+ // References come first, so the free half runs from it to the end; no appendix.
64
+ "refs-first.md": `## 1 Intro\n${words(8)}\n\n## References\n[1] x\n\n## Ethical note\n${words(2)}\n`,
65
+ // Limitations then References and no appendix: the free half stops at References.
66
+ "no-appendix.md": `## 1 Intro\n${words(8)}\n\n## Limitations\n${words(3)}\n\n## References\n[1] x\n`,
67
+ // No free half at all.
68
+ // ...and a section with no words at all.
69
+ "body-only.md": `## 1 Intro\n${words(8)}\n\n## 2 Rest\n${words(4)}\n\n## 3 Empty\n`,
70
+ });
71
+
72
+ const run = (...args) => {
73
+ const r = runNode(
74
+ SCRIPT,
75
+ args.map((a) => (a.startsWith("--") ? a : join(root, a))),
76
+ );
77
+ return { ...r, stdout: r.stdout.replaceAll(root, "<root>") };
78
+ };
79
+
80
+ test("no file: the usage line, exit 0", () => {
81
+ assert.deepEqual(run(), {
82
+ status: 0,
83
+ stdout: "",
84
+ stderr:
85
+ "usage: node structure.mjs <paper.md> [--section=N] (--flags-only is accepted and stays silent)\n",
86
+ });
87
+ });
88
+
89
+ test("--flags-only stays silent and exits 0 — every check moved to ESLint", () => {
90
+ assert.deepEqual(run("--flags-only", "full.md"), {
91
+ status: 0,
92
+ stdout: "",
93
+ stderr: "",
94
+ });
95
+ });
96
+
97
+ test.each(["full.md", "refs-first.md", "no-appendix.md", "body-only.md"])(
98
+ "the inventory of %s",
99
+ (file) => {
100
+ const r = run(file);
101
+ assert.deepEqual(
102
+ { status: r.status, stderr: r.stderr },
103
+ { status: 0, stderr: "" },
104
+ );
105
+ expect(r.stdout).toMatchSnapshot();
106
+ },
107
+ );
108
+
109
+ test("--section=1 keeps section 1 and its sub-sections, and nothing before it", () => {
110
+ const r = run("full.md", "--section=1");
111
+ assert.deepEqual(
112
+ { status: r.status, stderr: r.stderr },
113
+ { status: 0, stderr: "" },
114
+ );
115
+ expect(r.stdout).toMatchSnapshot();
116
+ });
@@ -4,7 +4,7 @@ description: Verify every citation is a real work with correct metadata, and tha
4
4
  allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, WebSearch, WebFetch, Agent]
5
5
  ---
6
6
 
7
- <!-- vigiles:sha256:2bb9f6f1233094cf compiled from skills/verify-citations/SKILL.md.spec.ts -->
7
+ <!-- vigiles:sha256:3a7e10662d0084c4 compiled from skills/verify-citations/SKILL.md.spec.ts -->
8
8
 
9
9
  # verify-citations — every cite real, the delta explicit
10
10
 
@@ -133,17 +133,26 @@ node scripts/verify-cites.mjs cites.json --offline # no network (everything de
133
133
  ```
134
134
  It prints per-cite `{id, verdict, reason, matched_db, matched_title?, flags?}` + a summary, and **exits 1 iff
135
135
  any `false` (fabrication)** — `unresolvable` alone is advisory and does NOT fail the gate, matching the
136
- narrowed-false philosophy. API responses are cached to `scripts/.cite-cache.json` (gitignored) so re-runs are
137
- cheap and deterministic.
136
+ narrowed-false philosophy. API responses are cached to `~/.cache/paperlint/cite-cache.json`
137
+ (`VERIFY_CITES_CACHE` overrides) so re-runs are cheap and deterministic — outside the installed package,
138
+ which `npm ci` wipes.
138
139
 
139
- ## 5a. Both checks run on every `paperlint build`
140
+ ## 5a. Both checks run on every `paperlint build` — from a committed cache
140
141
 
141
142
  `npx paperlint build` runs `verify-cites` and `bib-authors` over the paper's bibliography after the PDF is
142
143
  built and records the verdicts, with the SHA-256 of the bibliography it checked, in
143
144
  `<paper>/_build/references.json`. `paperlint lint` reads that record offline: `paper/cite-exists` and
144
145
  `paper/author-list` report a failing entry on its own line, `paper/refs-fresh` says when the bibliography
145
146
  changed since, and `paper/refs-checked` warns when nothing was recorded or the build had no network. The
146
- step never fails the build. Run the scripts by hand (below) to read a single verdict in full.
147
+ step never fails the build.
148
+
149
+ What the services ANSWERED is kept in `<paper>/repro/references-cache.json` — **commit it**. A build asks
150
+ only the questions that file cannot answer (a new or edited entry, or an answer older than 30 days), so an
151
+ unchanged bibliography builds with no network at all, in CI too, until its answers age out. It holds responses, never verdicts: the verdicts are derived again on
152
+ every build. A service that refuses (429, HTML instead of JSON, a timeout) is not asked again for the rest of the
153
+ build (the requests already in flight still land); the entries it would have answered say `not asked: <service>: <reason>`, and DBLP's become
154
+ `authors: unchecked`. Answers are asked again after 30 days; to refresh one sooner, delete its entry, or delete the file. Details:
155
+ `docs/references.md` in the package. Run the scripts by hand (below) to read a single verdict in full.
147
156
 
148
157
  ## 5b. The author-list gate — `scripts/bib-authors.mjs`
149
158
 
@@ -179,11 +188,18 @@ difference is real and not a metadata glitch. Entries that themselves declare a
179
188
  scope (a preprint entry is allowed to carry preprint metadata).
180
189
 
181
190
  ⚠️ **Discovery credit, and why it is NOT a dependency.** The class was found by running **rebiber**
182
- (`yuchenlin/rebiber`), which rewrites bib entries into their DBLP records. We deliberately do not depend on
183
- it: its install fights modern setuptools (the `bibtexparser` wheel dies with `AttributeError:
184
- install_layout`), and its *output* replaces our entries with DBLP's very long official booktitles — wrong for
185
- a page-limited paper. We needed the **comparison**, not the rewrite, and that is one fetch with zero deps.
186
- Note also that live DBLP beat rebiber's bundled dump: `raji2021benchmark` was found only by the live query.
191
+ (`yuchenlin/rebiber`), which rewrites bib entries into their DBLP records. Re-measured 2026-09-27 on rebiber
192
+ 1.4.0 (GitHub commit `b917e36`, the pin in `render-paper/checkers-requirements.txt`; the PyPI package is
193
+ a 2021 release upstream disowns):
194
+ - **Install: no longer broken, but heavy.** It installs in 36 s in a clean venv (bibtexparser 2.0.1 wheel,
195
+ rebiber built from source) — and carries **409 MB** of bundled venue data.
196
+ - **Rewrites: now optional.** `--dry-run --report` previews and leaves the file untouched; `-s True` shortens
197
+ venues (NAACL 2019 becomes `Proc. of NAACL-HLT` instead of a 160-character booktitle).
198
+ - **Bundled data still misses what live DBLP finds.** `raji2021benchmark` stayed an arXiv entry, local
199
+ index and `--live-lookup` alike, as it did in August.
200
+
201
+ So the reason not to depend on it is now scope, not breakage: we need the **comparison** of an author list
202
+ against the published record, not a rewrite of the entry, and that is one DBLP query with zero dependencies.
187
203
 
188
204
  🔴 **Two design decisions that are load-bearing, both learned by measurement the same day:**
189
205
  1. **Compare surname SEQUENCES, nothing else.** A first cut reported 13 differences of which **11 were
@@ -259,7 +275,7 @@ the same one MedSci Skills (arXiv:2606.09500) closes with `verify_refs.py`.
259
275
  | year | ±1 on a title search | exact, and only against the record that dates the cited version |
260
276
  | preprint→published | not asked | arXiv `journal_ref`/`doi` present and no venue named → say so |
261
277
  | what it could not check | folded into `unresolvable` | **enumerated, one rule per reason, never counted as verified** |
262
- | tests | `verify-cites.test.mjs` — not a `*.harness.mjs`, so `npx vigiles test` never runs it | `eslint-rules/ref-facts.harness.mjs` + `.claude/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs`, plus `eslint-rules/ref-facts.mutations.mjs` |
278
+ | tests | `verify-cites.test.mjs` — not a `*.harness.mjs`, so `npx vigiles test` never runs it | `src/references.test.ts` (the `refs/*` rules in `src/reference-rules.ts`) + `skills/paper-pipeline/scripts/extract-ref-facts.test.mjs` and `.harness.mjs`, under the package's 100% line and branch coverage gate |
263
279
 
264
280
  **The uncovered set is the point.** A reference with no resolvable identifier is a FINDING here
265
281
  (`refs/no-identifier`), and every partial comparison is another (`refs/partial-check`, one messageId per
@@ -274,9 +290,9 @@ and its `.bib` regex required a closing `}` on its own line, which neither real
274
290
  references`). The `.bib` half now goes through `@retorquere/bibtex-parser`. Two of the three papers are
275
291
  therefore being checked for the first time.
276
292
 
277
- **Every check is watched failing.** `node eslint-rules/ref-facts.mutations.mjs` neuters each rule in turn
278
- and requires the harness to go red at the named case — 21 mutations, all killed, and three of them were
279
- findings about the TEST rather than the rule. Run it after any edit to the rules.
293
+ **Every check is exercised.** `src/references.test.ts` drives the `refs/*` rules, and `npm run coverage`
294
+ fails CI if any line or branch of them goes unexercised. Run `npx vitest run src/references.test.ts`
295
+ after any edit to the rules.
280
296
 
281
297
  ## 6. Record the verdict
282
298
 
@@ -149,17 +149,26 @@ node scripts/verify-cites.mjs cites.json --offline # no network (everything de
149
149
  \`\`\`
150
150
  It prints per-cite \`{id, verdict, reason, matched_db, matched_title?, flags?}\` + a summary, and **exits 1 iff
151
151
  any \`false\` (fabrication)** — \`unresolvable\` alone is advisory and does NOT fail the gate, matching the
152
- narrowed-false philosophy. API responses are cached to \`scripts/.cite-cache.json\` (gitignored) so re-runs are
153
- cheap and deterministic.
152
+ narrowed-false philosophy. API responses are cached to \`~/.cache/paperlint/cite-cache.json\`
153
+ (\`VERIFY_CITES_CACHE\` overrides) so re-runs are cheap and deterministic — outside the installed package,
154
+ which \`npm ci\` wipes.
154
155
 
155
- ## 5a. Both checks run on every \`paperlint build\`
156
+ ## 5a. Both checks run on every \`paperlint build\` — from a committed cache
156
157
 
157
158
  \`npx paperlint build\` runs \`verify-cites\` and \`bib-authors\` over the paper's bibliography after the PDF is
158
159
  built and records the verdicts, with the SHA-256 of the bibliography it checked, in
159
160
  \`<paper>/_build/references.json\`. \`paperlint lint\` reads that record offline: \`paper/cite-exists\` and
160
161
  \`paper/author-list\` report a failing entry on its own line, \`paper/refs-fresh\` says when the bibliography
161
162
  changed since, and \`paper/refs-checked\` warns when nothing was recorded or the build had no network. The
162
- step never fails the build. Run the scripts by hand (below) to read a single verdict in full.
163
+ step never fails the build.
164
+
165
+ What the services ANSWERED is kept in \`<paper>/repro/references-cache.json\` — **commit it**. A build asks
166
+ only the questions that file cannot answer (a new or edited entry, or an answer older than 30 days), so an
167
+ unchanged bibliography builds with no network at all, in CI too, until its answers age out. It holds responses, never verdicts: the verdicts are derived again on
168
+ every build. A service that refuses (429, HTML instead of JSON, a timeout) is not asked again for the rest of the
169
+ build (the requests already in flight still land); the entries it would have answered say \`not asked: <service>: <reason>\`, and DBLP's become
170
+ \`authors: unchecked\`. Answers are asked again after 30 days; to refresh one sooner, delete its entry, or delete the file. Details:
171
+ \`docs/references.md\` in the package. Run the scripts by hand (below) to read a single verdict in full.
163
172
 
164
173
  ## 5b. The author-list gate — \`scripts/bib-authors.mjs\`
165
174
 
@@ -195,11 +204,18 @@ difference is real and not a metadata glitch. Entries that themselves declare a
195
204
  scope (a preprint entry is allowed to carry preprint metadata).
196
205
 
197
206
  ⚠️ **Discovery credit, and why it is NOT a dependency.** The class was found by running **rebiber**
198
- (\`yuchenlin/rebiber\`), which rewrites bib entries into their DBLP records. We deliberately do not depend on
199
- it: its install fights modern setuptools (the \`bibtexparser\` wheel dies with \`AttributeError:
200
- install_layout\`), and its *output* replaces our entries with DBLP's very long official booktitles — wrong for
201
- a page-limited paper. We needed the **comparison**, not the rewrite, and that is one fetch with zero deps.
202
- Note also that live DBLP beat rebiber's bundled dump: \`raji2021benchmark\` was found only by the live query.
207
+ (\`yuchenlin/rebiber\`), which rewrites bib entries into their DBLP records. Re-measured 2026-09-27 on rebiber
208
+ 1.4.0 (GitHub commit \`b917e36\`, the pin in \`render-paper/checkers-requirements.txt\`; the PyPI package is
209
+ a 2021 release upstream disowns):
210
+ - **Install: no longer broken, but heavy.** It installs in 36 s in a clean venv (bibtexparser 2.0.1 wheel,
211
+ rebiber built from source) — and carries **409 MB** of bundled venue data.
212
+ - **Rewrites: now optional.** \`--dry-run --report\` previews and leaves the file untouched; \`-s True\` shortens
213
+ venues (NAACL 2019 becomes \`Proc. of NAACL-HLT\` instead of a 160-character booktitle).
214
+ - **Bundled data still misses what live DBLP finds.** \`raji2021benchmark\` stayed an arXiv entry, local
215
+ index and \`--live-lookup\` alike, as it did in August.
216
+
217
+ So the reason not to depend on it is now scope, not breakage: we need the **comparison** of an author list
218
+ against the published record, not a rewrite of the entry, and that is one DBLP query with zero dependencies.
203
219
 
204
220
  🔴 **Two design decisions that are load-bearing, both learned by measurement the same day:**
205
221
  1. **Compare surname SEQUENCES, nothing else.** A first cut reported 13 differences of which **11 were
@@ -275,7 +291,7 @@ the same one MedSci Skills (arXiv:2606.09500) closes with \`verify_refs.py\`.
275
291
  | year | ±1 on a title search | exact, and only against the record that dates the cited version |
276
292
  | preprint→published | not asked | arXiv \`journal_ref\`/\`doi\` present and no venue named → say so |
277
293
  | what it could not check | folded into \`unresolvable\` | **enumerated, one rule per reason, never counted as verified** |
278
- | tests | \`verify-cites.test.mjs\` — not a \`*.harness.mjs\`, so \`npx vigiles test\` never runs it | \`eslint-rules/ref-facts.harness.mjs\` + \`.claude/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs\`, plus \`eslint-rules/ref-facts.mutations.mjs\` |
294
+ | tests | \`verify-cites.test.mjs\` — not a \`*.harness.mjs\`, so \`npx vigiles test\` never runs it | \`src/references.test.ts\` (the \`refs/*\` rules in \`src/reference-rules.ts\`) + \`skills/paper-pipeline/scripts/extract-ref-facts.test.mjs\` and \`.harness.mjs\`, under the package's 100% line and branch coverage gate |
279
295
 
280
296
  **The uncovered set is the point.** A reference with no resolvable identifier is a FINDING here
281
297
  (\`refs/no-identifier\`), and every partial comparison is another (\`refs/partial-check\`, one messageId per
@@ -290,9 +306,9 @@ and its \`.bib\` regex required a closing \`}\` on its own line, which neither r
290
306
  references\`). The \`.bib\` half now goes through \`@retorquere/bibtex-parser\`. Two of the three papers are
291
307
  therefore being checked for the first time.
292
308
 
293
- **Every check is watched failing.** \`node eslint-rules/ref-facts.mutations.mjs\` neuters each rule in turn
294
- and requires the harness to go red at the named case — 21 mutations, all killed, and three of them were
295
- findings about the TEST rather than the rule. Run it after any edit to the rules.
309
+ **Every check is exercised.** \`src/references.test.ts\` drives the \`refs/*\` rules, and \`npm run coverage\`
310
+ fails CI if any line or branch of them goes unexercised. Run \`npx vitest run src/references.test.ts\`
311
+ after any edit to the rules.
296
312
 
297
313
  ## 6. Record the verdict
298
314
 
@@ -0,0 +1,253 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`CVEs: found by count, found by list, not found, NVD down, and a malformed id 1`] = `
4
+ {
5
+ "calls": [
6
+ "https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2024-0001",
7
+ "https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2024-0002",
8
+ "https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2024-0003",
9
+ "https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2024-0004",
10
+ "https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2024-0005",
11
+ ],
12
+ "results": [
13
+ {
14
+ "id": "cve-2024-0001",
15
+ "matched_db": "nvd",
16
+ "matched_title": "cve-2024-0001",
17
+ "reason": "confirmed by nvd — "cve-2024-0001"",
18
+ "verdict": "true",
19
+ },
20
+ {
21
+ "id": "CVE-2024-0002",
22
+ "matched_db": "nvd",
23
+ "matched_title": "CVE-2024-0002",
24
+ "reason": "confirmed by nvd — "CVE-2024-0002"",
25
+ "verdict": "true",
26
+ },
27
+ {
28
+ "id": "CVE-2024-0003",
29
+ "matched_db": null,
30
+ "reason": "RESOLVED_TO_NOTHING: the provided CVE CVE-2024-0003 resolves to no record (checked nvd) — fabrication evidence",
31
+ "verdict": "false",
32
+ },
33
+ {
34
+ "id": "CVE-2024-0004",
35
+ "matched_db": null,
36
+ "reason": "all databases unreachable (network/rate-limit) — degraded to unresolvable, never to false",
37
+ "refused": [
38
+ "nvd: http 500",
39
+ ],
40
+ "verdict": "unresolvable",
41
+ },
42
+ {
43
+ "id": "CVE-2024-0005",
44
+ "matched_db": null,
45
+ "reason": "RESOLVED_TO_NOTHING: the provided CVE CVE-2024-0005 resolves to no record (checked nvd) — fabrication evidence",
46
+ "verdict": "false",
47
+ },
48
+ {
49
+ "id": "CVE-24-1",
50
+ "matched_db": null,
51
+ "reason": "RESOLVED_TO_NOTHING: the provided CVE CVE-24-1 resolves to no record (checked nvd) — fabrication evidence",
52
+ "verdict": "false",
53
+ },
54
+ ],
55
+ }
56
+ `;
57
+
58
+ exports[`S2 and arXiv unreachable for an arXiv id 1`] = `
59
+ {
60
+ "calls": [
61
+ "https://api.semanticscholar.org/graph/v1/paper/arXiv%3A1706.03762?fields=title,year",
62
+ "https://export.arxiv.org/api/query?id_list=1706.03762&max_results=1",
63
+ ],
64
+ "result": {
65
+ "id": "x",
66
+ "matched_db": null,
67
+ "reason": "all databases unreachable (network/rate-limit) — degraded to unresolvable, never to false",
68
+ "refused": [
69
+ "semantic_scholar: http 500",
70
+ "arxiv: http 500",
71
+ ],
72
+ "verdict": "unresolvable",
73
+ },
74
+ }
75
+ `;
76
+
77
+ exports[`a DOI every registry resolves to the claimed paper, and the authority knows: true 1`] = `
78
+ {
79
+ "calls": [
80
+ "https://api.crossref.org/works/10.1%2Fx",
81
+ ],
82
+ "result": {
83
+ "id": "v",
84
+ "matched_db": "crossref",
85
+ "matched_title": "Attention Is All You Need",
86
+ "reason": "confirmed by crossref — "Attention Is All You Need"",
87
+ "verdict": "true",
88
+ },
89
+ }
90
+ `;
91
+
92
+ exports[`a DOI nobody knows, and the authority says it does not exist: false 1`] = `
93
+ {
94
+ "calls": [
95
+ "https://api.crossref.org/works/10.1%2Fnope",
96
+ "https://api.openalex.org/works/doi:10.1%2Fnope",
97
+ "https://api.semanticscholar.org/graph/v1/paper/DOI%3A10.1%2Fnope?fields=title,year",
98
+ "https://doi.org/api/handles/10.1/nope",
99
+ ],
100
+ "result": {
101
+ "id": "f",
102
+ "matched_db": null,
103
+ "reason": "RESOLVED_TO_NOTHING: DOI 10.1/nope does not resolve at the DOI authority (doi.org handle responseCode 100 = not found) — fabrication evidence",
104
+ "verdict": "false",
105
+ },
106
+ }
107
+ `;
108
+
109
+ exports[`a title only: every registry's search endpoint, hits and all 1`] = `
110
+ {
111
+ "calls": [
112
+ "https://api.crossref.org/works?query.bibliographic=Attention%20Is%20All%20You%20Need&rows=5",
113
+ "https://api.openalex.org/works?search=Attention%20Is%20All%20You%20Need&per-page=5",
114
+ "https://api.semanticscholar.org/graph/v1/paper/search?query=Attention%20Is%20All%20You%20Need&fields=title,year&limit=5",
115
+ "https://export.arxiv.org/api/query?search_query=ti:%22Attention%20Is%20All%20You%20Need%22&max_results=5",
116
+ ],
117
+ "result": {
118
+ "id": "t",
119
+ "matched_db": "arxiv",
120
+ "matched_title": "Attention Is All You Need",
121
+ "reason": "confirmed by arxiv — "Attention Is All You Need"",
122
+ "verdict": "true",
123
+ },
124
+ }
125
+ `;
126
+
127
+ exports[`a title search that cannot reach any registry is unresolvable 1`] = `
128
+ {
129
+ "calls": [
130
+ "https://api.crossref.org/works?query.bibliographic=Attention%20Is%20All%20You%20Need&rows=5",
131
+ "https://api.openalex.org/works?search=Attention%20Is%20All%20You%20Need&per-page=5",
132
+ "https://api.semanticscholar.org/graph/v1/paper/search?query=Attention%20Is%20All%20You%20Need&fields=title,year&limit=5",
133
+ "https://export.arxiv.org/api/query?search_query=ti:%22Attention%20Is%20All%20You%20Need%22&max_results=5",
134
+ ],
135
+ "result": {
136
+ "id": "d",
137
+ "matched_db": null,
138
+ "reason": "all databases unreachable (network/rate-limit) — degraded to unresolvable, never to false",
139
+ "refused": [
140
+ "crossref: http 500",
141
+ "openalex: http 502",
142
+ "semantic_scholar: timeout",
143
+ "arxiv: http 503",
144
+ ],
145
+ "verdict": "unresolvable",
146
+ },
147
+ }
148
+ `;
149
+
150
+ exports[`a title search whose answers are empty or shapeless is a miss, not a match 1`] = `
151
+ {
152
+ "calls": [
153
+ "https://api.crossref.org/works?query.bibliographic=Attention%20Is%20All%20You%20Need&rows=5",
154
+ "https://api.openalex.org/works?search=Attention%20Is%20All%20You%20Need&per-page=5",
155
+ "https://api.semanticscholar.org/graph/v1/paper/search?query=Attention%20Is%20All%20You%20Need&fields=title,year&limit=5",
156
+ "https://export.arxiv.org/api/query?search_query=ti:%22Attention%20Is%20All%20You%20Need%22&max_results=5",
157
+ ],
158
+ "result": {
159
+ "id": "e",
160
+ "matched_db": null,
161
+ "reason": "not found by title in any database (no resolvable id to disprove) — could be a legit unindexed/regional/pre-digital work, NOT fabrication",
162
+ "verdict": "unresolvable",
163
+ },
164
+ }
165
+ `;
166
+
167
+ exports[`an arXiv id nobody has: the arXiv API's empty feed and S2's 404 1`] = `
168
+ {
169
+ "calls": [
170
+ "https://api.semanticscholar.org/graph/v1/paper/arXiv%3A9999.99999?fields=title,year",
171
+ "https://export.arxiv.org/api/query?id_list=9999.99999&max_results=1",
172
+ ],
173
+ "result": {
174
+ "id": "n",
175
+ "matched_db": null,
176
+ "reason": "RESOLVED_TO_NOTHING: arXiv id 9999.99999 returns a definitive not-found from the reachable arXiv API — fabrication evidence",
177
+ "verdict": "false",
178
+ },
179
+ }
180
+ `;
181
+
182
+ exports[`an arXiv id: Semantic Scholar by arXiv id, the arXiv API by id_list 1`] = `
183
+ {
184
+ "calls": [
185
+ "https://api.crossref.org/works?query.bibliographic=Attention%20Is%20All%20You%20Need&rows=5",
186
+ "https://api.openalex.org/works?search=Attention%20Is%20All%20You%20Need&per-page=5",
187
+ "https://api.semanticscholar.org/graph/v1/paper/arXiv%3A1706.03762?fields=title,year",
188
+ ],
189
+ "result": {
190
+ "id": "a",
191
+ "matched_db": "semantic_scholar",
192
+ "matched_title": "Attention Is All You Need",
193
+ "reason": "confirmed by semantic_scholar — "Attention Is All You Need"",
194
+ "refused": [
195
+ "crossref: Error: unrouted request: https://api.crossref.org/works?query.bibliographic=Attention%20Is%20All%20You%20Need&rows=5",
196
+ "openalex: Error: unrouted request: https://api.openalex.org/works?search=Attention%20Is%20All%20You%20Need&per-page=5",
197
+ ],
198
+ "verdict": "true",
199
+ },
200
+ }
201
+ `;
202
+
203
+ exports[`every registry down (429, 503, 403, a thrown error): unresolvable, never false 1`] = `
204
+ {
205
+ "calls": [
206
+ "https://api.crossref.org/works/10.1%2Fx",
207
+ "https://api.openalex.org/works/doi:10.1%2Fx",
208
+ "https://api.semanticscholar.org/graph/v1/paper/DOI%3A10.1%2Fx?fields=title,year",
209
+ "https://doi.org/api/handles/10.1/x",
210
+ ],
211
+ "result": {
212
+ "id": "u",
213
+ "matched_db": null,
214
+ "reason": "all databases unreachable (network/rate-limit) — degraded to unresolvable, never to false",
215
+ "refused": [
216
+ "crossref: http 429",
217
+ "openalex: http 503",
218
+ "semantic_scholar: http 403",
219
+ "doi_authority: TypeError: fetch failed",
220
+ ],
221
+ "verdict": "unresolvable",
222
+ },
223
+ }
224
+ `;
225
+
226
+ exports[`offline: no request at all, the verdict from the citation alone 1`] = `
227
+ {
228
+ "calls": [],
229
+ "result": {
230
+ "id": "o",
231
+ "matched_db": null,
232
+ "reason": "all databases unreachable (network/rate-limit) — degraded to unresolvable, never to false",
233
+ "verdict": "unresolvable",
234
+ },
235
+ }
236
+ `;
237
+
238
+ exports[`records with fields missing: no title, a subtitle, a year only in \`published\`, no id 1`] = `
239
+ {
240
+ "calls": [
241
+ "https://api.crossref.org/works/10.1%2Fx",
242
+ "https://api.openalex.org/works/doi:10.1%2Fx",
243
+ "https://api.semanticscholar.org/graph/v1/paper/DOI%3A10.1%2Fx?fields=title,year",
244
+ "https://doi.org/api/handles/10.1/x",
245
+ ],
246
+ "result": {
247
+ "id": "m",
248
+ "matched_db": null,
249
+ "reason": "DOI_MISMATCH: identifier resolves via crossref to an UNRELATED paper "" (claimed "Attention Is All You Need") — DOI misdirection",
250
+ "verdict": "false",
251
+ },
252
+ }
253
+ `;