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,560 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The online references adapter — verify-cites' existence check and bib-authors' author check
|
|
3
|
+
* over one bibliography, merged per entry — driven with a fake `fetch` that answers every service
|
|
4
|
+
* by URL. DBLP's gentle-client pauses run under fake timers.
|
|
5
|
+
*/
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { afterEach, test, vi } from "vitest";
|
|
8
|
+
import { dblpTitleKey, onlineReferences, referencesChecker } from "./index.ts";
|
|
9
|
+
import {
|
|
10
|
+
EMPTY_LOOKUP_CACHE,
|
|
11
|
+
type LookupCache,
|
|
12
|
+
} from "../../domain/lookup-cache.ts";
|
|
13
|
+
// @ts-expect-error — a skill script in .mjs, it has no types
|
|
14
|
+
import * as cites from "../../../skills/verify-citations/scripts/verify-cites.mjs";
|
|
15
|
+
|
|
16
|
+
/** The verdicts of a cold run — an empty cache, the adapter the CLI wires. */
|
|
17
|
+
const checkOnly = async (bib: string) =>
|
|
18
|
+
(await onlineReferences(bib, EMPTY_LOOKUP_CACHE)).check;
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.unstubAllGlobals();
|
|
22
|
+
vi.useRealTimers();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const json = (status: number, body?: unknown) =>
|
|
26
|
+
new Response(body === undefined ? null : JSON.stringify(body), { status });
|
|
27
|
+
const dblp = (title: string, authors: string[]) =>
|
|
28
|
+
json(200, {
|
|
29
|
+
result: {
|
|
30
|
+
hits: {
|
|
31
|
+
hit: [
|
|
32
|
+
{
|
|
33
|
+
info: {
|
|
34
|
+
title,
|
|
35
|
+
venue: "ICSE",
|
|
36
|
+
year: "2024",
|
|
37
|
+
type: "Conference and Workshop Papers",
|
|
38
|
+
authors: { author: authors.map((text) => ({ text })) },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** Route by URL prefix; the longest prefix wins. */
|
|
47
|
+
function fakeFetch(routes: Record<string, () => Response>): string[] {
|
|
48
|
+
const calls: string[] = [];
|
|
49
|
+
vi.stubGlobal("fetch", async (url: string) => {
|
|
50
|
+
calls.push(url);
|
|
51
|
+
const hit = Object.keys(routes)
|
|
52
|
+
.filter((p) => url.startsWith(p))
|
|
53
|
+
.sort((a, b) => b.length - a.length)[0];
|
|
54
|
+
if (hit === undefined) return json(404);
|
|
55
|
+
return routes[hit]!();
|
|
56
|
+
});
|
|
57
|
+
return calls;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Await `p` while fake time runs, so DBLP's pauses between requests elapse at once. */
|
|
61
|
+
async function settle<T>(p: Promise<T>): Promise<T> {
|
|
62
|
+
let done = false;
|
|
63
|
+
const out = p.finally(() => (done = true));
|
|
64
|
+
while (!done) await vi.advanceTimersByTimeAsync(1000);
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
test("no service answers: not-checked, with the reason", async () => {
|
|
69
|
+
vi.stubGlobal("fetch", async () => {
|
|
70
|
+
throw new Error("getaddrinfo ENOTFOUND api.crossref.org");
|
|
71
|
+
});
|
|
72
|
+
assert.deepEqual(await checkOnly("@misc{k, title={T}}"), {
|
|
73
|
+
kind: "not-checked",
|
|
74
|
+
why: "the citation services cannot be reached (getaddrinfo ENOTFOUND api.crossref.org)",
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("each entry gets its existence and its authors: confirmed, fabricated, mismatched, unchecked, skipped", async () => {
|
|
79
|
+
vi.useFakeTimers();
|
|
80
|
+
fakeFetch({
|
|
81
|
+
"https://api.crossref.org/works/10.1%2Fgood": () =>
|
|
82
|
+
json(200, {
|
|
83
|
+
message: { title: ["Good Paper"], issued: { "date-parts": [[2024]] } },
|
|
84
|
+
}),
|
|
85
|
+
"https://doi.org/api/handles/10.1/good": () =>
|
|
86
|
+
json(200, { responseCode: 1 }),
|
|
87
|
+
"https://doi.org/api/handles/10.1/fake": () => json(404),
|
|
88
|
+
"https://dblp.org/search/publ/api/?q=Good%20Paper": () =>
|
|
89
|
+
dblp("Good Paper", ["Ada Lovelace"]),
|
|
90
|
+
"https://dblp.org/search/publ/api/?q=Drift%20Paper": () =>
|
|
91
|
+
dblp("Drift Paper", ["Ada Lovelace", "Alan Turing"]),
|
|
92
|
+
"https://dblp.org/search/publ/api/?q=Flaky%20Paper": () => json(500),
|
|
93
|
+
"https://api.crossref.org/": () => json(200),
|
|
94
|
+
// Any other title: DBLP knows nothing, so the author check does not apply.
|
|
95
|
+
"https://dblp.org/": () => json(200, {}),
|
|
96
|
+
});
|
|
97
|
+
const bib = [
|
|
98
|
+
"@inproceedings{good, author={Ada Lovelace}, title={Good Paper}, booktitle={ICSE}, doi={10.1/good}}",
|
|
99
|
+
"@inproceedings{fake, author={Ada Lovelace}, title={Fake Paper}, booktitle={ICSE}, doi={10.1/fake}}",
|
|
100
|
+
"@inproceedings{drift, author={Ada Lovelace}, title={Drift Paper}, booktitle={ICSE}}",
|
|
101
|
+
"@inproceedings{flaky, author={Ada Lovelace}, title={Flaky Paper}, booktitle={ICSE}}",
|
|
102
|
+
"@misc{pre, author={Ada Lovelace}, title={A Preprint}, journal={arXiv preprint}}",
|
|
103
|
+
].join("\n");
|
|
104
|
+
const r = await settle(checkOnly(bib));
|
|
105
|
+
assert.equal(r.kind, "checked");
|
|
106
|
+
const byKey = Object.fromEntries(
|
|
107
|
+
(r.kind === "checked" ? r.entries : []).map((e) => [
|
|
108
|
+
e.key,
|
|
109
|
+
[e.exists, e.authors, e.why ?? ""],
|
|
110
|
+
]),
|
|
111
|
+
);
|
|
112
|
+
assert.deepEqual(
|
|
113
|
+
{
|
|
114
|
+
good: byKey["good"]!.slice(0, 2),
|
|
115
|
+
fake: byKey["fake"]!.slice(0, 2),
|
|
116
|
+
drift: byKey["drift"]!.slice(0, 2),
|
|
117
|
+
driftWhy: String(byKey["drift"]![2]).split("; ").pop(),
|
|
118
|
+
flaky: [byKey["flaky"]![1], String(byKey["flaky"]![2]).split("; ").pop()],
|
|
119
|
+
pre: byKey["pre"]!.slice(0, 2),
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
good: ["true", "match"],
|
|
123
|
+
fake: ["false", "skipped"],
|
|
124
|
+
drift: ["unresolvable", "mismatch"],
|
|
125
|
+
driftWhy: "missing turing (DBLP: ICSE 2024)",
|
|
126
|
+
flaky: [
|
|
127
|
+
"unchecked",
|
|
128
|
+
"DBLP lookup failed: DBLP refused earlier in this run (DBLP 500)",
|
|
129
|
+
],
|
|
130
|
+
pre: ["unresolvable", "skipped"],
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("an author finding names what is extra and what is out of order; an entry only the author check read is unresolvable", async () => {
|
|
136
|
+
vi.useFakeTimers();
|
|
137
|
+
fakeFetch({
|
|
138
|
+
"https://dblp.org/search/publ/api/?q=Extra%20Paper": () =>
|
|
139
|
+
dblp("Extra Paper", ["Ada Lovelace"]),
|
|
140
|
+
"https://dblp.org/search/publ/api/?q=Swapped%20Paper": () =>
|
|
141
|
+
dblp("Swapped Paper", ["Ada Lovelace", "Alan Turing"]),
|
|
142
|
+
"https://dblp.org/search/publ/api/?q=Comment%20Paper": () =>
|
|
143
|
+
dblp("Comment Paper", ["Grace Hopper"]),
|
|
144
|
+
"https://dblp.org/": () => json(200, {}),
|
|
145
|
+
"https://api.crossref.org/": () => json(200, { message: { items: [] } }),
|
|
146
|
+
});
|
|
147
|
+
const bib = [
|
|
148
|
+
"@inproceedings{extra, author={Ada Lovelace and Grace Hopper}, title={Extra Paper}, booktitle={ICSE}}",
|
|
149
|
+
"@inproceedings{swapped, author={Alan Turing and Ada Lovelace}, title={Swapped Paper}, booktitle={ICSE}}",
|
|
150
|
+
// verify-cites skips @comment blocks; bib-authors reads them — so only one checker sees this key.
|
|
151
|
+
"@comment{commented, author={Ada Lovelace}, title={Comment Paper}, booktitle={ICSE}}",
|
|
152
|
+
].join("\n");
|
|
153
|
+
const r = await settle(checkOnly(bib));
|
|
154
|
+
const why = Object.fromEntries(
|
|
155
|
+
(r.kind === "checked" ? r.entries : []).map((e) => [
|
|
156
|
+
e.key,
|
|
157
|
+
[e.exists, e.why],
|
|
158
|
+
]),
|
|
159
|
+
);
|
|
160
|
+
assert.deepEqual(why, {
|
|
161
|
+
extra: [
|
|
162
|
+
"unresolvable",
|
|
163
|
+
"not found by title in any database (no resolvable id to disprove) — could be a legit unindexed/regional/pre-digital work, NOT fabrication; extra hopper (DBLP: ICSE 2024)",
|
|
164
|
+
],
|
|
165
|
+
swapped: [
|
|
166
|
+
"unresolvable",
|
|
167
|
+
"not found by title in any database (no resolvable id to disprove) — could be a legit unindexed/regional/pre-digital work, NOT fabrication; order differs (DBLP: ICSE 2024)",
|
|
168
|
+
],
|
|
169
|
+
commented: [
|
|
170
|
+
"unresolvable",
|
|
171
|
+
"missing hopper; extra lovelace (DBLP: ICSE 2024)",
|
|
172
|
+
],
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ── #107: the per-citation lookups run concurrently ──────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
/** A fetch where every request takes `ms` of real time; `down` DOIs fail at the transport. */
|
|
179
|
+
function slowFetch(ms: number, down: readonly string[] = []) {
|
|
180
|
+
vi.stubGlobal("fetch", async (url: string) => {
|
|
181
|
+
await new Promise((r) => {
|
|
182
|
+
setTimeout(r, ms);
|
|
183
|
+
});
|
|
184
|
+
if (down.some((d) => url.includes(encodeURIComponent(d))))
|
|
185
|
+
throw Object.assign(new Error("aborted"), { name: "AbortError" });
|
|
186
|
+
if (url.startsWith("https://api.crossref.org/works/"))
|
|
187
|
+
return json(200, {
|
|
188
|
+
message: { title: ["Paper"], issued: { "date-parts": [[2024]] } },
|
|
189
|
+
});
|
|
190
|
+
if (url.startsWith("https://doi.org/api/handles/"))
|
|
191
|
+
return json(200, { responseCode: 1 });
|
|
192
|
+
return url === "https://api.crossref.org/" ? json(200) : json(404);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/** N preprint-style entries (no venue, so the DBLP pass and its pauses do not apply). */
|
|
196
|
+
const manyBib = (n: number) =>
|
|
197
|
+
Array.from(
|
|
198
|
+
{ length: n },
|
|
199
|
+
(_, i) =>
|
|
200
|
+
`@misc{k${String(i)}, author={Ada Lovelace}, title={Paper}, doi={10.1234/p${String(i)}}}`,
|
|
201
|
+
).join("\n");
|
|
202
|
+
|
|
203
|
+
// The serial baseline alone takes ~4 s, past vitest's 5 s default with the concurrent run added.
|
|
204
|
+
test(
|
|
205
|
+
"#107: twenty lookups of ~200 ms each finish in far less than twenty times 200 ms, with the serial verdicts",
|
|
206
|
+
{ timeout: 20_000 },
|
|
207
|
+
async () => {
|
|
208
|
+
// Five requests per citation (four resolvers and doi.org), 40 ms each: ~200 ms per lookup.
|
|
209
|
+
slowFetch(40);
|
|
210
|
+
const bib = manyBib(20);
|
|
211
|
+
const serial: string[] = [];
|
|
212
|
+
for (const c of cites.parseBib(bib) as { id: string }[])
|
|
213
|
+
serial.push(
|
|
214
|
+
(
|
|
215
|
+
(await cites.verifyCitationLive(c, { cache: {} })) as {
|
|
216
|
+
verdict: string;
|
|
217
|
+
}
|
|
218
|
+
).verdict,
|
|
219
|
+
);
|
|
220
|
+
const t0 = Date.now();
|
|
221
|
+
const r = await checkOnly(bib);
|
|
222
|
+
const ms = Date.now() - t0;
|
|
223
|
+
assert.equal(r.kind, "checked");
|
|
224
|
+
assert.deepEqual(
|
|
225
|
+
r.kind === "checked" ? r.entries.map((e) => e.exists) : [],
|
|
226
|
+
serial,
|
|
227
|
+
);
|
|
228
|
+
assert.ok(ms < (20 * 200) / 2, `${String(ms)} ms for 20 lookups`);
|
|
229
|
+
},
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
test("#107: a lookup whose requests time out is that entry's `unresolvable`, not the others' failure", async () => {
|
|
233
|
+
slowFetch(5, ["10.1234/p3"]);
|
|
234
|
+
const r = await checkOnly(manyBib(8));
|
|
235
|
+
assert.deepEqual(
|
|
236
|
+
r.kind === "checked" ? r.entries.map((e) => [e.key, e.exists]) : [],
|
|
237
|
+
Array.from({ length: 8 }, (_, i) => [
|
|
238
|
+
`k${String(i)}`,
|
|
239
|
+
i === 3 ? "unresolvable" : "true",
|
|
240
|
+
]),
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ── #107: the committed lookup cache ─────────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
const TODAY = "2026-09-27";
|
|
247
|
+
const cached = referencesChecker({ today: () => TODAY });
|
|
248
|
+
const PAPERS = [
|
|
249
|
+
"@inproceedings{good, author={Ada Lovelace}, title={Good Paper}, booktitle={ICSE}, doi={10.1/good}}",
|
|
250
|
+
"@inproceedings{other, author={Ada Lovelace}, title={Other Paper}, booktitle={ICSE}}",
|
|
251
|
+
"@misc{pre, author={Ada Lovelace}, title={A Preprint}, journal={arXiv preprint}}",
|
|
252
|
+
];
|
|
253
|
+
/** Every service answers; DBLP knows "Good Paper" and nothing else. */
|
|
254
|
+
const answering = () =>
|
|
255
|
+
fakeFetch({
|
|
256
|
+
"https://api.crossref.org/works/10.1%2Fgood": () =>
|
|
257
|
+
json(200, {
|
|
258
|
+
message: { title: ["Good Paper"], issued: { "date-parts": [[2024]] } },
|
|
259
|
+
}),
|
|
260
|
+
"https://doi.org/api/handles/10.1/good": () =>
|
|
261
|
+
json(200, { responseCode: 1 }),
|
|
262
|
+
"https://dblp.org/search/publ/api/?q=Good%20Paper": () =>
|
|
263
|
+
dblp("Good Paper", ["Ada Lovelace"]),
|
|
264
|
+
"https://dblp.org/": () => json(200, {}),
|
|
265
|
+
"https://api.crossref.org/": () => json(200, { message: { items: [] } }),
|
|
266
|
+
"https://api.openalex.org/": () => json(200, { results: [] }),
|
|
267
|
+
"https://api.semanticscholar.org/": () => json(200, { data: [] }),
|
|
268
|
+
"https://export.arxiv.org/": () => new Response("<feed></feed>"),
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
/** A cold run over `bib`, under fake timers: its answers become the cache the next run starts from. */
|
|
272
|
+
async function cold(bib: string) {
|
|
273
|
+
vi.useFakeTimers();
|
|
274
|
+
answering();
|
|
275
|
+
return settle(cached(bib, EMPTY_LOOKUP_CACHE));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
test("cold: every answer is stored with the day it was fetched — responses, never verdicts", async () => {
|
|
279
|
+
const { check, cache } = await cold(PAPERS.join("\n"));
|
|
280
|
+
assert.equal(check.kind, "checked");
|
|
281
|
+
assert.deepEqual(
|
|
282
|
+
{
|
|
283
|
+
// The lookups run concurrently, so insertion order is not the subject.
|
|
284
|
+
citations: [...cache.citations.keys()]
|
|
285
|
+
.map((k) => k.split(":").slice(0, 2).join(":"))
|
|
286
|
+
.sort(),
|
|
287
|
+
dblp: [...cache.dblp.values()].map((d) => [d.title, d.hits.length]),
|
|
288
|
+
dates: new Set(
|
|
289
|
+
[...cache.citations.values(), ...cache.dblp.values()].map(
|
|
290
|
+
(v) => v.fetched,
|
|
291
|
+
),
|
|
292
|
+
),
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
// `good` is confirmed by Crossref, which ends its lookups; the two title-only entries are
|
|
296
|
+
// found nowhere, so each asked all four registries.
|
|
297
|
+
citations: [
|
|
298
|
+
"arxiv:title",
|
|
299
|
+
"arxiv:title",
|
|
300
|
+
"crossref:doi",
|
|
301
|
+
"crossref:title",
|
|
302
|
+
"crossref:title",
|
|
303
|
+
"openalex:title",
|
|
304
|
+
"openalex:title",
|
|
305
|
+
"semantic_scholar:title",
|
|
306
|
+
"semantic_scholar:title",
|
|
307
|
+
],
|
|
308
|
+
dblp: [
|
|
309
|
+
["Good Paper", 1],
|
|
310
|
+
["Other Paper", 0],
|
|
311
|
+
],
|
|
312
|
+
dates: new Set([TODAY]),
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
// Guards: what is stored is the resolver's RESPONSE — never a verdict field.
|
|
316
|
+
assert.deepEqual(cache.citations.get("crossref:doi:10.1/good"), {
|
|
317
|
+
fetched: TODAY,
|
|
318
|
+
response: {
|
|
319
|
+
db: "crossref",
|
|
320
|
+
transport: "ok",
|
|
321
|
+
query: "doi",
|
|
322
|
+
record: { title: "Good Paper", subtitle: "", year: 2024 },
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("🔴 warm: a fully cached bibliography asks NOTHING — no lookup, no reachability probe, no DBLP pause", async () => {
|
|
328
|
+
const bib = PAPERS.join("\n");
|
|
329
|
+
const first = await cold(bib);
|
|
330
|
+
vi.useRealTimers();
|
|
331
|
+
// Fake timers with NO advancing: a single DBLP pause would never resolve and time the test out.
|
|
332
|
+
vi.useFakeTimers();
|
|
333
|
+
const calls = fakeFetch({});
|
|
334
|
+
const second = await cached(bib, first.cache);
|
|
335
|
+
assert.deepEqual(
|
|
336
|
+
// Nothing fetched, so the cache comes back as the very object passed in — nothing to write.
|
|
337
|
+
{ calls, check: second.check, sameCache: second.cache === first.cache },
|
|
338
|
+
{ calls: [], check: first.check, sameCache: true },
|
|
339
|
+
);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("🔴 one entry edited: only that entry is asked again, and the old answers are kept", async () => {
|
|
343
|
+
const first = await cold(PAPERS.join("\n"));
|
|
344
|
+
vi.useRealTimers();
|
|
345
|
+
vi.useFakeTimers();
|
|
346
|
+
const calls = answering();
|
|
347
|
+
const edited = PAPERS.join("\n").replace("Other Paper", "Renamed Paper");
|
|
348
|
+
const second = await settle(cached(edited, first.cache));
|
|
349
|
+
assert.deepEqual(
|
|
350
|
+
{
|
|
351
|
+
// The lookups run concurrently, so the order of requests is not the subject.
|
|
352
|
+
asked: calls.filter((u) => u !== "https://api.crossref.org/").sort(),
|
|
353
|
+
probed: calls.includes("https://api.crossref.org/"),
|
|
354
|
+
kept: [...first.cache.citations.keys()].every((k) =>
|
|
355
|
+
second.cache.citations.has(k),
|
|
356
|
+
),
|
|
357
|
+
newDblp: second.cache.dblp.has(dblpTitleKey("Renamed Paper")),
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
asked: [
|
|
361
|
+
"https://api.crossref.org/works?query.bibliographic=Renamed%20Paper&rows=5",
|
|
362
|
+
"https://api.openalex.org/works?search=Renamed%20Paper&per-page=5",
|
|
363
|
+
"https://api.semanticscholar.org/graph/v1/paper/search?query=Renamed%20Paper&fields=title,year&limit=5",
|
|
364
|
+
"https://dblp.org/search/publ/api/?q=Renamed%20Paper&format=json&h=6",
|
|
365
|
+
"https://export.arxiv.org/api/query?search_query=ti:%22Renamed%20Paper%22&max_results=5",
|
|
366
|
+
],
|
|
367
|
+
probed: true,
|
|
368
|
+
kept: true,
|
|
369
|
+
newDblp: true,
|
|
370
|
+
},
|
|
371
|
+
);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Guards: a cached answer is good for MAX_AGE_DAYS, then asked again. A registry or DBLP that had
|
|
375
|
+
// not indexed a new work yet would otherwise answer "no record" from the committed cache forever.
|
|
376
|
+
test("an answer older than 30 days is asked again and re-dated; at 30 days it is still used", async () => {
|
|
377
|
+
const bib = PAPERS.join("\n");
|
|
378
|
+
const first = await cold(bib);
|
|
379
|
+
vi.useRealTimers();
|
|
380
|
+
vi.useFakeTimers();
|
|
381
|
+
const quiet = fakeFetch({});
|
|
382
|
+
const at30 = await referencesChecker({ today: () => "2026-10-27" })(
|
|
383
|
+
bib,
|
|
384
|
+
first.cache,
|
|
385
|
+
);
|
|
386
|
+
vi.useRealTimers();
|
|
387
|
+
vi.useFakeTimers();
|
|
388
|
+
const calls = answering();
|
|
389
|
+
const at31 = await settle(
|
|
390
|
+
referencesChecker({ today: () => "2026-10-28" })(bib, first.cache),
|
|
391
|
+
);
|
|
392
|
+
assert.deepEqual(
|
|
393
|
+
{
|
|
394
|
+
at30: { calls: quiet, same: at30.cache === first.cache },
|
|
395
|
+
at31: {
|
|
396
|
+
asked: calls.some((u) => u.startsWith("https://dblp.org/search")),
|
|
397
|
+
dates: new Set(
|
|
398
|
+
[...at31.cache.citations.values(), ...at31.cache.dblp.values()].map(
|
|
399
|
+
(v) => v.fetched,
|
|
400
|
+
),
|
|
401
|
+
),
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
at30: { calls: [], same: true },
|
|
406
|
+
at31: { asked: true, dates: new Set(["2026-10-28"]) },
|
|
407
|
+
},
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("a failed lookup is not cached: the next run asks it again", async () => {
|
|
412
|
+
vi.useFakeTimers();
|
|
413
|
+
fakeFetch({
|
|
414
|
+
"https://api.crossref.org/": () => json(200, { message: { items: [] } }),
|
|
415
|
+
"https://dblp.org/": () => json(500),
|
|
416
|
+
});
|
|
417
|
+
const bib = PAPERS[1]!;
|
|
418
|
+
const r = await settle(cached(bib, EMPTY_LOOKUP_CACHE));
|
|
419
|
+
const failed: LookupCache = r.cache;
|
|
420
|
+
assert.deepEqual(
|
|
421
|
+
{
|
|
422
|
+
dblp: failed.dblp.size,
|
|
423
|
+
authors: r.check.kind === "checked" ? r.check.entries[0]?.authors : "",
|
|
424
|
+
},
|
|
425
|
+
{ dblp: 0, authors: "unchecked" },
|
|
426
|
+
);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("offline with an incomplete cache: not-checked, and the cache comes back unchanged", async () => {
|
|
430
|
+
vi.stubGlobal("fetch", async () => {
|
|
431
|
+
throw new Error("getaddrinfo ENOTFOUND api.crossref.org");
|
|
432
|
+
});
|
|
433
|
+
const r = await cached(PAPERS[0]!, EMPTY_LOOKUP_CACHE);
|
|
434
|
+
assert.deepEqual(r, {
|
|
435
|
+
check: {
|
|
436
|
+
kind: "not-checked",
|
|
437
|
+
why: "the citation services cannot be reached (getaddrinfo ENOTFOUND api.crossref.org)",
|
|
438
|
+
},
|
|
439
|
+
cache: EMPTY_LOOKUP_CACHE,
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// ── #120: a service that refuses costs one request per run ───────────────────────────────
|
|
444
|
+
|
|
445
|
+
const PUBLISHED = [
|
|
446
|
+
"@inproceedings{a, author={Ada Lovelace}, title={First Paper}, booktitle={ICSE}}",
|
|
447
|
+
"@inproceedings{b, author={Ada Lovelace}, title={Second Paper}, booktitle={ICSE}}",
|
|
448
|
+
"@inproceedings{c, author={Ada Lovelace}, title={Third Paper}, booktitle={ICSE}}",
|
|
449
|
+
].join("\n");
|
|
450
|
+
|
|
451
|
+
/** Every registry answers "not found"; DBLP and Semantic Scholar answer with `dblp` and `s2`. */
|
|
452
|
+
function refusing(dblpAnswer: () => Response, s2: () => Response) {
|
|
453
|
+
return fakeFetch({
|
|
454
|
+
"https://api.crossref.org/": () => json(200, { message: { items: [] } }),
|
|
455
|
+
"https://api.openalex.org/": () => json(200, { results: [] }),
|
|
456
|
+
"https://api.semanticscholar.org/": s2,
|
|
457
|
+
"https://export.arxiv.org/": () => new Response("<feed></feed>"),
|
|
458
|
+
"https://dblp.org/": dblpAnswer,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
const count = (calls: string[], prefix: string) =>
|
|
462
|
+
calls.filter((u) => u.startsWith(prefix)).length;
|
|
463
|
+
|
|
464
|
+
test("🔴 #120: DBLP answering HTML is asked ONCE — no retries, no pause — and every entry says why it is unchecked", async () => {
|
|
465
|
+
// Fake timers with NO advancing: a single 900 ms pause or retry backoff would hang the test.
|
|
466
|
+
vi.useFakeTimers();
|
|
467
|
+
const calls = refusing(
|
|
468
|
+
() => new Response("<!doctype html><title>429</title>"),
|
|
469
|
+
() => json(429),
|
|
470
|
+
);
|
|
471
|
+
const { check } = await cached(PUBLISHED, EMPTY_LOOKUP_CACHE);
|
|
472
|
+
const entries = check.kind === "checked" ? check.entries : [];
|
|
473
|
+
assert.deepEqual(
|
|
474
|
+
{
|
|
475
|
+
dblp: count(calls, "https://dblp.org/"),
|
|
476
|
+
s2: count(calls, "https://api.semanticscholar.org/"),
|
|
477
|
+
crossref: count(calls, "https://api.crossref.org/works"),
|
|
478
|
+
entries: entries.map((e) => [e.key, e.exists, e.authors]),
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
dblp: 1,
|
|
482
|
+
s2: 1,
|
|
483
|
+
crossref: 3,
|
|
484
|
+
entries: [
|
|
485
|
+
["a", "unresolvable", "unchecked"],
|
|
486
|
+
["b", "unresolvable", "unchecked"],
|
|
487
|
+
["c", "unresolvable", "unchecked"],
|
|
488
|
+
],
|
|
489
|
+
},
|
|
490
|
+
);
|
|
491
|
+
// The reason, as the record carries it — its last part names both refusals.
|
|
492
|
+
assert.deepEqual(
|
|
493
|
+
entries.map((e) => e.why?.split("; ").slice(1)),
|
|
494
|
+
[
|
|
495
|
+
[
|
|
496
|
+
"not asked: semantic_scholar: http 429",
|
|
497
|
+
"DBLP lookup failed: DBLP refused earlier in this run (Unexpected token '<', \"<!doctype \"... is not valid JSON)",
|
|
498
|
+
],
|
|
499
|
+
[
|
|
500
|
+
"not asked: semantic_scholar: semantic_scholar refused earlier in this run (http 429)",
|
|
501
|
+
"DBLP lookup failed: DBLP refused earlier in this run (Unexpected token '<', \"<!doctype \"... is not valid JSON)",
|
|
502
|
+
],
|
|
503
|
+
[
|
|
504
|
+
"not asked: semantic_scholar: semantic_scholar refused earlier in this run (http 429)",
|
|
505
|
+
"DBLP lookup failed: DBLP refused earlier in this run (Unexpected token '<', \"<!doctype \"... is not valid JSON)",
|
|
506
|
+
],
|
|
507
|
+
],
|
|
508
|
+
);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
test("#120: a DBLP 429 trips it as well; DBLP answering keeps its 900 ms pace between queries", async () => {
|
|
512
|
+
vi.useFakeTimers();
|
|
513
|
+
const refused = refusing(
|
|
514
|
+
() => json(429),
|
|
515
|
+
() => json(200, { data: [] }),
|
|
516
|
+
);
|
|
517
|
+
await cached(PUBLISHED, EMPTY_LOOKUP_CACHE);
|
|
518
|
+
vi.useRealTimers();
|
|
519
|
+
vi.useFakeTimers();
|
|
520
|
+
const answering = refusing(
|
|
521
|
+
() => dblp("First Paper", ["Ada Lovelace"]),
|
|
522
|
+
() => json(200, { data: [] }),
|
|
523
|
+
);
|
|
524
|
+
const pending = cached(PUBLISHED, EMPTY_LOOKUP_CACHE);
|
|
525
|
+
// One compared entry, then the pause: without advancing time, the run cannot finish.
|
|
526
|
+
let done = false;
|
|
527
|
+
void pending.finally(() => (done = true));
|
|
528
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
529
|
+
const beforePause = done;
|
|
530
|
+
await settle(pending);
|
|
531
|
+
assert.deepEqual(
|
|
532
|
+
{
|
|
533
|
+
refused: count(refused, "https://dblp.org/"),
|
|
534
|
+
answering: count(answering, "https://dblp.org/"),
|
|
535
|
+
beforePause,
|
|
536
|
+
},
|
|
537
|
+
{ refused: 1, answering: 3, beforePause: false },
|
|
538
|
+
);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// Guards: the DBLP key must not change with presentation markup (BibTeX braces, case, accents,
|
|
542
|
+
// spacing), or a re-cased title in the .bib re-asks DBLP with its 900 ms pause.
|
|
543
|
+
test("dblpTitleKey is the same for the same title under braces, case, accents and spacing", () => {
|
|
544
|
+
const k = dblpTitleKey("Évaluating {LLM}-based Agents");
|
|
545
|
+
assert.equal(dblpTitleKey("evaluating llm-based agents"), k);
|
|
546
|
+
assert.match(k, /^[0-9a-f]{16}$/);
|
|
547
|
+
assert.notEqual(dblpTitleKey("Evaluating LLM-based Tools"), k);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// Guards: punctuation can be part of a title's identity. A shared key would serve one work's
|
|
551
|
+
// DBLP answer for another; a separate key costs one extra lookup at most.
|
|
552
|
+
const PUNCTUATION_PAIRS: readonly (readonly [string, string])[] = [
|
|
553
|
+
["Fuzzing C Compilers", "Fuzzing C++ Compilers"],
|
|
554
|
+
["Verified F Programs", "Verified F# Programs"],
|
|
555
|
+
];
|
|
556
|
+
for (const [a, b] of PUNCTUATION_PAIRS) {
|
|
557
|
+
test(`dblpTitleKey keeps ${a} and ${b} apart`, () => {
|
|
558
|
+
assert.notEqual(dblpTitleKey(a), dblpTitleKey(b));
|
|
559
|
+
});
|
|
560
|
+
}
|