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.
- package/.github/workflows/ci.yml +28 -41
- package/.github/workflows/dependabot-automerge.yml +1 -1
- package/CLAUDE.md +21 -105
- package/CONTRIBUTING.md +23 -25
- package/README.md +11 -7
- package/bin/paperlint.test.mjs +45 -0
- package/dist/adapters/node/files.io.d.ts +2 -0
- package/dist/adapters/node/files.io.d.ts.map +1 -1
- package/dist/adapters/node/files.io.js +3 -2
- package/dist/adapters/node/files.io.js.map +1 -1
- package/dist/adapters/pdfjs/fill.d.ts +15 -4
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
- package/dist/adapters/pdfjs/fill.js +31 -21
- package/dist/adapters/pdfjs/fill.js.map +1 -1
- package/dist/adapters/references/clock.io.d.ts +5 -0
- package/dist/adapters/references/clock.io.d.ts.map +1 -0
- package/dist/adapters/references/clock.io.js +7 -0
- package/dist/adapters/references/clock.io.js.map +1 -0
- package/dist/adapters/references/index.d.ts +24 -2
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +131 -12
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/adapters/references/pool.d.ts +8 -0
- package/dist/adapters/references/pool.d.ts.map +1 -0
- package/dist/adapters/references/pool.js +19 -0
- package/dist/adapters/references/pool.js.map +1 -0
- package/dist/build.d.ts +14 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +17 -11
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +27 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +71 -55
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +27 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +4 -3
- package/dist/doctor.js.map +1 -1
- package/dist/domain/lookup-cache.d.ts +75 -0
- package/dist/domain/lookup-cache.d.ts.map +1 -0
- package/dist/domain/lookup-cache.js +137 -0
- package/dist/domain/lookup-cache.js.map +1 -0
- package/dist/domain/result.d.ts +0 -2
- package/dist/domain/result.d.ts.map +1 -1
- package/dist/domain/result.js +0 -4
- package/dist/domain/result.js.map +1 -1
- package/dist/hooks-settings.d.ts +8 -5
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +32 -32
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +8 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +3 -3
- package/dist/init.js.map +1 -1
- package/dist/latex-log.d.ts +0 -2
- package/dist/latex-log.d.ts.map +1 -1
- package/dist/latex-log.js +7 -13
- package/dist/latex-log.js.map +1 -1
- package/dist/link-skills.d.ts +15 -2
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +36 -20
- package/dist/link-skills.js.map +1 -1
- package/dist/paper-files.d.ts.map +1 -1
- package/dist/paper-files.js +6 -4
- package/dist/paper-files.js.map +1 -1
- package/dist/pdf-facts.d.ts +11 -0
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +16 -18
- package/dist/pdf-facts.js.map +1 -1
- package/dist/pdf-geometry.js +1 -1
- package/dist/pdf-geometry.js.map +1 -1
- package/dist/ports/check-references.d.ts +12 -2
- package/dist/ports/check-references.d.ts.map +1 -1
- package/dist/ports/check-references.js +0 -5
- package/dist/ports/check-references.js.map +1 -1
- package/dist/presets.d.ts +2 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +3 -1
- package/dist/presets.js.map +1 -1
- package/dist/references.d.ts +11 -3
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +63 -15
- package/dist/references.js.map +1 -1
- package/dist/tex-requirements.d.ts +8 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +6 -5
- package/dist/tex-requirements.js.map +1 -1
- package/dist/toolchain.d.ts +3 -2
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +4 -5
- package/dist/toolchain.js.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +11 -9
- package/dist/venue-rules.js.map +1 -1
- package/docs/prior-art/README.md +11 -10
- package/docs/prior-art/citation-checkers.md +174 -0
- package/docs/prior-art/test-tooling.md +7 -30
- package/docs/references.md +126 -0
- package/docs/testing.md +118 -0
- package/docs/toolchain.md +6 -4
- package/eslint-rules/install-path-literals.harness.mjs +17 -1
- package/eslint-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.mjs +12 -9
- package/eslint-rules/latex-language.test.mjs +200 -0
- package/eslint-rules/paper-research-question.harness.mjs +73 -7
- package/eslint-rules/paper-stages.harness.mjs +9 -14
- package/eslint-rules/paper-stages.test.mjs +135 -0
- package/eslint-rules/paper-typography.mjs +9 -2
- package/eslint-rules/paper-typography.test.mjs +146 -1
- package/eslint-rules/pdf-last-page-balance.harness.mjs +63 -7
- package/eslint-rules/review-frontmatter.mjs +2 -1
- package/eslint-rules/review-frontmatter.test.mjs +37 -1
- package/eslint-rules/temp-root-realpath.harness.mjs +49 -0
- package/eslint-rules/tex-build.mjs +13 -13
- package/eslint.config.mjs +6 -2
- package/hooks/hooks.harness.mjs +6 -11
- package/hooks/paper-edit-guard.hook.mjs +2 -2
- package/hooks/paper-edit-guard.test.mjs +61 -0
- package/hooks/paper-skills-nudge.hook.mjs +2 -2
- package/hooks/paper-skills-nudge.test.mjs +38 -0
- package/hooks/paper-status-gates.test.mjs +42 -0
- package/lib/agent-cli-version.harness.mjs +7 -0
- package/lib/check.d.mts +17 -0
- package/lib/check.mjs +50 -0
- package/lib/check.test.ts +67 -0
- package/lib/markdown.mjs +16 -69
- package/lib/markdown.test.mjs +246 -0
- package/lib/paper-config.harness.mjs +28 -9
- package/lib/paper-config.mjs +7 -2
- package/lib/skill-checks.mjs +18 -46
- package/lib/skill-checks.test.mjs +297 -0
- package/lib/skill-corpus.mjs +11 -13
- package/lib/skill-corpus.test.mjs +54 -0
- package/lib/skill-trigger-cases.harness.mjs +57 -4
- package/package.json +6 -5
- package/scripts/check.harness.mjs +4 -10
- package/scripts/check.mjs +45 -42
- package/scripts/check.test.mjs +104 -0
- package/scripts/coverage-config.test.ts +406 -0
- package/scripts/eslint-report-guard.test.mjs +48 -0
- package/scripts/harness-api.frozen.json +0 -2
- package/scripts/layer-legacy-frozen.d.mts +9 -1
- package/scripts/layer-legacy-frozen.mjs +28 -11
- package/scripts/layer-legacy-frozen.test.ts +143 -1
- package/scripts/rules-are-content-only.harness.mjs +3 -11
- package/scripts/rules-are-content-only.mjs +29 -19
- package/scripts/rules-are-content-only.test.mjs +61 -0
- package/scripts/rules-see-files.mjs +11 -9
- package/scripts/rules-see-files.test.mjs +71 -0
- package/scripts/test.mjs +44 -0
- package/scripts/test.test.mjs +52 -0
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.mjs.snap +57 -0
- package/skills/grade-paper-writing/prose-lint.mjs +31 -12
- package/skills/grade-paper-writing/prose-lint.test.mjs +153 -0
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +3 -3
- package/skills/paper-pipeline/references/artifact-checklist.md +7 -7
- package/skills/paper-pipeline/scripts/README.md +13 -132
- package/skills/paper-pipeline/scripts/announce.test.mjs +102 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.test.mjs +86 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +8 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +3 -3
- package/skills/paper-pipeline/scripts/consumer.mjs +13 -25
- package/skills/paper-pipeline/scripts/consumer.test.mjs +113 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +9 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +380 -0
- package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +71 -0
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +4 -4
- package/skills/paper-pipeline/scripts/generated-code.mjs +3 -2
- package/skills/paper-pipeline/scripts/generated-code.test.mjs +62 -0
- package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +302 -0
- package/skills/paper-pipeline/scripts/ledger.mjs +1 -1
- package/skills/paper-pipeline/scripts/{ledger.selftest.mjs → ledger.test.mjs} +44 -76
- package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +474 -0
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -63
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +37 -21
- package/skills/paper-pipeline/scripts/pipeline-edges.mjs +2 -2
- package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +73 -0
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +4 -47
- package/skills/paper-pipeline/scripts/population-map.mjs +12 -2
- package/skills/paper-pipeline/scripts/population-map.test.mjs +154 -0
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +26 -0
- package/skills/paper-pipeline/scripts/round-diff.harness.mjs +1 -2
- package/skills/paper-pipeline/scripts/round-diff.mjs +13 -6
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +159 -0
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +30 -6
- package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +468 -0
- package/skills/paper-pipeline/scripts/status.mjs +2 -2
- package/skills/paper-pipeline/scripts/status.test.mjs +232 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +2 -2
- package/skills/render-paper/checkers-requirements.txt +30 -19
- package/skills/render-paper/ensure-checkers.sh +20 -4
- package/skills/render-paper/extract-pdf-facts.harness.mjs +56 -7
- package/skills/tighten-paper/__snapshots__/structure.test.mjs.snap +133 -0
- package/skills/tighten-paper/structure.test.mjs +116 -0
- package/skills/verify-citations/SKILL.md +30 -14
- package/skills/verify-citations/SKILL.md.spec.ts +29 -13
- package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +253 -0
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +274 -0
- package/skills/verify-citations/scripts/bib-authors.cli.test.mjs +178 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +6 -6
- package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
- package/skills/verify-citations/scripts/verify-cites.mjs +360 -215
- package/skills/verify-citations/scripts/verify-cites.net.test.mjs +588 -0
- package/skills/verify-citations/scripts/verify-cites.test.mjs +151 -5
- package/src/adapters/banal/failure.test.ts +70 -1
- package/src/adapters/banal/index.test.ts +16 -0
- package/src/adapters/banal/output.test.ts +5 -0
- package/src/adapters/curl/download.test.ts +43 -1
- package/src/adapters/memory/index.test.ts +25 -0
- package/src/adapters/node/files.io.ts +3 -2
- package/src/adapters/node/files.test.ts +13 -1
- package/src/adapters/node/process.test.ts +38 -0
- package/src/adapters/pdfjs/fill.test.ts +34 -0
- package/src/adapters/pdfjs/fill.ts +44 -25
- package/src/adapters/references/clock.io.ts +8 -0
- package/src/adapters/references/index.test.ts +560 -0
- package/src/adapters/references/index.ts +203 -19
- package/src/adapters/references/pool.test.ts +31 -0
- package/src/adapters/references/pool.ts +24 -0
- package/src/build-engine.harness.mjs +3 -7
- package/src/build-engine.test.ts +137 -0
- package/src/build.harness.mjs +5 -9
- package/src/build.test.ts +362 -0
- package/src/build.ts +23 -12
- package/src/cli.harness.mjs +10 -17
- package/src/cli.test.ts +584 -0
- package/src/cli.ts +112 -66
- package/src/doctor.harness.mjs +3 -8
- package/src/doctor.test.ts +64 -0
- package/src/doctor.ts +12 -4
- package/src/domain/lookup-cache.test.ts +178 -0
- package/src/domain/lookup-cache.ts +202 -0
- package/src/domain/result.test.ts +4 -0
- package/src/domain/result.ts +0 -5
- package/src/engine.harness.mjs +27 -7
- package/src/facts-file.test.ts +110 -0
- package/src/hooks-settings.harness.mjs +43 -13
- package/src/hooks-settings.ts +41 -35
- package/src/init.test.ts +308 -5
- package/src/init.ts +3 -3
- package/src/latex-log.harness.mjs +32 -7
- package/src/latex-log.ts +6 -13
- package/src/latex-loop.harness.mjs +8 -7
- package/src/link-skills.harness.mjs +3 -8
- package/src/link-skills.test.ts +105 -0
- package/src/link-skills.ts +75 -21
- package/src/new-paper.harness.mjs +3 -7
- package/src/new-paper.test.ts +31 -1
- package/src/paper-files.test.ts +12 -0
- package/src/paper-files.ts +9 -5
- package/src/paper-settings.test.ts +20 -0
- package/src/pdf-facts.harness.mjs +3 -6
- package/src/pdf-facts.test.ts +144 -0
- package/src/pdf-facts.ts +17 -19
- package/src/pdf-geometry.harness.mjs +3 -7
- package/src/pdf-geometry.ts +1 -1
- package/src/ports/check-references.ts +16 -2
- package/src/presets.test.ts +2 -0
- package/src/presets.ts +7 -1
- package/src/references.test.ts +257 -10
- package/src/references.ts +93 -17
- package/src/rules-config.harness.mjs +10 -8
- package/src/structure.harness.mjs +3 -7
- package/src/tex-requirements.harness.mjs +34 -7
- package/src/tex-requirements.ts +12 -6
- package/src/toolchain.harness.mjs +3 -7
- package/src/toolchain.test.ts +280 -0
- package/src/toolchain.ts +8 -7
- package/src/venue-rules.test.ts +60 -0
- package/src/venue-rules.ts +13 -11
- package/action.mutations.mjs +0 -162
- package/eslint-rules/install-path-literals.mutations.mjs +0 -62
- package/eslint-rules/latex-language.mutations.mjs +0 -196
- package/eslint-rules/paper-research-question.mutations.mjs +0 -127
- package/eslint-rules/paper-stages.mutations.mjs +0 -81
- package/eslint-rules/papers.mutations.mjs +0 -186
- package/eslint-rules/temp-root-realpath.mutations.mjs +0 -99
- package/eslint-rules/tex-build.mutations.mjs +0 -258
- package/hooks/hooks.mutations.mjs +0 -337
- package/lib/agent-cli-version.mutations.mjs +0 -109
- package/lib/mutation-driver.harness.mjs +0 -227
- package/lib/mutation-driver.mjs +0 -397
- package/lib/mutation-driver.mutations.mjs +0 -68
- package/lib/paper-config.mutations.mjs +0 -143
- package/lib/skill-trigger-cases.mutations.mjs +0 -65
- package/scripts/check.mutations.mjs +0 -110
- package/scripts/exclusive.mjs +0 -138
- package/scripts/mutation-batteries-frozen.harness.mjs +0 -204
- package/scripts/mutation-batteries-frozen.mjs +0 -238
- package/scripts/mutation-batteries.frozen.json +0 -108
- package/scripts/rules-are-content-only.mutations.mjs +0 -81
- package/scripts/rules-see-files.mutations.mjs +0 -131
- package/scripts/run-mutations.mjs +0 -100
- package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +0 -218
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +0 -190
- package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +0 -254
- package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/population-map.mutations.mjs +0 -65
- package/skills/paper-pipeline/scripts/population-map.selftest.mjs +0 -122
- package/skills/paper-pipeline/scripts/provenance.mutations.mjs +0 -59
- package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +0 -276
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +0 -65
- package/skills/skill-contract.mutations.mjs +0 -179
- package/src/build.mutations.mjs +0 -196
- package/src/cli.mutations.mjs +0 -414
- package/src/doctor.mutations.mjs +0 -175
- package/src/hooks-settings.mutations.mjs +0 -99
- package/src/link-skills.mutations.mjs +0 -140
- package/src/new-paper.mutations.mjs +0 -65
- package/src/structure.mutations.mjs +0 -83
|
@@ -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
|
+
});
|
|
@@ -28,11 +28,10 @@
|
|
|
28
28
|
*
|
|
29
29
|
* ⚠️ Discovery credit and a deliberate NON-dependency: this class was found by running
|
|
30
30
|
* `rebiber` (yuchenlin/rebiber), which rewrites bib entries to their DBLP records. We do
|
|
31
|
-
* NOT depend on it here.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* we actually needed was the comparison, and that is one HTTPS call with no dependencies.
|
|
31
|
+
* NOT depend on it here. Re-measured 2026-09-27 on 1.4.0 (GitHub b917e36): it installs cleanly
|
|
32
|
+
* now but carries 409 MB of venue data, its bundled data still misses entries live DBLP finds,
|
|
33
|
+
* and what it does is REWRITE an entry — we need the comparison of an author list against the
|
|
34
|
+
* published record, and that is one HTTPS call with no dependencies.
|
|
36
35
|
*
|
|
37
36
|
* 🔴 The normalisation below is load-bearing, not tidiness. A first cut of this comparison
|
|
38
37
|
* reported 13 mismatches on our bibliography, of which ELEVEN were "Last, First" vs
|
|
@@ -278,7 +277,8 @@ export function surnames(authorField) {
|
|
|
278
277
|
.filter((s) => s && s !== "others");
|
|
279
278
|
}
|
|
280
279
|
|
|
281
|
-
|
|
280
|
+
// Only called for an entry that has an author (checkAuthors filters on it first).
|
|
281
|
+
export const truncated = (authorField) => /\band\s+others\b/i.test(authorField);
|
|
282
282
|
|
|
283
283
|
/** Is the entry claiming a PUBLISHED venue (as opposed to a preprint)? */
|
|
284
284
|
export function claimsPublished(e) {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verify-cites.mjs as a process — the way the verify-citations skill runs it, which is THROUGH A
|
|
3
|
+
* SYMLINK (`.claude/skills/verify-citations -> …/skills/verify-citations`, made by `paperlint
|
|
4
|
+
* init`). Only `--offline` runs here: the online resolvers are driven in-process with a fake
|
|
5
|
+
* `fetch` (verify-cites.net.test.mjs), because a test must not depend on four live APIs.
|
|
6
|
+
*/
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join } 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 HERE = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const root = useTempDir("verify-cites-cli-");
|
|
16
|
+
// The skill's own address for the script: a link to the skill directory, as `paperlint init` makes.
|
|
17
|
+
symlinkSync(join(HERE, ".."), join(root, "verify-citations"));
|
|
18
|
+
const LINKED = join(root, "verify-citations", "scripts", "verify-cites.mjs");
|
|
19
|
+
|
|
20
|
+
test("run through the skill's symlink, the CLI runs at all (--help prints the usage)", () => {
|
|
21
|
+
assert.deepEqual(runNode(LINKED, ["--help"]), {
|
|
22
|
+
status: 0,
|
|
23
|
+
stdout: "",
|
|
24
|
+
stderr: "usage: verify-cites.mjs <cites.json | refs.bib | -> [--offline]\n",
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const SCRIPT = join(HERE, "verify-cites.mjs");
|
|
29
|
+
const CACHE = join(root, "cache.json");
|
|
30
|
+
const env = { VERIFY_CITES_CACHE: CACHE };
|
|
31
|
+
writeTree(root, {
|
|
32
|
+
"cites.json": JSON.stringify([
|
|
33
|
+
{ id: "a", doi: "10.1/x", title: "A Title Of Some Length" },
|
|
34
|
+
{ title: "no id at all" },
|
|
35
|
+
{ id: "h", title: "Hygiene", commit: "abc123" },
|
|
36
|
+
]),
|
|
37
|
+
"one.json": JSON.stringify({ id: "solo", title: "Just One Citation" }),
|
|
38
|
+
"refs.bib": "@article{k, title={A Bib Title}, doi={10.2/y}, year={2020}}\n",
|
|
39
|
+
"bad.json": "{ not json",
|
|
40
|
+
// Loaded with --import before the script: every registry answers 404, so a DOI the authority
|
|
41
|
+
// does not know is FALSE — the one verdict that fails the run.
|
|
42
|
+
"fetch-404.mjs":
|
|
43
|
+
"globalThis.fetch = async () => new Response(null, { status: 404 });\n",
|
|
44
|
+
// Prints the User-Agent of every request, then answers 404.
|
|
45
|
+
"fetch-ua.mjs":
|
|
46
|
+
"globalThis.fetch = async (url, init) => {\n" +
|
|
47
|
+
" process.stderr.write(`UA ${init.headers['User-Agent']}\\n`);\n" +
|
|
48
|
+
" return new Response(null, { status: 404 });\n};\n",
|
|
49
|
+
"consumer/paperlint.json": JSON.stringify({ contactEmail: "me@example.org" }),
|
|
50
|
+
});
|
|
51
|
+
const offline = (args, opts = {}) =>
|
|
52
|
+
runNode(SCRIPT, [...args, "--offline"], { env, ...opts });
|
|
53
|
+
const parsed = (r) => ({ ...r, stdout: JSON.parse(r.stdout) });
|
|
54
|
+
const SUMMARY_PASS = (n, t, u, flagged) =>
|
|
55
|
+
`\nverify-cites: ${n} citation(s) — ${t} true, 0 false (fabrication), ${u} unresolvable` +
|
|
56
|
+
(flagged ? `, ${flagged} with hygiene flag(s)` : "") +
|
|
57
|
+
"\nPASS — no fabrication (unresolvable is advisory)\n";
|
|
58
|
+
|
|
59
|
+
test("-h is --help", () => {
|
|
60
|
+
assert.equal(
|
|
61
|
+
runNode(SCRIPT, ["-h"]).stderr,
|
|
62
|
+
runNode(SCRIPT, ["--help"]).stderr,
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("an input that cannot be read: exit 2, named", () => {
|
|
67
|
+
const r = offline([join(root, "bad.json")]);
|
|
68
|
+
assert.deepEqual(
|
|
69
|
+
{ status: r.status, stdout: r.stdout, stderr: r.stderr.split(" — ")[0] },
|
|
70
|
+
{ status: 2, stdout: "", stderr: "error: could not read input" },
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("offline over a JSON list: a citation with no id is unresolvable by name, a short SHA is flagged", () => {
|
|
75
|
+
const r = parsed(offline([join(root, "cites.json")]));
|
|
76
|
+
assert.deepEqual(
|
|
77
|
+
{
|
|
78
|
+
status: r.status,
|
|
79
|
+
ids: r.stdout.map((x) => [x.id, x.verdict, x.flags?.length ?? 0]),
|
|
80
|
+
stderr: r.stderr,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
status: 0,
|
|
84
|
+
ids: [
|
|
85
|
+
["a", "unresolvable", 0],
|
|
86
|
+
[null, "unresolvable", 0],
|
|
87
|
+
["h", "unresolvable", 1],
|
|
88
|
+
],
|
|
89
|
+
stderr: SUMMARY_PASS(3, 0, 3, 1),
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("one JSON object on stdin, with `-` and with no path at all", () => {
|
|
95
|
+
const input = readFileSync(join(root, "one.json"), "utf8");
|
|
96
|
+
const dash = parsed(offline(["-"], { input }));
|
|
97
|
+
const bare = parsed(offline([], { input }));
|
|
98
|
+
assert.deepEqual(
|
|
99
|
+
[dash.stdout.map((x) => x.id), bare.stdout.map((x) => x.id), dash.status],
|
|
100
|
+
[["solo"], ["solo"], 0],
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("a .bib file is parsed into citations", () => {
|
|
105
|
+
const r = parsed(offline([join(root, "refs.bib")]));
|
|
106
|
+
assert.deepEqual(
|
|
107
|
+
r.stdout.map((x) => x.id),
|
|
108
|
+
["k"],
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("online, a DOI the authority does not know is FALSE: FAIL, exit 1, and the cache is written", () => {
|
|
113
|
+
rmSync(CACHE, { force: true });
|
|
114
|
+
const r = runNode(SCRIPT, [join(root, "refs.bib")], {
|
|
115
|
+
env,
|
|
116
|
+
nodeArgs: ["--import", join(root, "fetch-404.mjs")],
|
|
117
|
+
});
|
|
118
|
+
assert.deepEqual(
|
|
119
|
+
{
|
|
120
|
+
status: r.status,
|
|
121
|
+
verdicts: JSON.parse(r.stdout).map((x) => x.verdict),
|
|
122
|
+
tail: r.stderr.trim().split("\n").pop(),
|
|
123
|
+
cached: Object.keys(JSON.parse(readFileSync(CACHE, "utf8"))).sort(),
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
status: 1,
|
|
127
|
+
verdicts: ["false"],
|
|
128
|
+
tail: "FAIL — fabrication(s) found (exit 1)",
|
|
129
|
+
// 404s are successful transports, so they are cached; the authority's 100 is not.
|
|
130
|
+
cached: [
|
|
131
|
+
"crossref:doi:10.2/y",
|
|
132
|
+
"openalex:doi:10.2/y",
|
|
133
|
+
"semantic_scholar:doi:10.2/y",
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("a corrupt cache is ignored, and a cache that cannot be written does not fail the run", () => {
|
|
140
|
+
writeFileSync(CACHE, "{ corrupt");
|
|
141
|
+
const corrupt = offline([join(root, "one.json")]);
|
|
142
|
+
const unwritable = runNode(SCRIPT, [join(root, "one.json"), "--offline"], {
|
|
143
|
+
env: { VERIFY_CITES_CACHE: root }, // a directory: reading and writing both fail
|
|
144
|
+
});
|
|
145
|
+
assert.deepEqual([corrupt.status, unwritable.status], [0, 0]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("the consumer's declared contact reaches every request's User-Agent (Crossref's polite pool)", () => {
|
|
149
|
+
const r = runNode(SCRIPT, [join(root, "refs.bib")], {
|
|
150
|
+
env: { ...env, CLAUDE_PROJECT_DIR: join(root, "consumer") },
|
|
151
|
+
nodeArgs: ["--import", join(root, "fetch-ua.mjs")],
|
|
152
|
+
});
|
|
153
|
+
const agents = new Set(
|
|
154
|
+
r.stderr.split("\n").filter((l) => l.startsWith("UA ")),
|
|
155
|
+
);
|
|
156
|
+
assert.deepEqual(
|
|
157
|
+
[...agents],
|
|
158
|
+
["UA verify-cites/1.0 (citation gate; mailto:me@example.org)"],
|
|
159
|
+
);
|
|
160
|
+
});
|