paperlint 3.1.0 → 3.1.1

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 (284) hide show
  1. package/.github/workflows/ci.yml +28 -41
  2. package/.github/workflows/dependabot-automerge.yml +1 -1
  3. package/CLAUDE.md +51 -56
  4. package/CONTRIBUTING.md +30 -18
  5. package/bin/paperlint.test.mjs +45 -0
  6. package/dist/adapters/node/files.io.d.ts +2 -0
  7. package/dist/adapters/node/files.io.d.ts.map +1 -1
  8. package/dist/adapters/node/files.io.js +3 -2
  9. package/dist/adapters/node/files.io.js.map +1 -1
  10. package/dist/adapters/pdfjs/fill.d.ts +15 -4
  11. package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
  12. package/dist/adapters/pdfjs/fill.js +31 -21
  13. package/dist/adapters/pdfjs/fill.js.map +1 -1
  14. package/dist/adapters/references/index.d.ts.map +1 -1
  15. package/dist/adapters/references/index.js +14 -5
  16. package/dist/adapters/references/index.js.map +1 -1
  17. package/dist/adapters/references/pool.d.ts +8 -0
  18. package/dist/adapters/references/pool.d.ts.map +1 -0
  19. package/dist/adapters/references/pool.js +19 -0
  20. package/dist/adapters/references/pool.js.map +1 -0
  21. package/dist/build.d.ts +10 -2
  22. package/dist/build.d.ts.map +1 -1
  23. package/dist/build.js +7 -8
  24. package/dist/build.js.map +1 -1
  25. package/dist/cli.d.ts +27 -1
  26. package/dist/cli.d.ts.map +1 -1
  27. package/dist/cli.js +71 -55
  28. package/dist/cli.js.map +1 -1
  29. package/dist/doctor.d.ts +27 -1
  30. package/dist/doctor.d.ts.map +1 -1
  31. package/dist/doctor.js +4 -3
  32. package/dist/doctor.js.map +1 -1
  33. package/dist/domain/result.d.ts +0 -2
  34. package/dist/domain/result.d.ts.map +1 -1
  35. package/dist/domain/result.js +0 -4
  36. package/dist/domain/result.js.map +1 -1
  37. package/dist/hooks-settings.d.ts +8 -5
  38. package/dist/hooks-settings.d.ts.map +1 -1
  39. package/dist/hooks-settings.js +32 -32
  40. package/dist/hooks-settings.js.map +1 -1
  41. package/dist/init.d.ts +8 -0
  42. package/dist/init.d.ts.map +1 -1
  43. package/dist/init.js +3 -3
  44. package/dist/init.js.map +1 -1
  45. package/dist/latex-log.d.ts +0 -2
  46. package/dist/latex-log.d.ts.map +1 -1
  47. package/dist/latex-log.js +7 -13
  48. package/dist/latex-log.js.map +1 -1
  49. package/dist/link-skills.d.ts +15 -2
  50. package/dist/link-skills.d.ts.map +1 -1
  51. package/dist/link-skills.js +36 -20
  52. package/dist/link-skills.js.map +1 -1
  53. package/dist/paper-files.d.ts.map +1 -1
  54. package/dist/paper-files.js +6 -4
  55. package/dist/paper-files.js.map +1 -1
  56. package/dist/pdf-facts.d.ts +11 -0
  57. package/dist/pdf-facts.d.ts.map +1 -1
  58. package/dist/pdf-facts.js +16 -18
  59. package/dist/pdf-facts.js.map +1 -1
  60. package/dist/pdf-geometry.js +1 -1
  61. package/dist/pdf-geometry.js.map +1 -1
  62. package/dist/presets.d.ts +2 -0
  63. package/dist/presets.d.ts.map +1 -1
  64. package/dist/presets.js +3 -1
  65. package/dist/presets.js.map +1 -1
  66. package/dist/references.js +2 -2
  67. package/dist/references.js.map +1 -1
  68. package/dist/tex-requirements.d.ts +8 -0
  69. package/dist/tex-requirements.d.ts.map +1 -1
  70. package/dist/tex-requirements.js +6 -5
  71. package/dist/tex-requirements.js.map +1 -1
  72. package/dist/toolchain.d.ts +3 -2
  73. package/dist/toolchain.d.ts.map +1 -1
  74. package/dist/toolchain.js +4 -5
  75. package/dist/toolchain.js.map +1 -1
  76. package/dist/venue-rules.d.ts.map +1 -1
  77. package/dist/venue-rules.js +11 -9
  78. package/dist/venue-rules.js.map +1 -1
  79. package/docs/prior-art/README.md +1 -1
  80. package/docs/prior-art/test-tooling.md +7 -30
  81. package/eslint-rules/install-path-literals.harness.mjs +17 -1
  82. package/eslint-rules/latex-language.harness.mjs +3 -3
  83. package/eslint-rules/latex-language.mjs +12 -9
  84. package/eslint-rules/latex-language.test.mjs +200 -0
  85. package/eslint-rules/paper-research-question.harness.mjs +73 -7
  86. package/eslint-rules/paper-stages.harness.mjs +9 -14
  87. package/eslint-rules/paper-stages.test.mjs +135 -0
  88. package/eslint-rules/paper-typography.mjs +9 -2
  89. package/eslint-rules/paper-typography.test.mjs +146 -1
  90. package/eslint-rules/pdf-last-page-balance.harness.mjs +63 -7
  91. package/eslint-rules/review-frontmatter.mjs +2 -1
  92. package/eslint-rules/review-frontmatter.test.mjs +37 -1
  93. package/eslint-rules/temp-root-realpath.harness.mjs +49 -0
  94. package/eslint-rules/tex-build.mjs +13 -13
  95. package/eslint.config.mjs +6 -2
  96. package/hooks/hooks.harness.mjs +6 -11
  97. package/hooks/paper-edit-guard.hook.mjs +2 -2
  98. package/hooks/paper-edit-guard.test.mjs +61 -0
  99. package/hooks/paper-skills-nudge.hook.mjs +2 -2
  100. package/hooks/paper-skills-nudge.test.mjs +38 -0
  101. package/hooks/paper-status-gates.test.mjs +42 -0
  102. package/lib/agent-cli-version.harness.mjs +7 -0
  103. package/lib/check.d.mts +17 -0
  104. package/lib/check.mjs +50 -0
  105. package/lib/check.test.ts +67 -0
  106. package/lib/markdown.mjs +16 -69
  107. package/lib/markdown.test.mjs +246 -0
  108. package/lib/paper-config.harness.mjs +28 -9
  109. package/lib/paper-config.mjs +7 -2
  110. package/lib/skill-checks.mjs +18 -46
  111. package/lib/skill-checks.test.mjs +297 -0
  112. package/lib/skill-corpus.mjs +11 -13
  113. package/lib/skill-corpus.test.mjs +54 -0
  114. package/lib/skill-trigger-cases.harness.mjs +57 -4
  115. package/package.json +6 -5
  116. package/scripts/check.harness.mjs +4 -10
  117. package/scripts/check.mjs +45 -42
  118. package/scripts/check.test.mjs +104 -0
  119. package/scripts/coverage-config.test.ts +406 -0
  120. package/scripts/eslint-report-guard.test.mjs +48 -0
  121. package/scripts/harness-api.frozen.json +0 -2
  122. package/scripts/layer-legacy-frozen.d.mts +9 -1
  123. package/scripts/layer-legacy-frozen.mjs +28 -11
  124. package/scripts/layer-legacy-frozen.test.ts +143 -1
  125. package/scripts/rules-are-content-only.harness.mjs +3 -11
  126. package/scripts/rules-are-content-only.mjs +29 -19
  127. package/scripts/rules-are-content-only.test.mjs +61 -0
  128. package/scripts/rules-see-files.mjs +11 -9
  129. package/scripts/rules-see-files.test.mjs +71 -0
  130. package/scripts/test.mjs +44 -0
  131. package/scripts/test.test.mjs +52 -0
  132. package/skills/grade-paper-writing/__snapshots__/prose-lint.test.mjs.snap +57 -0
  133. package/skills/grade-paper-writing/prose-lint.mjs +31 -12
  134. package/skills/grade-paper-writing/prose-lint.test.mjs +153 -0
  135. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +3 -3
  136. package/skills/paper-pipeline/scripts/README.md +13 -132
  137. package/skills/paper-pipeline/scripts/announce.test.mjs +102 -0
  138. package/skills/paper-pipeline/scripts/artifact-coverage.test.mjs +86 -0
  139. package/skills/paper-pipeline/scripts/consumer.d.mts +8 -1
  140. package/skills/paper-pipeline/scripts/consumer.harness.mjs +3 -3
  141. package/skills/paper-pipeline/scripts/consumer.mjs +13 -25
  142. package/skills/paper-pipeline/scripts/consumer.test.mjs +113 -0
  143. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +9 -4
  144. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +380 -0
  145. package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +71 -0
  146. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +4 -4
  147. package/skills/paper-pipeline/scripts/generated-code.mjs +3 -2
  148. package/skills/paper-pipeline/scripts/generated-code.test.mjs +62 -0
  149. package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +302 -0
  150. package/skills/paper-pipeline/scripts/ledger.mjs +1 -1
  151. package/skills/paper-pipeline/scripts/{ledger.selftest.mjs → ledger.test.mjs} +44 -76
  152. package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +474 -0
  153. package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -63
  154. package/skills/paper-pipeline/scripts/pipeline-check.mjs +37 -21
  155. package/skills/paper-pipeline/scripts/pipeline-edges.mjs +2 -2
  156. package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +73 -0
  157. package/skills/paper-pipeline/scripts/population-map.harness.mjs +4 -47
  158. package/skills/paper-pipeline/scripts/population-map.mjs +12 -2
  159. package/skills/paper-pipeline/scripts/population-map.test.mjs +154 -0
  160. package/skills/paper-pipeline/scripts/provenance.harness.mjs +26 -0
  161. package/skills/paper-pipeline/scripts/round-diff.harness.mjs +1 -2
  162. package/skills/paper-pipeline/scripts/round-diff.mjs +13 -6
  163. package/skills/paper-pipeline/scripts/round-diff.test.mjs +159 -0
  164. package/skills/paper-pipeline/scripts/run-mechanical.mjs +30 -6
  165. package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +468 -0
  166. package/skills/paper-pipeline/scripts/status.mjs +2 -2
  167. package/skills/paper-pipeline/scripts/status.test.mjs +232 -0
  168. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +2 -2
  169. package/skills/render-paper/extract-pdf-facts.harness.mjs +56 -7
  170. package/skills/tighten-paper/__snapshots__/structure.test.mjs.snap +133 -0
  171. package/skills/tighten-paper/structure.test.mjs +116 -0
  172. package/skills/verify-citations/SKILL.md +5 -5
  173. package/skills/verify-citations/SKILL.md.spec.ts +4 -4
  174. package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +235 -0
  175. package/skills/verify-citations/scripts/bib-authors.check.test.mjs +274 -0
  176. package/skills/verify-citations/scripts/bib-authors.cli.test.mjs +178 -0
  177. package/skills/verify-citations/scripts/bib-authors.mjs +2 -1
  178. package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
  179. package/skills/verify-citations/scripts/verify-cites.mjs +96 -96
  180. package/skills/verify-citations/scripts/verify-cites.net.test.mjs +287 -0
  181. package/skills/verify-citations/scripts/verify-cites.test.mjs +149 -5
  182. package/src/adapters/banal/failure.test.ts +70 -1
  183. package/src/adapters/banal/index.test.ts +16 -0
  184. package/src/adapters/banal/output.test.ts +5 -0
  185. package/src/adapters/curl/download.test.ts +43 -1
  186. package/src/adapters/memory/index.test.ts +25 -0
  187. package/src/adapters/node/files.io.ts +3 -2
  188. package/src/adapters/node/files.test.ts +13 -1
  189. package/src/adapters/node/process.test.ts +38 -0
  190. package/src/adapters/pdfjs/fill.test.ts +34 -0
  191. package/src/adapters/pdfjs/fill.ts +44 -25
  192. package/src/adapters/references/index.test.ts +228 -0
  193. package/src/adapters/references/index.ts +21 -6
  194. package/src/adapters/references/pool.test.ts +31 -0
  195. package/src/adapters/references/pool.ts +24 -0
  196. package/src/build-engine.harness.mjs +3 -7
  197. package/src/build-engine.test.ts +137 -0
  198. package/src/build.harness.mjs +5 -9
  199. package/src/build.test.ts +362 -0
  200. package/src/build.ts +13 -9
  201. package/src/cli.harness.mjs +10 -17
  202. package/src/cli.test.ts +584 -0
  203. package/src/cli.ts +112 -66
  204. package/src/doctor.harness.mjs +3 -8
  205. package/src/doctor.test.ts +64 -0
  206. package/src/doctor.ts +12 -4
  207. package/src/domain/result.test.ts +4 -0
  208. package/src/domain/result.ts +0 -5
  209. package/src/engine.harness.mjs +27 -7
  210. package/src/facts-file.test.ts +110 -0
  211. package/src/hooks-settings.harness.mjs +43 -13
  212. package/src/hooks-settings.ts +41 -35
  213. package/src/init.test.ts +308 -5
  214. package/src/init.ts +3 -3
  215. package/src/latex-log.harness.mjs +32 -7
  216. package/src/latex-log.ts +6 -13
  217. package/src/latex-loop.harness.mjs +8 -7
  218. package/src/link-skills.harness.mjs +3 -8
  219. package/src/link-skills.test.ts +105 -0
  220. package/src/link-skills.ts +75 -21
  221. package/src/new-paper.harness.mjs +3 -7
  222. package/src/new-paper.test.ts +31 -1
  223. package/src/paper-files.test.ts +12 -0
  224. package/src/paper-files.ts +9 -5
  225. package/src/paper-settings.test.ts +20 -0
  226. package/src/pdf-facts.harness.mjs +3 -6
  227. package/src/pdf-facts.test.ts +144 -0
  228. package/src/pdf-facts.ts +17 -19
  229. package/src/pdf-geometry.harness.mjs +3 -7
  230. package/src/pdf-geometry.ts +1 -1
  231. package/src/presets.test.ts +2 -0
  232. package/src/presets.ts +7 -1
  233. package/src/references.test.ts +97 -0
  234. package/src/references.ts +2 -2
  235. package/src/rules-config.harness.mjs +10 -8
  236. package/src/structure.harness.mjs +3 -7
  237. package/src/tex-requirements.harness.mjs +34 -7
  238. package/src/tex-requirements.ts +12 -6
  239. package/src/toolchain.harness.mjs +3 -7
  240. package/src/toolchain.test.ts +280 -0
  241. package/src/toolchain.ts +8 -7
  242. package/src/venue-rules.test.ts +60 -0
  243. package/src/venue-rules.ts +13 -11
  244. package/action.mutations.mjs +0 -162
  245. package/eslint-rules/install-path-literals.mutations.mjs +0 -62
  246. package/eslint-rules/latex-language.mutations.mjs +0 -196
  247. package/eslint-rules/paper-research-question.mutations.mjs +0 -127
  248. package/eslint-rules/paper-stages.mutations.mjs +0 -81
  249. package/eslint-rules/papers.mutations.mjs +0 -186
  250. package/eslint-rules/temp-root-realpath.mutations.mjs +0 -99
  251. package/eslint-rules/tex-build.mutations.mjs +0 -258
  252. package/hooks/hooks.mutations.mjs +0 -337
  253. package/lib/agent-cli-version.mutations.mjs +0 -109
  254. package/lib/mutation-driver.harness.mjs +0 -227
  255. package/lib/mutation-driver.mjs +0 -397
  256. package/lib/mutation-driver.mutations.mjs +0 -68
  257. package/lib/paper-config.mutations.mjs +0 -143
  258. package/lib/skill-trigger-cases.mutations.mjs +0 -65
  259. package/scripts/check.mutations.mjs +0 -110
  260. package/scripts/exclusive.mjs +0 -138
  261. package/scripts/mutation-batteries-frozen.harness.mjs +0 -204
  262. package/scripts/mutation-batteries-frozen.mjs +0 -238
  263. package/scripts/mutation-batteries.frozen.json +0 -108
  264. package/scripts/rules-are-content-only.mutations.mjs +0 -81
  265. package/scripts/rules-see-files.mutations.mjs +0 -131
  266. package/scripts/run-mutations.mjs +0 -100
  267. package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +0 -218
  268. package/skills/paper-pipeline/scripts/consumer.mutations.mjs +0 -190
  269. package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +0 -54
  270. package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +0 -254
  271. package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +0 -54
  272. package/skills/paper-pipeline/scripts/population-map.mutations.mjs +0 -65
  273. package/skills/paper-pipeline/scripts/population-map.selftest.mjs +0 -122
  274. package/skills/paper-pipeline/scripts/provenance.mutations.mjs +0 -59
  275. package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +0 -276
  276. package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +0 -65
  277. package/skills/skill-contract.mutations.mjs +0 -179
  278. package/src/build.mutations.mjs +0 -196
  279. package/src/cli.mutations.mjs +0 -414
  280. package/src/doctor.mutations.mjs +0 -175
  281. package/src/hooks-settings.mutations.mjs +0 -99
  282. package/src/link-skills.mutations.mjs +0 -140
  283. package/src/new-paper.mutations.mjs +0 -65
  284. package/src/structure.mutations.mjs +0 -83
