paperlint 8.0.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +9 -25
- package/CONTRIBUTING.md +5 -4
- package/README.md +4 -3
- package/dist/adapters/bibtex/index.d.ts +3 -0
- package/dist/adapters/bibtex/index.d.ts.map +1 -0
- package/dist/adapters/bibtex/index.js +103 -0
- package/dist/adapters/bibtex/index.js.map +1 -0
- package/dist/adapters/latex/filecontents.d.ts +33 -0
- package/dist/adapters/latex/filecontents.d.ts.map +1 -0
- package/dist/adapters/latex/filecontents.js +65 -0
- package/dist/adapters/latex/filecontents.js.map +1 -0
- package/dist/adapters/latex/includes.d.ts +3 -3
- package/dist/adapters/latex/includes.d.ts.map +1 -1
- package/dist/adapters/latex/includes.js +18 -10
- package/dist/adapters/latex/includes.js.map +1 -1
- package/dist/adapters/latex/index.d.ts.map +1 -1
- package/dist/adapters/latex/index.js +2 -0
- package/dist/adapters/latex/index.js.map +1 -1
- package/dist/adapters/latex/nodes.d.ts +12 -0
- package/dist/adapters/latex/nodes.d.ts.map +1 -1
- package/dist/adapters/latex/nodes.js +39 -0
- package/dist/adapters/latex/nodes.js.map +1 -1
- package/dist/adapters/latex/parse.d.ts +7 -1
- package/dist/adapters/latex/parse.d.ts.map +1 -1
- package/dist/adapters/latex/parse.js +55 -6
- package/dist/adapters/latex/parse.js.map +1 -1
- package/dist/adapters/memory/index.d.ts +3 -0
- package/dist/adapters/memory/index.d.ts.map +1 -1
- package/dist/adapters/memory/index.js +9 -0
- package/dist/adapters/memory/index.js.map +1 -1
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +27 -24
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/adapters/sources-record/index.d.ts +9 -0
- package/dist/adapters/sources-record/index.d.ts.map +1 -0
- package/dist/adapters/sources-record/index.js +68 -0
- package/dist/adapters/sources-record/index.js.map +1 -0
- package/dist/adapters/tex-output/blg.d.ts +9 -0
- package/dist/adapters/tex-output/blg.d.ts.map +1 -0
- package/dist/adapters/tex-output/blg.js +20 -0
- package/dist/adapters/tex-output/blg.js.map +1 -0
- package/dist/adapters/tex-output/fls.d.ts +8 -0
- package/dist/adapters/tex-output/fls.d.ts.map +1 -0
- package/dist/adapters/tex-output/fls.js +16 -0
- package/dist/adapters/tex-output/fls.js.map +1 -0
- package/dist/adapters/tex-output/index.d.ts +7 -0
- package/dist/adapters/tex-output/index.d.ts.map +1 -0
- package/dist/adapters/tex-output/index.js +7 -0
- package/dist/adapters/tex-output/index.js.map +1 -0
- package/dist/bib-rules.d.ts +47 -0
- package/dist/bib-rules.d.ts.map +1 -0
- package/dist/bib-rules.js +127 -0
- package/dist/bib-rules.js.map +1 -0
- package/dist/build.d.ts +29 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +40 -24
- package/dist/build.js.map +1 -1
- package/dist/claim-provenance.d.ts +2 -2
- package/dist/cli.d.ts +6 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +30 -26
- package/dist/cli.js.map +1 -1
- package/dist/domain/paper-source.d.ts +1 -10
- package/dist/domain/paper-source.d.ts.map +1 -1
- package/dist/domain/paper-source.js +34 -24
- package/dist/domain/paper-source.js.map +1 -1
- package/dist/domain/paper-sources.d.ts +72 -0
- package/dist/domain/paper-sources.d.ts.map +1 -0
- package/dist/domain/paper-sources.js +18 -0
- package/dist/domain/paper-sources.js.map +1 -0
- package/dist/domain/sources-record.d.ts +102 -0
- package/dist/domain/sources-record.d.ts.map +1 -0
- package/dist/domain/sources-record.js +38 -0
- package/dist/domain/sources-record.js.map +1 -0
- package/dist/domain/tex-run.d.ts +25 -0
- package/dist/domain/tex-run.d.ts.map +1 -0
- package/dist/domain/tex-run.js +79 -0
- package/dist/domain/tex-run.js.map +1 -0
- package/dist/eslint-rules/paper-context.d.ts.map +1 -1
- package/dist/eslint-rules/paper-context.js +4 -3
- package/dist/eslint-rules/paper-context.js.map +1 -1
- package/dist/eslint-rules/paper-typography.d.ts +0 -9
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
- package/dist/eslint-rules/paper-typography.js +12 -5
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/latex-log.d.ts +10 -0
- package/dist/latex-log.d.ts.map +1 -1
- package/dist/latex-log.js +10 -0
- package/dist/latex-log.js.map +1 -1
- package/dist/latex-loop.d.ts +2 -0
- package/dist/latex-loop.d.ts.map +1 -1
- package/dist/latex-loop.js.map +1 -1
- package/dist/package-dirs.d.ts.map +1 -1
- package/dist/package-dirs.js +3 -2
- package/dist/package-dirs.js.map +1 -1
- package/dist/paper-includes.d.ts +4 -7
- package/dist/paper-includes.d.ts.map +1 -1
- package/dist/paper-includes.js +29 -21
- package/dist/paper-includes.js.map +1 -1
- package/dist/paper-prose.d.ts +2 -2
- package/dist/paper-record.d.ts +25 -0
- package/dist/paper-record.d.ts.map +1 -0
- package/dist/paper-record.js +39 -0
- package/dist/paper-record.js.map +1 -0
- package/dist/paper-sources.d.ts +66 -0
- package/dist/paper-sources.d.ts.map +1 -0
- package/dist/paper-sources.js +92 -0
- package/dist/paper-sources.js.map +1 -0
- package/dist/ports/bib-reader.d.ts +18 -0
- package/dist/ports/bib-reader.d.ts.map +1 -0
- package/dist/ports/bib-reader.js +2 -0
- package/dist/ports/bib-reader.js.map +1 -0
- package/dist/ports/check-references.d.ts +9 -4
- package/dist/ports/check-references.d.ts.map +1 -1
- package/dist/ports/latex.d.ts +15 -0
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/ports/sources-codec.d.ts +11 -0
- package/dist/ports/sources-codec.d.ts.map +1 -0
- package/dist/ports/sources-codec.js +2 -0
- package/dist/ports/sources-codec.js.map +1 -0
- package/dist/ports/tex-output.d.ts +17 -0
- package/dist/ports/tex-output.d.ts.map +1 -0
- package/dist/ports/tex-output.js +2 -0
- package/dist/ports/tex-output.js.map +1 -0
- package/dist/recorded-bibliography.d.ts +89 -0
- package/dist/recorded-bibliography.d.ts.map +1 -0
- package/dist/recorded-bibliography.js +136 -0
- package/dist/recorded-bibliography.js.map +1 -0
- package/dist/reference-rules.d.ts +5 -5
- package/dist/reference-rules.d.ts.map +1 -1
- package/dist/reference-rules.js +150 -77
- package/dist/reference-rules.js.map +1 -1
- package/dist/references.d.ts +71 -16
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +110 -26
- package/dist/references.js.map +1 -1
- package/dist/register-bands.d.ts +3 -3
- package/dist/register-bands.d.ts.map +1 -1
- package/dist/register-bands.js.map +1 -1
- package/dist/register.d.ts +2 -2
- package/dist/sources-record.d.ts +38 -0
- package/dist/sources-record.d.ts.map +1 -0
- package/dist/sources-record.js +123 -0
- package/dist/sources-record.js.map +1 -0
- package/dist/sources-rules.d.ts +44 -0
- package/dist/sources-rules.d.ts.map +1 -0
- package/dist/sources-rules.js +76 -0
- package/dist/sources-rules.js.map +1 -0
- package/dist/tex-paper.d.ts +10 -15
- package/dist/tex-paper.d.ts.map +1 -1
- package/dist/tex-paper.js +24 -40
- package/dist/tex-paper.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +3 -3
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js.map +1 -1
- package/docs/configuration.md +12 -4
- package/docs/design/paper-sources.md +197 -0
- package/docs/package-shape-options.md +1 -1
- package/docs/references.md +34 -8
- package/docs/rules/bib/reachable-entry.md +64 -0
- package/docs/rules/paper/sources-fresh.md +81 -0
- package/docs/rules.md +62 -57
- package/eslint-rules/paper-context.ts +4 -3
- package/eslint-rules/paper-typography.test.mjs +12 -72
- package/eslint-rules/paper-typography.ts +13 -6
- package/eslint.config.mjs +22 -0
- package/fixtures/build-e2e/aidc/paper.tex +1 -1
- package/fixtures/build-e2e/anon-ack/paper.tex +1 -1
- package/fixtures/build-e2e/anon-author/paper.tex +1 -1
- package/fixtures/build-e2e/anon-bib/paper.tex +1 -1
- package/fixtures/build-e2e/anon-href/paper.tex +1 -1
- package/fixtures/build-e2e/anon-macro/paper.tex +1 -1
- package/fixtures/build-e2e/anon-pdfauthor/paper.tex +1 -1
- package/fixtures/paper-sources/p1/paper.tex +30 -0
- package/fixtures/paper-sources/p1/refs.bib +6 -0
- package/fixtures/paper-sources/p1/sections/dead.tex +6 -0
- package/fixtures/paper-sources/p1/sections/intro.tex +7 -0
- package/fixtures/paper-sources/p1/sections/parked.tex +6 -0
- package/fixtures/paper-sources/p1/tex-truth.json +37 -0
- package/fixtures/paper-sources/p1/versions/2026-01-01-review.tex +4 -0
- package/fixtures/paper-sources/v1-stale/paper.tex +14 -0
- package/fixtures/paper-sources/v1-stale/refs.bib +6 -0
- package/fixtures/paper-sources/v1-stale/tex-truth.json +32 -0
- package/fixtures/paper-sources/v10-ifthenelse/anon.bib +1 -0
- package/fixtures/paper-sources/v10-ifthenelse/paper.tex +9 -0
- package/fixtures/paper-sources/v10-ifthenelse/real.bib +1 -0
- package/fixtures/paper-sources/v10-ifthenelse/tex-truth.json +28 -0
- package/fixtures/paper-sources/v11-two-blocks/paper.tex +12 -0
- package/fixtures/paper-sources/v11-two-blocks/tex-truth.json +30 -0
- package/fixtures/paper-sources/v13-percent-text/paper.tex +6 -0
- package/fixtures/paper-sources/v13-percent-text/refs.bib +3 -0
- package/fixtures/paper-sources/v13-percent-text/tex-truth.json +30 -0
- package/fixtures/paper-sources/v15-after-end/paper.tex +6 -0
- package/fixtures/paper-sources/v15-after-end/refs.bib +1 -0
- package/fixtures/paper-sources/v15-after-end/tex-truth.json +17 -0
- package/fixtures/paper-sources/v16-included-block/bibblock.tex +3 -0
- package/fixtures/paper-sources/v16-included-block/paper.tex +7 -0
- package/fixtures/paper-sources/v16-included-block/refs.bib +1 -0
- package/fixtures/paper-sources/v16-included-block/tex-truth.json +35 -0
- package/fixtures/paper-sources/v17-include-order/bibsetup.tex +1 -0
- package/fixtures/paper-sources/v17-include-order/first.bib +1 -0
- package/fixtures/paper-sources/v17-include-order/later.bib +1 -0
- package/fixtures/paper-sources/v17-include-order/paper.tex +7 -0
- package/fixtures/paper-sources/v17-include-order/tex-truth.json +40 -0
- package/fixtures/paper-sources/v2-overwrite/paper.tex +14 -0
- package/fixtures/paper-sources/v2-overwrite/refs.bib +6 -0
- package/fixtures/paper-sources/v2-overwrite/tex-truth.json +30 -0
- package/fixtures/paper-sources/v22-parked-thebibliography/paper.tex +7 -0
- package/fixtures/paper-sources/v22-parked-thebibliography/tex-truth.json +17 -0
- package/fixtures/paper-sources/v23-parked-include/old.bib +1 -0
- package/fixtures/paper-sources/v23-parked-include/paper.tex +7 -0
- package/fixtures/paper-sources/v23-parked-include/parked.tex +1 -0
- package/fixtures/paper-sources/v23-parked-include/refs.bib +1 -0
- package/fixtures/paper-sources/v23-parked-include/tex-truth.json +28 -0
- package/fixtures/paper-sources/v24-quoted-fields/paper.tex +6 -0
- package/fixtures/paper-sources/v24-quoted-fields/refs.bib +5 -0
- package/fixtures/paper-sources/v24-quoted-fields/tex-truth.json +32 -0
- package/fixtures/paper-sources/v25-string-differs/paper.tex +10 -0
- package/fixtures/paper-sources/v25-string-differs/refs.bib +2 -0
- package/fixtures/paper-sources/v25-string-differs/tex-truth.json +32 -0
- package/fixtures/paper-sources/v26-comment-in-names/one.bib +1 -0
- package/fixtures/paper-sources/v26-comment-in-names/paper.tex +7 -0
- package/fixtures/paper-sources/v26-comment-in-names/tex-truth.json +32 -0
- package/fixtures/paper-sources/v26-comment-in-names/two.bib +1 -0
- package/fixtures/paper-sources/v27-shared-string/abbreviations.bib +2 -0
- package/fixtures/paper-sources/v27-shared-string/paper.tex +6 -0
- package/fixtures/paper-sources/v27-shared-string/references.bib +6 -0
- package/fixtures/paper-sources/v27-shared-string/tex-truth.json +31 -0
- package/fixtures/paper-sources/v28-shared-string-block/abbreviations.bib +2 -0
- package/fixtures/paper-sources/v28-shared-string-block/paper.tex +14 -0
- package/fixtures/paper-sources/v28-shared-string-block/tex-truth.json +33 -0
- package/fixtures/paper-sources/v29-generated-input/paper.tex +9 -0
- package/fixtures/paper-sources/v29-generated-input/tex-truth.json +24 -0
- package/fixtures/paper-sources/v3-declared/paper.bib +6 -0
- package/fixtures/paper-sources/v3-declared/paper.tex +6 -0
- package/fixtures/paper-sources/v3-declared/tex-truth.json +28 -0
- package/fixtures/paper-sources/v30-name-next-line/paper.tex +14 -0
- package/fixtures/paper-sources/v30-name-next-line/tex-truth.json +30 -0
- package/fixtures/paper-sources/v4-commented/paper.tex +14 -0
- package/fixtures/paper-sources/v4-commented/refs.bib +6 -0
- package/fixtures/paper-sources/v4-commented/tex-truth.json +28 -0
- package/fixtures/paper-sources/v5-percent-entry/paper.tex +20 -0
- package/fixtures/paper-sources/v5-percent-entry/tex-truth.json +37 -0
- package/fixtures/paper-sources/v6-unclosed/paper.tex +6 -0
- package/fixtures/paper-sources/v6-unclosed/refs.bib +5 -0
- package/fixtures/paper-sources/v6-unclosed/tex-truth.json +37 -0
- package/fixtures/paper-sources/v8-jobname/paper.tex +9 -0
- package/fixtures/paper-sources/v8-jobname/tex-truth.json +30 -0
- package/fixtures/paper-sources/v9-two-bibliographies/one.bib +1 -0
- package/fixtures/paper-sources/v9-two-bibliographies/paper.tex +7 -0
- package/fixtures/paper-sources/v9-two-bibliographies/tex-truth.json +35 -0
- package/fixtures/paper-sources/v9-two-bibliographies/two.bib +1 -0
- package/fixtures/paper-typography/clean-paper/paper.tex +1 -1
- package/fixtures/paper-typography/messy-paper/paper.tex +1 -1
- package/package.json +2 -10
- package/scripts/check.test.ts +23 -1
- package/scripts/check.ts +7 -3
- package/scripts/coverage-config.test.ts +19 -10
- package/scripts/layer-legacy-frozen.test.ts +41 -2
- package/scripts/layer-legacy-frozen.ts +31 -2
- package/skills/grade-paper-writing/SKILL.md +4 -3
- package/skills/grade-paper-writing/SKILL.md.spec.ts +3 -2
- package/skills/grade-paper-writing/prose-lint.mjs +11 -2
- package/skills/grade-paper-writing/prose-lint.test.ts +52 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +83 -33
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +67 -115
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +128 -121
- package/skills/verify-citations/SKILL.md +7 -4
- package/skills/verify-citations/SKILL.md.spec.ts +6 -3
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +17 -19
- package/skills/verify-citations/scripts/bib-authors.cli.test.ts +129 -46
- package/skills/verify-citations/scripts/bib-authors.d.mts +22 -2
- package/skills/verify-citations/scripts/bib-authors.mjs +57 -111
- package/skills/verify-citations/scripts/verify-cites.cli.test.ts +16 -0
- package/skills/verify-citations/scripts/verify-cites.d.mts +19 -3
- package/skills/verify-citations/scripts/verify-cites.mjs +42 -108
- package/skills/verify-citations/scripts/verify-cites.test.mjs +23 -36
- package/src/adapters/bibtex/index.test.ts +163 -0
- package/src/adapters/bibtex/index.ts +155 -0
- package/src/adapters/latex/filecontents.ts +114 -0
- package/src/adapters/latex/includes.ts +18 -10
- package/src/adapters/latex/index.test.ts +73 -0
- package/src/adapters/latex/index.ts +2 -0
- package/src/adapters/latex/nodes.ts +64 -0
- package/src/adapters/latex/parse.test.ts +38 -0
- package/src/adapters/latex/parse.ts +67 -6
- package/src/adapters/memory/index.ts +17 -0
- package/src/adapters/references/index.test.ts +75 -21
- package/src/adapters/references/index.ts +37 -32
- package/src/adapters/sources-record/index.test.ts +87 -0
- package/src/adapters/sources-record/index.ts +82 -0
- package/src/adapters/tex-output/blg.ts +27 -0
- package/src/adapters/tex-output/fls.ts +24 -0
- package/src/adapters/tex-output/index.test.ts +171 -0
- package/src/adapters/tex-output/index.ts +12 -0
- package/src/bib-rules.test.ts +356 -0
- package/src/bib-rules.ts +228 -0
- package/src/bibliography-at.test.ts +286 -0
- package/src/build.test.ts +210 -0
- package/src/build.ts +100 -35
- package/src/claim-provenance.test.ts +33 -7
- package/src/claim-provenance.ts +2 -2
- package/src/cli.harness.mjs +20 -10
- package/src/cli.test.ts +14 -10
- package/src/cli.ts +37 -28
- package/src/domain/paper-source.test.ts +58 -20
- package/src/domain/paper-source.ts +38 -42
- package/src/domain/paper-sources.ts +94 -0
- package/src/domain/sources-record.test.ts +71 -0
- package/src/domain/sources-record.ts +136 -0
- package/src/domain/tex-run.test.ts +228 -0
- package/src/domain/tex-run.ts +139 -0
- package/src/heading-case.test.ts +24 -10
- package/src/latex-log.ts +20 -0
- package/src/latex-loop.harness.ts +1 -0
- package/src/latex-loop.ts +2 -0
- package/src/linted-set.test.ts +9 -1
- package/src/new-paper.harness.mjs +3 -2
- package/src/new-paper.test.ts +9 -5
- package/src/package-dirs.ts +3 -2
- package/src/paper-includes.test.ts +157 -38
- package/src/paper-includes.ts +39 -35
- package/src/paper-prose.test.ts +34 -5
- package/src/paper-prose.ts +2 -2
- package/src/paper-record.test.ts +173 -0
- package/src/paper-record.ts +66 -0
- package/src/paper-settings-commands.test.ts +5 -2
- package/src/paper-sources.ts +151 -0
- package/src/ports/bib-reader.ts +27 -0
- package/src/ports/check-references.ts +7 -3
- package/src/ports/latex.ts +19 -0
- package/src/ports/sources-codec.ts +11 -0
- package/src/ports/tex-output.ts +17 -0
- package/src/recorded-bibliography.ts +297 -0
- package/src/reference-rules.ts +217 -104
- package/src/references.test.ts +520 -177
- package/src/references.ts +185 -37
- package/src/register-bands.test.ts +5 -1
- package/src/register-bands.ts +3 -5
- package/src/register.test.ts +6 -1
- package/src/register.ts +2 -2
- package/src/rule-cases.test.ts +68 -12
- package/src/rule-docs.test.ts +0 -1
- package/src/sources-record.test.ts +308 -0
- package/src/sources-record.ts +231 -0
- package/src/sources-rules.test.ts +152 -0
- package/src/sources-rules.ts +117 -0
- package/src/tex-paper.test.ts +148 -78
- package/src/tex-paper.ts +31 -61
- package/src/tex-venue-rules.test.ts +36 -8
- package/src/tex-venue-rules.ts +3 -5
- package/dist/eslint-rules/bib-reachable-entry.d.ts +0 -22
- package/dist/eslint-rules/bib-reachable-entry.d.ts.map +0 -1
- package/dist/eslint-rules/bib-reachable-entry.js +0 -75
- package/dist/eslint-rules/bib-reachable-entry.js.map +0 -1
- package/eslint-rules/bib-reachable-entry.ts +0 -76
|
@@ -11,10 +11,16 @@ import {
|
|
|
11
11
|
type LookupCache,
|
|
12
12
|
} from "../../domain/lookup-cache.ts";
|
|
13
13
|
import * as cites from "../../../skills/verify-citations/scripts/verify-cites.mjs";
|
|
14
|
+
import { absolutePath } from "../../domain/paths.ts";
|
|
15
|
+
import { bibReader } from "../bibtex/index.ts";
|
|
16
|
+
|
|
17
|
+
/** A BibTeX text's entries, as the bibtex reader reads them. */
|
|
18
|
+
const read = (bib: string) =>
|
|
19
|
+
bibReader.readFile(absolutePath("/p/refs.bib"), bib).entries;
|
|
14
20
|
|
|
15
21
|
/** The verdicts of a cold run — an empty cache, the adapter the CLI wires. */
|
|
16
22
|
const checkOnly = async (bib: string) =>
|
|
17
|
-
(await onlineReferences(bib, EMPTY_LOOKUP_CACHE)).check;
|
|
23
|
+
(await onlineReferences(read(bib), EMPTY_LOOKUP_CACHE)).check;
|
|
18
24
|
|
|
19
25
|
afterEach(() => {
|
|
20
26
|
vi.unstubAllGlobals();
|
|
@@ -131,23 +137,75 @@ test("each entry gets its existence and its authors: confirmed, fabricated, mism
|
|
|
131
137
|
);
|
|
132
138
|
});
|
|
133
139
|
|
|
134
|
-
test("
|
|
140
|
+
test("one key in two databases bibtex opened: each entry gets its own verdict, in the order given", async () => {
|
|
141
|
+
vi.useFakeTimers();
|
|
142
|
+
fakeFetch({
|
|
143
|
+
"https://api.crossref.org/works/10.1%2Fgood": () =>
|
|
144
|
+
json(200, {
|
|
145
|
+
message: { title: ["Good Paper"], issued: { "date-parts": [[2024]] } },
|
|
146
|
+
}),
|
|
147
|
+
"https://doi.org/api/handles/10.1/good": () =>
|
|
148
|
+
json(200, { responseCode: 1 }),
|
|
149
|
+
"https://doi.org/api/handles/10.1/fake": () => json(404),
|
|
150
|
+
// Crossref knows nothing of the fake: a 404, not a refusal that would stop the run asking it.
|
|
151
|
+
"https://api.crossref.org/works/10.1%2Ffake": () => json(404),
|
|
152
|
+
"https://dblp.org/search/publ/api/?q=Good%20Paper": () =>
|
|
153
|
+
dblp("Good Paper", ["Ada Lovelace"]),
|
|
154
|
+
"https://api.crossref.org/": () => json(200),
|
|
155
|
+
"https://dblp.org/": () => json(200, {}),
|
|
156
|
+
});
|
|
157
|
+
// Two databases of one `\bibliography`: both define `k` (bibtex takes the first), and only one
|
|
158
|
+
// copy fails — its existence, or its authors. The failing verdict is the failing copy's own.
|
|
159
|
+
const good =
|
|
160
|
+
"@inproceedings{k, author={Ada Lovelace}, title={Good Paper}, booktitle={ICSE}, doi={10.1/good}}";
|
|
161
|
+
const fake =
|
|
162
|
+
"@inproceedings{k, author={Ada Lovelace}, title={Fake Paper}, booktitle={ICSE}, doi={10.1/fake}}";
|
|
163
|
+
const drift =
|
|
164
|
+
"@inproceedings{k, author={Ada Lovelace and Alan Turing}, title={Good Paper}, booktitle={ICSE}, doi={10.1/good}}";
|
|
165
|
+
const verdicts = async (bib: string) => {
|
|
166
|
+
const r = await settle(checkOnly(bib));
|
|
167
|
+
return (r.kind === "checked" ? r.entries : []).map((e) => [
|
|
168
|
+
e.key,
|
|
169
|
+
e.exists,
|
|
170
|
+
e.authors,
|
|
171
|
+
]);
|
|
172
|
+
};
|
|
173
|
+
assert.deepEqual(
|
|
174
|
+
[
|
|
175
|
+
await verdicts(`${good}\n${fake}`),
|
|
176
|
+
await verdicts(`${fake}\n${good}`),
|
|
177
|
+
await verdicts(`${good}\n${drift}`),
|
|
178
|
+
],
|
|
179
|
+
[
|
|
180
|
+
[
|
|
181
|
+
["k", "true", "match"],
|
|
182
|
+
["k", "false", "skipped"],
|
|
183
|
+
],
|
|
184
|
+
[
|
|
185
|
+
["k", "false", "skipped"],
|
|
186
|
+
["k", "true", "match"],
|
|
187
|
+
],
|
|
188
|
+
[
|
|
189
|
+
["k", "true", "match"],
|
|
190
|
+
["k", "true", "mismatch"],
|
|
191
|
+
],
|
|
192
|
+
],
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("an author finding names what is extra and what is out of order", async () => {
|
|
135
197
|
vi.useFakeTimers();
|
|
136
198
|
fakeFetch({
|
|
137
199
|
"https://dblp.org/search/publ/api/?q=Extra%20Paper": () =>
|
|
138
200
|
dblp("Extra Paper", ["Ada Lovelace"]),
|
|
139
201
|
"https://dblp.org/search/publ/api/?q=Swapped%20Paper": () =>
|
|
140
202
|
dblp("Swapped Paper", ["Ada Lovelace", "Alan Turing"]),
|
|
141
|
-
"https://dblp.org/search/publ/api/?q=Comment%20Paper": () =>
|
|
142
|
-
dblp("Comment Paper", ["Grace Hopper"]),
|
|
143
203
|
"https://dblp.org/": () => json(200, {}),
|
|
144
204
|
"https://api.crossref.org/": () => json(200, { message: { items: [] } }),
|
|
145
205
|
});
|
|
146
206
|
const bib = [
|
|
147
207
|
"@inproceedings{extra, author={Ada Lovelace and Grace Hopper}, title={Extra Paper}, booktitle={ICSE}}",
|
|
148
208
|
"@inproceedings{swapped, author={Alan Turing and Ada Lovelace}, title={Swapped Paper}, booktitle={ICSE}}",
|
|
149
|
-
// verify-cites skips @comment blocks; bib-authors reads them — so only one checker sees this key.
|
|
150
|
-
"@comment{commented, author={Ada Lovelace}, title={Comment Paper}, booktitle={ICSE}}",
|
|
151
209
|
].join("\n");
|
|
152
210
|
const r = await settle(checkOnly(bib));
|
|
153
211
|
const why = Object.fromEntries(
|
|
@@ -165,10 +223,6 @@ test("an author finding names what is extra and what is out of order; an entry o
|
|
|
165
223
|
"unresolvable",
|
|
166
224
|
"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)",
|
|
167
225
|
],
|
|
168
|
-
commented: [
|
|
169
|
-
"unresolvable",
|
|
170
|
-
"missing hopper; extra lovelace (DBLP: ICSE 2024)",
|
|
171
|
-
],
|
|
172
226
|
});
|
|
173
227
|
});
|
|
174
228
|
|
|
@@ -208,7 +262,7 @@ test(
|
|
|
208
262
|
slowFetch(40);
|
|
209
263
|
const bib = manyBib(20);
|
|
210
264
|
const serial: string[] = [];
|
|
211
|
-
for (const c of cites.
|
|
265
|
+
for (const c of cites.citationsOf(read(bib)))
|
|
212
266
|
serial.push((await cites.verifyCitationLive(c, { cache: {} })).verdict);
|
|
213
267
|
const t0 = Date.now();
|
|
214
268
|
const r = await checkOnly(bib);
|
|
@@ -265,7 +319,7 @@ const answering = () =>
|
|
|
265
319
|
async function cold(bib: string) {
|
|
266
320
|
vi.useFakeTimers();
|
|
267
321
|
answering();
|
|
268
|
-
return settle(cached(bib, EMPTY_LOOKUP_CACHE));
|
|
322
|
+
return settle(cached(read(bib), EMPTY_LOOKUP_CACHE));
|
|
269
323
|
}
|
|
270
324
|
|
|
271
325
|
test("cold: every answer is stored with the day it was fetched — responses, never verdicts", async () => {
|
|
@@ -324,7 +378,7 @@ test("🔴 warm: a fully cached bibliography asks NOTHING — no lookup, no reac
|
|
|
324
378
|
// Fake timers with NO advancing: a single DBLP pause would never resolve and time the test out.
|
|
325
379
|
vi.useFakeTimers();
|
|
326
380
|
const calls = fakeFetch({});
|
|
327
|
-
const second = await cached(bib, first.cache);
|
|
381
|
+
const second = await cached(read(bib), first.cache);
|
|
328
382
|
assert.deepEqual(
|
|
329
383
|
// Nothing fetched, so the cache comes back as the very object passed in — nothing to write.
|
|
330
384
|
{ calls, check: second.check, sameCache: second.cache === first.cache },
|
|
@@ -338,7 +392,7 @@ test("🔴 one entry edited: only that entry is asked again, and the old answers
|
|
|
338
392
|
vi.useFakeTimers();
|
|
339
393
|
const calls = answering();
|
|
340
394
|
const edited = PAPERS.join("\n").replace("Other Paper", "Renamed Paper");
|
|
341
|
-
const second = await settle(cached(edited, first.cache));
|
|
395
|
+
const second = await settle(cached(read(edited), first.cache));
|
|
342
396
|
assert.deepEqual(
|
|
343
397
|
{
|
|
344
398
|
// The lookups run concurrently, so the order of requests is not the subject.
|
|
@@ -373,14 +427,14 @@ test("an answer older than 30 days is asked again and re-dated; at 30 days it is
|
|
|
373
427
|
vi.useFakeTimers();
|
|
374
428
|
const quiet = fakeFetch({});
|
|
375
429
|
const at30 = await referencesChecker({ today: () => "2026-10-27" })(
|
|
376
|
-
bib,
|
|
430
|
+
read(bib),
|
|
377
431
|
first.cache,
|
|
378
432
|
);
|
|
379
433
|
vi.useRealTimers();
|
|
380
434
|
vi.useFakeTimers();
|
|
381
435
|
const calls = answering();
|
|
382
436
|
const at31 = await settle(
|
|
383
|
-
referencesChecker({ today: () => "2026-10-28" })(bib, first.cache),
|
|
437
|
+
referencesChecker({ today: () => "2026-10-28" })(read(bib), first.cache),
|
|
384
438
|
);
|
|
385
439
|
assert.deepEqual(
|
|
386
440
|
{
|
|
@@ -408,7 +462,7 @@ test("a failed lookup is not cached: the next run asks it again", async () => {
|
|
|
408
462
|
"https://dblp.org/": () => json(500),
|
|
409
463
|
});
|
|
410
464
|
const bib = PAPERS[1];
|
|
411
|
-
const r = await settle(cached(bib, EMPTY_LOOKUP_CACHE));
|
|
465
|
+
const r = await settle(cached(read(bib), EMPTY_LOOKUP_CACHE));
|
|
412
466
|
const failed: LookupCache = r.cache;
|
|
413
467
|
assert.deepEqual(
|
|
414
468
|
{
|
|
@@ -423,7 +477,7 @@ test("offline with an incomplete cache: not-checked, and the cache comes back un
|
|
|
423
477
|
vi.stubGlobal("fetch", () =>
|
|
424
478
|
Promise.reject(new Error("getaddrinfo ENOTFOUND api.crossref.org")),
|
|
425
479
|
);
|
|
426
|
-
const r = await cached(PAPERS[0], EMPTY_LOOKUP_CACHE);
|
|
480
|
+
const r = await cached(read(PAPERS[0]), EMPTY_LOOKUP_CACHE);
|
|
427
481
|
assert.deepEqual(r, {
|
|
428
482
|
check: {
|
|
429
483
|
kind: "not-checked",
|
|
@@ -461,7 +515,7 @@ test("🔴 #120: DBLP answering HTML is asked ONCE — no retries, no pause —
|
|
|
461
515
|
() => new Response("<!doctype html><title>429</title>"),
|
|
462
516
|
() => json(429),
|
|
463
517
|
);
|
|
464
|
-
const { check } = await cached(PUBLISHED, EMPTY_LOOKUP_CACHE);
|
|
518
|
+
const { check } = await cached(read(PUBLISHED), EMPTY_LOOKUP_CACHE);
|
|
465
519
|
const entries = check.kind === "checked" ? check.entries : [];
|
|
466
520
|
assert.deepEqual(
|
|
467
521
|
{
|
|
@@ -507,14 +561,14 @@ test("#120: a DBLP 429 trips it as well; DBLP answering keeps its 900 ms pace be
|
|
|
507
561
|
() => json(429),
|
|
508
562
|
() => json(200, { data: [] }),
|
|
509
563
|
);
|
|
510
|
-
await cached(PUBLISHED, EMPTY_LOOKUP_CACHE);
|
|
564
|
+
await cached(read(PUBLISHED), EMPTY_LOOKUP_CACHE);
|
|
511
565
|
vi.useRealTimers();
|
|
512
566
|
vi.useFakeTimers();
|
|
513
567
|
const answering = refusing(
|
|
514
568
|
() => dblp("First Paper", ["Ada Lovelace"]),
|
|
515
569
|
() => json(200, { data: [] }),
|
|
516
570
|
);
|
|
517
|
-
const pending = cached(PUBLISHED, EMPTY_LOOKUP_CACHE);
|
|
571
|
+
const pending = cached(read(PUBLISHED), EMPTY_LOOKUP_CACHE);
|
|
518
572
|
// One compared entry, then the pause: without advancing time, the run cannot finish.
|
|
519
573
|
let done = false;
|
|
520
574
|
void pending.finally(() => (done = true));
|
|
@@ -43,6 +43,7 @@ import type {
|
|
|
43
43
|
AuthorFinding,
|
|
44
44
|
BibEntry,
|
|
45
45
|
} from "../../../skills/verify-citations/scripts/bib-authors.mjs";
|
|
46
|
+
import { zip } from "remeda";
|
|
46
47
|
import { messageOf } from "../../domain/text.ts";
|
|
47
48
|
import { isRecord } from "../../domain/record.ts";
|
|
48
49
|
|
|
@@ -55,12 +56,13 @@ const describeAuthors = (f: AuthorFinding): string =>
|
|
|
55
56
|
.filter(Boolean)
|
|
56
57
|
.join("; ") + ` (DBLP: ${f.venue})`;
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
/** bib-authors' answer for one entry, from its buckets for that entry alone. */
|
|
60
|
+
const authorsOf = (a: AuthorBuckets): EntryVerdict["authors"] =>
|
|
61
|
+
a.findings.length > 0
|
|
60
62
|
? "mismatch"
|
|
61
|
-
: a.unchecked.
|
|
63
|
+
: a.unchecked.length > 0
|
|
62
64
|
? "unchecked"
|
|
63
|
-
: a.matched.
|
|
65
|
+
: a.matched.length > 0
|
|
64
66
|
? "match"
|
|
65
67
|
: "skipped";
|
|
66
68
|
|
|
@@ -69,36 +71,45 @@ const authorsOf = (key: string, a: AuthorBuckets): EntryVerdict["authors"] =>
|
|
|
69
71
|
* one, so "unresolvable" is not read as "every registry looked and found nothing". Nothing for a
|
|
70
72
|
* confirmed work: a refusal after a confirmation changed nothing.
|
|
71
73
|
*/
|
|
72
|
-
const unconfirmed = (
|
|
73
|
-
c
|
|
74
|
-
): readonly (string | undefined)[] =>
|
|
75
|
-
c && c.verdict !== "true"
|
|
74
|
+
const unconfirmed = (c: CiteVerdict): readonly (string | undefined)[] =>
|
|
75
|
+
c.verdict !== "true"
|
|
76
76
|
? [c.reason, ...(c.refused ?? []).map((r) => `not asked: ${r}`)]
|
|
77
77
|
: [];
|
|
78
78
|
|
|
79
|
-
/** One entry's verdict
|
|
80
|
-
function entryVerdict(
|
|
81
|
-
key: string,
|
|
82
|
-
found: readonly CiteVerdict[],
|
|
83
|
-
a: AuthorBuckets,
|
|
84
|
-
): EntryVerdict {
|
|
85
|
-
const c = found.find((x) => x.id === key);
|
|
86
|
-
const mismatch = a.findings.find((f) => f.key === key);
|
|
79
|
+
/** One entry's verdict: verify-cites' answer for it, and bib-authors' buckets for it alone. */
|
|
80
|
+
function entryVerdict(c: CiteVerdict, a: AuthorBuckets): EntryVerdict {
|
|
87
81
|
const why = [
|
|
88
82
|
...unconfirmed(c),
|
|
89
|
-
|
|
90
|
-
a.unchecked.
|
|
83
|
+
...a.findings.map(describeAuthors),
|
|
84
|
+
...a.unchecked.map((u) => u.why),
|
|
91
85
|
]
|
|
92
86
|
.filter(Boolean)
|
|
93
87
|
.join("; ");
|
|
94
88
|
return {
|
|
95
|
-
key,
|
|
96
|
-
exists: c
|
|
97
|
-
authors: authorsOf(
|
|
89
|
+
key: c.id,
|
|
90
|
+
exists: c.verdict,
|
|
91
|
+
authors: authorsOf(a),
|
|
98
92
|
...(why ? { why } : {}),
|
|
99
93
|
};
|
|
100
94
|
}
|
|
101
95
|
|
|
96
|
+
/**
|
|
97
|
+
* bib-authors over each entry on its own, one after another as it would run them: its buckets name
|
|
98
|
+
* an entry by key, and two entries of a bibliography may share a key (two databases bibtex
|
|
99
|
+
* opened may each define it, with other metadata).
|
|
100
|
+
*/
|
|
101
|
+
const authorsOfEach = (
|
|
102
|
+
parsed: readonly BibEntry[],
|
|
103
|
+
d: Pick<ReturnType<typeof cachedDblp>, "lookup" | "pause">,
|
|
104
|
+
): Promise<readonly AuthorBuckets[]> =>
|
|
105
|
+
parsed.reduce<Promise<readonly AuthorBuckets[]>>(
|
|
106
|
+
async (done, e) => [
|
|
107
|
+
...(await done),
|
|
108
|
+
await authors.checkAuthors([e], { lookup: d.lookup, pause: d.pause }),
|
|
109
|
+
],
|
|
110
|
+
Promise.resolve([]),
|
|
111
|
+
);
|
|
112
|
+
|
|
102
113
|
/** Whether bib-authors will ask DBLP about this entry — the filter its `checkAuthors` applies. */
|
|
103
114
|
const asksDblp = (e: BibEntry): boolean =>
|
|
104
115
|
Boolean(e.title && e.author) &&
|
|
@@ -212,8 +223,8 @@ export const referencesChecker =
|
|
|
212
223
|
async (bib, cache) => {
|
|
213
224
|
// Answers past MAX_AGE_DAYS are left out, so the run asks them again (and `grown` re-dates them).
|
|
214
225
|
const usable = freshPart(cache, today());
|
|
215
|
-
const citations = cites.
|
|
216
|
-
const parsed = authors.
|
|
226
|
+
const citations = cites.citationsOf(bib);
|
|
227
|
+
const parsed = authors.authorEntries(bib);
|
|
217
228
|
const store: Record<string, unknown> = Object.fromEntries(
|
|
218
229
|
[...usable.citations].map(([k, v]) => [k, v.response]),
|
|
219
230
|
);
|
|
@@ -237,16 +248,10 @@ export const referencesChecker =
|
|
|
237
248
|
breaker,
|
|
238
249
|
}),
|
|
239
250
|
),
|
|
240
|
-
|
|
241
|
-
lookup: d.lookup,
|
|
242
|
-
pause: d.pause,
|
|
243
|
-
}),
|
|
244
|
-
]);
|
|
245
|
-
const keys = new Set([
|
|
246
|
-
...found.map((c) => c.id),
|
|
247
|
-
...a.findings.map((f) => f.key),
|
|
251
|
+
authorsOfEach(parsed, d),
|
|
248
252
|
]);
|
|
249
|
-
|
|
253
|
+
// Both lists are one per entry, in its order: so is the verdict list.
|
|
254
|
+
const entries = zip(found, a).map(([c, b]) => entryVerdict(c, b));
|
|
250
255
|
return {
|
|
251
256
|
check: { kind: "checked", entries },
|
|
252
257
|
cache: grown({ cache, usable }, { store, dblp: d.dblp }, today),
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The schema of `_build/sources.json`: the one text it is parsed from, and every way a file can fail to
|
|
3
|
+
* be a record of this schema — refused by name, never read as a record.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
serializeSourcesRecord,
|
|
8
|
+
type SourcesRecord,
|
|
9
|
+
} from "../../domain/sources-record.ts";
|
|
10
|
+
import { sourcesCodec } from "./index.ts";
|
|
11
|
+
|
|
12
|
+
const parseSourcesRecord = sourcesCodec.parse;
|
|
13
|
+
const A = "a".repeat(64);
|
|
14
|
+
|
|
15
|
+
const RECORD: SourcesRecord = {
|
|
16
|
+
schema: 1,
|
|
17
|
+
inputs: [
|
|
18
|
+
{ path: "paper.tex", role: "body" },
|
|
19
|
+
{ path: "macros.tex", role: "preamble" },
|
|
20
|
+
],
|
|
21
|
+
written: ["refs.bib"],
|
|
22
|
+
bibdata: ["refs"],
|
|
23
|
+
bibtex: {
|
|
24
|
+
ran: true,
|
|
25
|
+
databases: ["refs.bib"],
|
|
26
|
+
keys: ["k1"],
|
|
27
|
+
exit: 2,
|
|
28
|
+
errors: [{ message: "bad", file: "refs.bib", line: 4 }],
|
|
29
|
+
},
|
|
30
|
+
sha256: { "paper.tex": A, "macros.tex": null },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe("parsing a sources.json", () => {
|
|
34
|
+
it("what serialize wrote parses back to the same record, and the text ends with a newline", () => {
|
|
35
|
+
const text = serializeSourcesRecord(RECORD);
|
|
36
|
+
expect(text.endsWith("}\n")).toBe(true);
|
|
37
|
+
expect(parseSourcesRecord(text)).toEqual({ ok: true, record: RECORD });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("a build that ran no bibtex parses too", () => {
|
|
41
|
+
const record: SourcesRecord = { ...RECORD, bibtex: { ran: false } };
|
|
42
|
+
expect(parseSourcesRecord(serializeSourcesRecord(record))).toEqual({
|
|
43
|
+
ok: true,
|
|
44
|
+
record,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("text that is not JSON is refused by name", () => {
|
|
49
|
+
expect(parseSourcesRecord("{")).toEqual({
|
|
50
|
+
ok: false,
|
|
51
|
+
why: "sources.json is not JSON",
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("another schema number is refused by name, not read as this one", () => {
|
|
56
|
+
expect(parseSourcesRecord('{"schema":2}')).toEqual({
|
|
57
|
+
ok: false,
|
|
58
|
+
why: "sources.json is schema 2, this paperlint reads schema 1",
|
|
59
|
+
});
|
|
60
|
+
expect(parseSourcesRecord("[]")).toEqual({
|
|
61
|
+
ok: false,
|
|
62
|
+
why: "sources.json is schema unknown, this paperlint reads schema 1",
|
|
63
|
+
});
|
|
64
|
+
expect(parseSourcesRecord('{"schema":"1"}')).toEqual({
|
|
65
|
+
ok: false,
|
|
66
|
+
why: "sources.json is schema unknown, this paperlint reads schema 1",
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("a record of this schema that is not shaped like one names where", () => {
|
|
71
|
+
const bad = (patch: object): string => {
|
|
72
|
+
const r = parseSourcesRecord(JSON.stringify({ ...RECORD, ...patch }));
|
|
73
|
+
return r.ok ? "accepted" : r.why;
|
|
74
|
+
};
|
|
75
|
+
expect(bad({ inputs: "paper.tex" })).toContain("inputs");
|
|
76
|
+
expect(bad({ sha256: { "paper.tex": "not a digest" } })).toContain(
|
|
77
|
+
"sha256.paper.tex",
|
|
78
|
+
);
|
|
79
|
+
expect(bad({ bibtex: { ran: "yes" } })).toContain("bibtex");
|
|
80
|
+
expect(bad({ inputs: [{ path: "a.tex", role: "main" }] })).toContain(
|
|
81
|
+
"inputs.0.role",
|
|
82
|
+
);
|
|
83
|
+
expect(parseSourcesRecord(JSON.stringify({ schema: 1 }))).toMatchObject({
|
|
84
|
+
ok: false,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The schema of `_build/sources.json` — the one place its shape is checked (docs/design/paper-sources.md
|
|
3
|
+
* §1). The text is parsed once, here, into the domain's `SourcesRecord`; whatever reads a record after
|
|
4
|
+
* it never re-validates. A record of another schema number is refused by name, not read as this one.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import {
|
|
8
|
+
SOURCES_FILE,
|
|
9
|
+
SOURCES_SCHEMA,
|
|
10
|
+
type ParsedRecord,
|
|
11
|
+
} from "../../domain/sources-record.ts";
|
|
12
|
+
import type { SourcesCodec } from "../../ports/sources-codec.ts";
|
|
13
|
+
|
|
14
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
15
|
+
|
|
16
|
+
const bibtexError = z.object({
|
|
17
|
+
message: z.string(),
|
|
18
|
+
file: z.string(),
|
|
19
|
+
line: z.number().int().nullable(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const recordSchema = z.object({
|
|
23
|
+
schema: z.literal(SOURCES_SCHEMA),
|
|
24
|
+
inputs: z.array(
|
|
25
|
+
z.object({ path: z.string(), role: z.enum(["preamble", "body"]) }),
|
|
26
|
+
),
|
|
27
|
+
written: z.array(z.string()),
|
|
28
|
+
bibdata: z.array(z.string()),
|
|
29
|
+
bibtex: z.discriminatedUnion("ran", [
|
|
30
|
+
z.object({ ran: z.literal(false) }),
|
|
31
|
+
z.object({
|
|
32
|
+
ran: z.literal(true),
|
|
33
|
+
databases: z.array(z.string()),
|
|
34
|
+
keys: z.array(z.string()),
|
|
35
|
+
exit: z.number().int(),
|
|
36
|
+
errors: z.array(bibtexError),
|
|
37
|
+
}),
|
|
38
|
+
]),
|
|
39
|
+
sha256: z.record(z.string(), z.string().regex(SHA256).nullable()),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** The JSON `text` holds, or undefined when it is not JSON. */
|
|
43
|
+
function jsonOf(text: string): unknown {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(text);
|
|
46
|
+
} catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The `schema` field of a parsed file, whatever it holds, or undefined when there is none. */
|
|
52
|
+
const schemaOf = (json: unknown): unknown =>
|
|
53
|
+
typeof json === "object" && json !== null && "schema" in json
|
|
54
|
+
? json.schema
|
|
55
|
+
: undefined;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A `sources.json`'s text, parsed once at the boundary: the record, or the one reason it is not one.
|
|
59
|
+
* A record of another schema number is refused by name, not read as this one.
|
|
60
|
+
*/
|
|
61
|
+
export function parseSourcesRecord(text: string): ParsedRecord {
|
|
62
|
+
const json = jsonOf(text);
|
|
63
|
+
if (json === undefined)
|
|
64
|
+
return { ok: false, why: `${SOURCES_FILE} is not JSON` };
|
|
65
|
+
const schema = schemaOf(json);
|
|
66
|
+
if (schema !== SOURCES_SCHEMA)
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
why: `${SOURCES_FILE} is schema ${typeof schema === "number" ? String(schema) : "unknown"}, this paperlint reads schema ${String(SOURCES_SCHEMA)}`,
|
|
70
|
+
};
|
|
71
|
+
const parsed = recordSchema.safeParse(json);
|
|
72
|
+
if (parsed.success) return { ok: true, record: parsed.data };
|
|
73
|
+
const issues = parsed.error.issues.map(
|
|
74
|
+
(i) => `${i.path.join(".")} ${i.message}`,
|
|
75
|
+
);
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
why: `${SOURCES_FILE} does not match schema ${String(SOURCES_SCHEMA)}: ${issues.join("; ")}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const sourcesCodec: SourcesCodec = { parse: parseSourcesRecord };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The errors in bibtex's log (`paper.blg`). bibtex ends an error with the place it was reading —
|
|
3
|
+
* `---line 4 of file refs.bib`, or `---while reading file paper.aux` when it names no line — and a
|
|
4
|
+
* warning with a single dash (`--line 1 of file x.bib`), so the three dashes are the whole grammar.
|
|
5
|
+
* The message is what stands before them on the line, or, when they start the line, the line before.
|
|
6
|
+
*/
|
|
7
|
+
import type { BibtexError } from "../../domain/sources-record.ts";
|
|
8
|
+
|
|
9
|
+
const PLACE = /^(.*?)---(?:line (\d+) of file|while reading file) (.+)$/;
|
|
10
|
+
const LINE_NUMBER = /---line (\d+) of file/;
|
|
11
|
+
|
|
12
|
+
export function parseBlgErrors(blg: string): readonly BibtexError[] {
|
|
13
|
+
const rows = blg.split(/\r?\n/);
|
|
14
|
+
return rows.flatMap((row, i): readonly BibtexError[] => {
|
|
15
|
+
const m = PLACE.exec(row);
|
|
16
|
+
if (m === null) return [];
|
|
17
|
+
const [, before = "", , file = ""] = m;
|
|
18
|
+
const line = LINE_NUMBER.exec(row)?.[1];
|
|
19
|
+
return [
|
|
20
|
+
{
|
|
21
|
+
message: (before === "" ? (rows[i - 1] ?? "") : before).trim(),
|
|
22
|
+
file: file.trim(),
|
|
23
|
+
line: line === undefined ? null : Number(line),
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pdflatex's `-recorder` file (`paper.fls`): one line per file TeX opened — `INPUT <path>` to read
|
|
3
|
+
* it, `OUTPUT <path>` to write it — in the order TeX opened them, and a `PWD <dir>` line first. The
|
|
4
|
+
* format has no quoting: a path is the rest of its line.
|
|
5
|
+
*/
|
|
6
|
+
import type { Fls, FlsLine } from "../../domain/tex-run.ts";
|
|
7
|
+
|
|
8
|
+
const OPS = ["INPUT", "OUTPUT"] as const;
|
|
9
|
+
|
|
10
|
+
/** `INPUT a.tex` → the line; anything else (`PWD …`, a blank, a word with no path) → none. */
|
|
11
|
+
function lineOf(raw: string): readonly FlsLine[] {
|
|
12
|
+
const op = OPS.find((o) => raw.startsWith(`${o} `));
|
|
13
|
+
const path = op === undefined ? "" : raw.slice(op.length + 1).trimEnd();
|
|
14
|
+
return op === undefined || path === "" ? [] : [{ op, path }];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseFls(text: string): Fls {
|
|
18
|
+
const rows = text.split(/\r?\n/);
|
|
19
|
+
const pwd = rows.find((r) => r.startsWith("PWD "));
|
|
20
|
+
return {
|
|
21
|
+
pwd: pwd === undefined ? null : pwd.slice("PWD ".length).trimEnd(),
|
|
22
|
+
lines: rows.flatMap(lineOf),
|
|
23
|
+
};
|
|
24
|
+
}
|