@@ -0,0 +1,235 @@
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
+ "verdict": "unresolvable",
38
+ },
39
+ {
40
+ "id": "CVE-2024-0005",
41
+ "matched_db": null,
42
+ "reason": "RESOLVED_TO_NOTHING: the provided CVE CVE-2024-0005 resolves to no record (checked nvd) — fabrication evidence",
43
+ "verdict": "false",
44
+ },
45
+ {
46
+ "id": "CVE-24-1",
47
+ "matched_db": null,
48
+ "reason": "RESOLVED_TO_NOTHING: the provided CVE CVE-24-1 resolves to no record (checked nvd) — fabrication evidence",
49
+ "verdict": "false",
50
+ },
51
+ ],
52
+ }
53
+ `;
54
+
55
+ exports[`S2 and arXiv unreachable for an arXiv id 1`] = `
56
+ {
57
+ "calls": [
58
+ "https://api.semanticscholar.org/graph/v1/paper/arXiv%3A1706.03762?fields=title,year",
59
+ "https://export.arxiv.org/api/query?id_list=1706.03762&max_results=1",
60
+ ],
61
+ "result": {
62
+ "id": "x",
63
+ "matched_db": null,
64
+ "reason": "all databases unreachable (network/rate-limit) — degraded to unresolvable, never to false",
65
+ "verdict": "unresolvable",
66
+ },
67
+ }
68
+ `;
69
+
70
+ exports[`a DOI every registry resolves to the claimed paper, and the authority knows: true 1`] = `
71
+ {
72
+ "calls": [
73
+ "https://api.crossref.org/works/10.1%2Fx",
74
+ "https://api.openalex.org/works/doi:10.1%2Fx",
75
+ "https://api.semanticscholar.org/graph/v1/paper/DOI%3A10.1%2Fx?fields=title,year",
76
+ "https://doi.org/api/handles/10.1/x",
77
+ ],
78
+ "result": {
79
+ "id": "v",
80
+ "matched_db": "crossref",
81
+ "matched_title": "Attention Is All You Need",
82
+ "reason": "confirmed by crossref — "Attention Is All You Need"",
83
+ "verdict": "true",
84
+ },
85
+ }
86
+ `;
87
+
88
+ exports[`a DOI nobody knows, and the authority says it does not exist: false 1`] = `
89
+ {
90
+ "calls": [
91
+ "https://api.crossref.org/works/10.1%2Fnope",
92
+ "https://api.openalex.org/works/doi:10.1%2Fnope",
93
+ "https://api.semanticscholar.org/graph/v1/paper/DOI%3A10.1%2Fnope?fields=title,year",
94
+ "https://doi.org/api/handles/10.1/nope",
95
+ ],
96
+ "result": {
97
+ "id": "f",
98
+ "matched_db": null,
99
+ "reason": "RESOLVED_TO_NOTHING: DOI 10.1/nope does not resolve at the DOI authority (doi.org handle responseCode 100 = not found) — fabrication evidence",
100
+ "verdict": "false",
101
+ },
102
+ }
103
+ `;
104
+
105
+ exports[`a title only: every registry's search endpoint, hits and all 1`] = `
106
+ {
107
+ "calls": [
108
+ "https://api.crossref.org/works?query.bibliographic=Attention%20Is%20All%20You%20Need&rows=5",
109
+ "https://api.openalex.org/works?search=Attention%20Is%20All%20You%20Need&per-page=5",
110
+ "https://api.semanticscholar.org/graph/v1/paper/search?query=Attention%20Is%20All%20You%20Need&fields=title,year&limit=5",
111
+ "https://export.arxiv.org/api/query?search_query=ti:%22Attention%20Is%20All%20You%20Need%22&max_results=5",
112
+ ],
113
+ "result": {
114
+ "id": "t",
115
+ "matched_db": "crossref",
116
+ "matched_title": "Attention Is All You Need",
117
+ "reason": "confirmed by crossref — "Attention Is All You Need"",
118
+ "verdict": "true",
119
+ },
120
+ }
121
+ `;
122
+
123
+ exports[`a title search that cannot reach any registry is unresolvable 1`] = `
124
+ {
125
+ "calls": [
126
+ "https://api.crossref.org/works?query.bibliographic=Attention%20Is%20All%20You%20Need&rows=5",
127
+ "https://api.openalex.org/works?search=Attention%20Is%20All%20You%20Need&per-page=5",
128
+ "https://api.semanticscholar.org/graph/v1/paper/search?query=Attention%20Is%20All%20You%20Need&fields=title,year&limit=5",
129
+ "https://export.arxiv.org/api/query?search_query=ti:%22Attention%20Is%20All%20You%20Need%22&max_results=5",
130
+ ],
131
+ "result": {
132
+ "id": "d",
133
+ "matched_db": null,
134
+ "reason": "all databases unreachable (network/rate-limit) — degraded to unresolvable, never to false",
135
+ "verdict": "unresolvable",
136
+ },
137
+ }
138
+ `;
139
+
140
+ exports[`a title search whose answers are empty or shapeless is a miss, not a match 1`] = `
141
+ {
142
+ "calls": [
143
+ "https://api.crossref.org/works?query.bibliographic=Attention%20Is%20All%20You%20Need&rows=5",
144
+ "https://api.openalex.org/works?search=Attention%20Is%20All%20You%20Need&per-page=5",
145
+ "https://api.semanticscholar.org/graph/v1/paper/search?query=Attention%20Is%20All%20You%20Need&fields=title,year&limit=5",
146
+ "https://export.arxiv.org/api/query?search_query=ti:%22Attention%20Is%20All%20You%20Need%22&max_results=5",
147
+ ],
148
+ "result": {
149
+ "id": "e",
150
+ "matched_db": null,
151
+ "reason": "not found by title in any database (no resolvable id to disprove) — could be a legit unindexed/regional/pre-digital work, NOT fabrication",
152
+ "verdict": "unresolvable",
153
+ },
154
+ }
155
+ `;
156
+
157
+ exports[`an arXiv id nobody has: the arXiv API's empty feed and S2's 404 1`] = `
158
+ {
159
+ "calls": [
160
+ "https://api.semanticscholar.org/graph/v1/paper/arXiv%3A9999.99999?fields=title,year",
161
+ "https://export.arxiv.org/api/query?id_list=9999.99999&max_results=1",
162
+ ],
163
+ "result": {
164
+ "id": "n",
165
+ "matched_db": null,
166
+ "reason": "RESOLVED_TO_NOTHING: arXiv id 9999.99999 returns a definitive not-found from the reachable arXiv API — fabrication evidence",
167
+ "verdict": "false",
168
+ },
169
+ }
170
+ `;
171
+
172
+ exports[`an arXiv id: Semantic Scholar by arXiv id, the arXiv API by id_list 1`] = `
173
+ {
174
+ "calls": [
175
+ "https://api.crossref.org/works?query.bibliographic=Attention%20Is%20All%20You%20Need&rows=5",
176
+ "https://api.openalex.org/works?search=Attention%20Is%20All%20You%20Need&per-page=5",
177
+ "https://api.semanticscholar.org/graph/v1/paper/arXiv%3A1706.03762?fields=title,year",
178
+ "https://export.arxiv.org/api/query?id_list=1706.03762&max_results=1",
179
+ ],
180
+ "result": {
181
+ "id": "a",
182
+ "matched_db": "semantic_scholar",
183
+ "matched_title": "Attention Is All You Need",
184
+ "reason": "confirmed by semantic_scholar — "Attention Is All You Need"",
185
+ "verdict": "true",
186
+ },
187
+ }
188
+ `;
189
+
190
+ exports[`every registry down (429, 503, 403, a thrown error): unresolvable, never false 1`] = `
191
+ {
192
+ "calls": [
193
+ "https://api.crossref.org/works/10.1%2Fx",
194
+ "https://api.openalex.org/works/doi:10.1%2Fx",
195
+ "https://api.semanticscholar.org/graph/v1/paper/DOI%3A10.1%2Fx?fields=title,year",
196
+ "https://doi.org/api/handles/10.1/x",
197
+ ],
198
+ "result": {
199
+ "id": "u",
200
+ "matched_db": null,
201
+ "reason": "all databases unreachable (network/rate-limit) — degraded to unresolvable, never to false",
202
+ "verdict": "unresolvable",
203
+ },
204
+ }
205
+ `;
206
+
207
+ exports[`offline: no request at all, the verdict from the citation alone 1`] = `
208
+ {
209
+ "calls": [],
210
+ "result": {
211
+ "id": "o",
212
+ "matched_db": null,
213
+ "reason": "all databases unreachable (network/rate-limit) — degraded to unresolvable, never to false",
214
+ "verdict": "unresolvable",
215
+ },
216
+ }
217
+ `;
218
+
219
+ exports[`records with fields missing: no title, a subtitle, a year only in \`published\`, no id 1`] = `
220
+ {
221
+ "calls": [
222
+ "https://api.crossref.org/works/10.1%2Fx",
223
+ "https://api.openalex.org/works/doi:10.1%2Fx",
224
+ "https://api.semanticscholar.org/graph/v1/paper/DOI%3A10.1%2Fx?fields=title,year",
225
+ "https://doi.org/api/handles/10.1/x",
226
+ ],
227
+ "result": {
228
+ "id": "m",
229
+ "matched_db": "crossref",
230
+ "matched_title": "",
231
+ "reason": "confirmed by crossref",
232
+ "verdict": "true",
233
+ },
234
+ }
235
+ `;
@@ -0,0 +1,274 @@
1
+ /**
2
+ * bib-authors.mjs's comparison (`checkAuthors`) and its reading of DBLP's answer (`dblpHits`).
3
+ *
4
+ * `checkAuthors` takes its lookup and its pause as parameters for exactly this: the buckets it
5
+ * sorts entries into — finding, matched, not applicable, NOT CHECKED — are our logic, and each is
6
+ * driven here with a lookup that answers on purpose. `dblpHits` is our parser of DBLP's search
7
+ * JSON; it is fed that documented shape (`result.hits.hit[].info`), which pins how WE read it,
8
+ * not how DBLP behaves — the colocated bib-authors.test.mjs keeps the live service out of tests.
9
+ */
10
+ import assert from "node:assert/strict";
11
+ import { afterEach, test, vi } from "vitest";
12
+ import {
13
+ checkAuthors,
14
+ claimsPublished,
15
+ dblpHits,
16
+ parseBib,
17
+ surnames,
18
+ } from "./bib-authors.mjs";
19
+
20
+ afterEach(() => vi.unstubAllGlobals());
21
+
22
+ const entry = (key, author, title, booktitle = "Proc. X") => ({
23
+ type: "inproceedings",
24
+ key,
25
+ author,
26
+ title,
27
+ booktitle,
28
+ journal: "",
29
+ });
30
+ const hit = (
31
+ title,
32
+ authors,
33
+ venue = "X",
34
+ type = "Conference and Workshop Papers",
35
+ ) => ({
36
+ title,
37
+ authors,
38
+ venue,
39
+ year: "2024",
40
+ type,
41
+ });
42
+
43
+ test("checkAuthors sorts every kind of entry into its bucket", async () => {
44
+ const parsed = [
45
+ entry("same", "Ada Lovelace and Alan Turing", "Same Paper"),
46
+ entry("dropped", "Ada Lovelace", "Dropped Author"),
47
+ entry("order", "Alan Turing and Ada Lovelace", "Order Swapped"),
48
+ entry("pre", "Ada Lovelace", "A Preprint", "arXiv preprint"),
49
+ entry("etal", "Ada Lovelace and others", "Truncated"),
50
+ entry("nomatch", "Ada Lovelace", "Nobody Has This"),
51
+ entry("onlycorr", "Ada Lovelace", "Only On CoRR"),
52
+ entry("down", "Ada Lovelace", "Service Down"),
53
+ {
54
+ type: "mdref",
55
+ key: "sw",
56
+ unparsed: true,
57
+ author: "",
58
+ title: "",
59
+ booktitle: "",
60
+ journal: "",
61
+ },
62
+ ];
63
+ const answers = {
64
+ "Same Paper": [hit("Same Paper.", ["Ada Lovelace", "Alan Turing"])],
65
+ "Dropped Author": [hit("Dropped Author", ["Ada Lovelace", "Alan Turing"])],
66
+ "Order Swapped": [hit("Order Swapped", ["Ada Lovelace", "Alan Turing"])],
67
+ "Nobody Has This": [hit("Something Else", ["X"])],
68
+ "Only On CoRR": [
69
+ hit("Only On CoRR", ["Ada Lovelace"], "CoRR", "Informal Publications"),
70
+ ],
71
+ };
72
+ const pauses = [];
73
+ const result = await checkAuthors(parsed, {
74
+ lookup: async (title) => {
75
+ if (title === "Service Down") throw new Error("DBLP 500");
76
+ return answers[title];
77
+ },
78
+ pause: (ms) => pauses.push(ms),
79
+ });
80
+ assert.deepEqual(
81
+ { ...result, pauses },
82
+ {
83
+ findings: [
84
+ {
85
+ key: "dropped",
86
+ venue: "X 2024",
87
+ ours: ["lovelace"],
88
+ theirs: ["lovelace", "turing"],
89
+ missing: ["turing"],
90
+ extra: [],
91
+ orderDiffers: false,
92
+ },
93
+ {
94
+ key: "order",
95
+ venue: "X 2024",
96
+ ours: ["turing", "lovelace"],
97
+ theirs: ["lovelace", "turing"],
98
+ missing: [],
99
+ extra: [],
100
+ orderDiffers: true,
101
+ },
102
+ ],
103
+ skipped: [
104
+ {
105
+ key: "sw",
106
+ why: "no author/title — a reference to software or a dataset, DBLP does not apply",
107
+ },
108
+ {
109
+ key: "etal",
110
+ why: "author list ends in `and others` — completeness not checkable",
111
+ },
112
+ { key: "nomatch", why: "no DBLP title match" },
113
+ { key: "onlycorr", why: "only a preprint record on DBLP" },
114
+ ],
115
+ unchecked: [{ key: "down", why: "DBLP lookup failed: DBLP 500" }],
116
+ matched: ["same"],
117
+ // 900 ms after each compared entry; 1500 and 3000 between the three failed attempts.
118
+ pauses: [900, 900, 900, 1500, 3000],
119
+ },
120
+ );
121
+ });
122
+
123
+ test("a lookup that fails once and then answers is retried, not reported", async () => {
124
+ let calls = 0;
125
+ const result = await checkAuthors([entry("k", "Ada Lovelace", "Flaky")], {
126
+ lookup: async () => {
127
+ if (calls++ === 0)
128
+ throw Object.assign(new Error("DBLP 429"), { retryable: true });
129
+ return [hit("Flaky", ["Ada Lovelace"])];
130
+ },
131
+ pause: () => {},
132
+ });
133
+ assert.deepEqual(result, {
134
+ findings: [],
135
+ skipped: [],
136
+ unchecked: [],
137
+ matched: ["k"],
138
+ });
139
+ });
140
+
141
+ test("the default pause really waits", async () => {
142
+ vi.useFakeTimers();
143
+ try {
144
+ const done = checkAuthors([entry("k", "Ada Lovelace", "T")], {
145
+ lookup: async () => [hit("T", ["Ada Lovelace"])],
146
+ });
147
+ await vi.advanceTimersByTimeAsync(900);
148
+ assert.deepEqual((await done).matched, ["k"]);
149
+ } finally {
150
+ vi.useRealTimers();
151
+ }
152
+ });
153
+
154
+ /** A stubbed fetch that answers every request with `status` and `body`, recording the URLs. */
155
+ function fakeDblp(status, body) {
156
+ const urls = [];
157
+ vi.stubGlobal("fetch", async (url) => {
158
+ urls.push(url);
159
+ return new Response(body === undefined ? null : JSON.stringify(body), {
160
+ status,
161
+ });
162
+ });
163
+ return urls;
164
+ }
165
+
166
+ test("dblpHits reads DBLP's hit list: one author or several, the disambiguation number dropped", async () => {
167
+ const urls = fakeDblp(200, {
168
+ result: {
169
+ hits: {
170
+ hit: [
171
+ {
172
+ info: {
173
+ title: "One",
174
+ venue: "ICSE",
175
+ year: "2024",
176
+ type: "Conference and Workshop Papers",
177
+ authors: { author: { text: "Ada Lovelace 0001" } },
178
+ },
179
+ },
180
+ {
181
+ info: {
182
+ title: "Two",
183
+ authors: { author: [{ text: "A B" }, { text: "C D" }] },
184
+ },
185
+ },
186
+ { info: {} },
187
+ {},
188
+ ],
189
+ },
190
+ },
191
+ });
192
+ assert.deepEqual(
193
+ { hits: await dblpHits("a title"), urls },
194
+ {
195
+ hits: [
196
+ {
197
+ venue: "ICSE",
198
+ year: "2024",
199
+ type: "Conference and Workshop Papers",
200
+ title: "One",
201
+ authors: ["Ada Lovelace"],
202
+ },
203
+ {
204
+ venue: "",
205
+ year: "",
206
+ type: "",
207
+ title: "Two",
208
+ authors: ["A B", "C D"],
209
+ },
210
+ { venue: "", year: "", type: "", title: "", authors: [] },
211
+ { venue: "", year: "", type: "", title: "", authors: [] },
212
+ ],
213
+ urls: ["https://dblp.org/search/publ/api/?q=a%20title&format=json&h=6"],
214
+ },
215
+ );
216
+ });
217
+
218
+ test("dblpHits: no hits is an empty list; 429 is retryable; other errors are not", async () => {
219
+ fakeDblp(200, {});
220
+ const none = await dblpHits("x");
221
+ fakeDblp(429);
222
+ const limited = await dblpHits("x").catch((e) => [e.message, e.retryable]);
223
+ fakeDblp(500);
224
+ const down = await dblpHits("x").catch((e) => [e.message, e.retryable]);
225
+ assert.deepEqual(
226
+ [none, limited, down],
227
+ [[], ["DBLP 429", true], ["DBLP 500", undefined]],
228
+ );
229
+ });
230
+
231
+ test("parseBib reads quoted and bare field values, and a field that is absent", () => {
232
+ assert.deepEqual(
233
+ parseBib(
234
+ '@article{k, author = "Ada Lovelace", title = {T}, journal = J, }',
235
+ ),
236
+ [
237
+ {
238
+ type: "article",
239
+ key: "k",
240
+ author: "Ada Lovelace",
241
+ title: "T",
242
+ booktitle: "",
243
+ journal: "J",
244
+ },
245
+ ],
246
+ );
247
+ });
248
+
249
+ test("parseBib: braces nested inside a value, and a bare last value with no comma after it", () => {
250
+ assert.deepEqual(
251
+ parseBib("@article{k, title = {The {BERT} Model}, journal = J}"),
252
+ [
253
+ {
254
+ type: "article",
255
+ key: "k",
256
+ author: "",
257
+ title: "The {BERT} Model",
258
+ booktitle: "",
259
+ journal: "J",
260
+ },
261
+ ],
262
+ );
263
+ });
264
+
265
+ test("surnames of nothing is nothing; a name with an empty family part contributes nothing", () => {
266
+ assert.deepEqual(
267
+ [surnames(""), surnames(", Ada and Lovelace")],
268
+ [[], ["lovelace"]],
269
+ );
270
+ });
271
+
272
+ test("an entry naming no venue at all does not claim to be published", () => {
273
+ assert.equal(claimsPublished({ booktitle: "", journal: "" }), false);
274
+ });
@@ -0,0 +1,178 @@
1
+ /**
2
+ * bib-authors.mjs as a process: which file it picks from a paper directory, the three
3
+ * bibliography sources it reads (.bib, a .tex's filecontents or its sibling refs.bib, a markdown
4
+ * reference list), and the report and exit code for each outcome. DBLP is a preload that answers
5
+ * by title from a table (the documented `result.hits.hit[].info` shape) — no network.
6
+ */
7
+ import assert from "node:assert/strict";
8
+ import { join } from "node:path";
9
+ import { dirname } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { test } from "vitest";
12
+ import { runNode, useTempDir, writeTree } from "../../../test/support.mjs";
13
+
14
+ const SCRIPT = join(dirname(fileURLToPath(import.meta.url)), "bib-authors.mjs");
15
+ const root = useTempDir("bib-authors-cli-");
16
+
17
+ const DBLP_ANSWERS = {
18
+ "Dropped Author": [["Ada Lovelace", "Alan Turing"]],
19
+ "Swapped Order": [["Ada Lovelace", "Alan Turing"]],
20
+ "All Good": [["Ada Lovelace"]],
21
+ };
22
+ const bib = (key, author, title, venue = "booktitle = {Proc. X}") =>
23
+ `@inproceedings{${key}, author = {${author}}, title = {${title}}, ${venue}}\n`;
24
+
25
+ writeTree(root, {
26
+ // The retry backoff (1.5 s, then 3 s) and the 900 ms courtesy pause are real sleeps in the
27
+ // script. Under CPU load they made "DBLP down" take 11.7 s once and time out. The preload
28
+ // stands in for the clock too: every timer fires at once, so no test here depends on time.
29
+ "dblp.mjs":
30
+ "const realSetTimeout = globalThis.setTimeout;\n" +
31
+ "globalThis.setTimeout = (fn, _ms, ...args) => realSetTimeout(fn, 0, ...args);\n" +
32
+ `const answers = ${JSON.stringify(DBLP_ANSWERS)};\n` +
33
+ "globalThis.fetch = async (url) => {\n" +
34
+ " const q = new URL(url).searchParams.get('q');\n" +
35
+ " if (q === 'Service Down') return new Response(null, { status: 500 });\n" +
36
+ " const hit = (answers[q] ?? []).map((authors) => ({ info: { title: q, venue: 'X', year: '2024',\n" +
37
+ " type: 'Conference and Workshop Papers', authors: { author: authors.map((text) => ({ text })) } } }));\n" +
38
+ " return new Response(JSON.stringify({ result: { hits: { hit } } }), { status: 200 });\n};\n",
39
+ "findings/paper.bib":
40
+ bib("dropped", "Ada Lovelace and Grace Hopper", "Dropped Author") +
41
+ bib("order", "Alan Turing and Ada Lovelace", "Swapped Order"),
42
+ "findings/other.bib": bib("never", "X", "Never Read"),
43
+ "twotex/b.tex": "\\documentclass{article}\n",
44
+ "twotex/a.tex":
45
+ "\\begin{filecontents*}{refs.bib}\n" +
46
+ bib("pre", "Ada Lovelace", "A Preprint", "journal = {arXiv preprint}") +
47
+ "\\end{filecontents*}\n",
48
+ "sibling/paper.tex": "\\documentclass{article}\n",
49
+ "sibling/refs.bib": bib("good", "Ada Lovelace", "All Good"),
50
+ "nobib/paper.tex": "\\documentclass{article}\n",
51
+ "md/paper.md":
52
+ "# P\n\n## References\n\n1. A. Lovelace. *All Good.* Proc. X, 2024.\n2. some-npm-package, https://npm.example\n",
53
+ "down/refs.bib": bib("down", "Ada Lovelace", "Service Down"),
54
+ "empty/.keep": "",
55
+ });
56
+
57
+ const run = (...args) => {
58
+ const r = runNode(SCRIPT, args, {
59
+ nodeArgs: ["--import", join(root, "dblp.mjs")],
60
+ });
61
+ return {
62
+ ...r,
63
+ stdout: r.stdout.replaceAll(root, "<root>"),
64
+ stderr: r.stderr.replaceAll(root, "<root>"),
65
+ };
66
+ };
67
+
68
+ test("refusals: no argument, no such path, a directory with nothing to read, a .tex with no bibliography", () => {
69
+ assert.deepEqual(
70
+ [
71
+ run(),
72
+ run(join(root, "nope")),
73
+ run(join(root, "empty")),
74
+ run(join(root, "nobib", "paper.tex")),
75
+ ].map(({ status, stdout, stderr }) => ({ status, stdout, stderr })),
76
+ [
77
+ {
78
+ status: 2,
79
+ stdout: "",
80
+ stderr:
81
+ "bib-authors: usage: bib-authors.mjs <paper-dir|file.bib|file.tex> [--json]\n",
82
+ },
83
+ {
84
+ status: 2,
85
+ stdout: "",
86
+ stderr: "bib-authors: no such path: <root>/nope\n",
87
+ },
88
+ {
89
+ status: 2,
90
+ stdout: "",
91
+ stderr: "bib-authors: no .bib, .tex or paper.md in <root>/empty\n",
92
+ },
93
+ {
94
+ status: 2,
95
+ stdout: "",
96
+ stderr:
97
+ "bib-authors: no bibliography found in <root>/nobib/paper.tex (no filecontents block, no refs.bib beside it)\n",
98
+ },
99
+ ],
100
+ );
101
+ });
102
+
103
+ test("a paper directory: the canonical paper.bib is read, and each difference is printed — FAIL, exit 1", () => {
104
+ assert.deepEqual(run(join(root, "findings")), {
105
+ status: 1,
106
+ stderr: "",
107
+ stdout: [
108
+ "== bib-authors: <root>/findings/paper.bib ==",
109
+ "",
110
+ " 🔴 dropped (DBLP: X 2024)",
111
+ " MISSING from ours : turing",
112
+ " EXTRA in ours : hopper",
113
+ "",
114
+ " 🔴 order (DBLP: X 2024)",
115
+ " ORDER differs",
116
+ " ours : turing > lovelace",
117
+ " DBLP : lovelace > turing",
118
+ "",
119
+ "-- 2 entries · 2 difference(s) · 0 not applicable · 0 NOT CHECKED",
120
+ "FAIL: our author list disagrees with the version we claim to cite.",
121
+ "",
122
+ ].join("\n"),
123
+ });
124
+ });
125
+
126
+ test("two .tex files and no paper.tex: the first by name, said out loud; its filecontents is read", () => {
127
+ const r = run(join(root, "twotex"), "--json");
128
+ assert.deepEqual(
129
+ { status: r.status, stderr: r.stderr, report: JSON.parse(r.stdout) },
130
+ {
131
+ status: 0,
132
+ stderr:
133
+ "⚠️ <root>/twotex holds several .tex files and no canonical paper.tex: a.tex, b.tex — took a.tex. If that is the wrong one, name the file explicitly.\n",
134
+ report: {
135
+ file: "<root>/twotex/a.tex",
136
+ entries: 1,
137
+ findings: [],
138
+ skipped: [],
139
+ unchecked: [],
140
+ },
141
+ },
142
+ );
143
+ });
144
+
145
+ test("a .tex with no filecontents reads the refs.bib beside it — PASS, exit 0", () => {
146
+ const r = run(join(root, "sibling", "paper.tex"));
147
+ assert.deepEqual(r, {
148
+ status: 0,
149
+ stderr: "",
150
+ stdout:
151
+ "== bib-authors: <root>/sibling/refs.bib ==\n\n-- 1 entries · 0 difference(s) · 0 not applicable · 0 NOT CHECKED\nPASS: no author-list disagreement.\n",
152
+ });
153
+ });
154
+
155
+ test("a markdown reference list: a software entry is not applicable, not dropped", () => {
156
+ const r = run(join(root, "md"));
157
+ assert.deepEqual(r, {
158
+ status: 0,
159
+ stderr: "",
160
+ stdout:
161
+ "== bib-authors: <root>/md/paper.md ==\n" +
162
+ " · n/a ref2 — no author/title — a reference to software or a dataset, DBLP does not apply\n\n" +
163
+ "-- 1 entries · 0 difference(s) · 1 not applicable · 0 NOT CHECKED\nPASS: no author-list disagreement.\n",
164
+ });
165
+ });
166
+
167
+ test("DBLP down: NOT CHECKED, PARTIAL, exit 2 — never a pass", () => {
168
+ const r = run(join(root, "down"));
169
+ assert.deepEqual(r, {
170
+ status: 2,
171
+ stderr: "",
172
+ stdout:
173
+ "== bib-authors: <root>/down/refs.bib ==\n" +
174
+ " ⚠️ NOT CHECKED down — DBLP lookup failed: DBLP 500\n\n" +
175
+ "-- 1 entries · 0 difference(s) · 0 not applicable · 1 NOT CHECKED\n" +
176
+ "PARTIAL: no disagreement among the entries reached, but 1 could not be checked — this is NOT a pass.\n",
177
+ });
178
+ });