paperlint 7.2.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 +41 -30
- package/dist/build.js.map +1 -1
- package/dist/claim-provenance.d.ts +2 -2
- package/dist/cli.d.ts +9 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +43 -68
- 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/latex-language.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.js +10 -13
- package/dist/eslint-rules/latex-language.js.map +1 -1
- package/dist/eslint-rules/paper-context.d.ts.map +1 -1
- package/dist/eslint-rules/paper-context.js +6 -5
- 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 +34 -96
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/eslint-rules/papers.d.ts +1 -3
- package/dist/eslint-rules/papers.d.ts.map +1 -1
- package/dist/eslint-rules/papers.js +2 -4
- package/dist/eslint-rules/papers.js.map +1 -1
- package/dist/eslint-rules/tex-build.d.ts.map +1 -1
- package/dist/eslint-rules/tex-build.js +8 -25
- package/dist/eslint-rules/tex-build.js.map +1 -1
- package/dist/init.d.ts +1 -4
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +3 -9
- package/dist/init.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/lib/markdown.d.ts +5 -81
- package/dist/lib/markdown.d.ts.map +1 -1
- package/dist/lib/markdown.js +5 -116
- package/dist/lib/markdown.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +0 -2
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/lib/skill-eval-fixture.d.ts +6 -4
- package/dist/lib/skill-eval-fixture.d.ts.map +1 -1
- package/dist/lib/skill-eval-fixture.js +25 -18
- package/dist/lib/skill-eval-fixture.js.map +1 -1
- package/dist/new-paper.d.ts +5 -11
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +12 -23
- package/dist/new-paper.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-files.d.ts +4 -4
- 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 +17 -0
- package/dist/paper-prose.d.ts.map +1 -0
- package/dist/paper-prose.js +46 -0
- package/dist/paper-prose.js.map +1 -0
- 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/structure.d.ts.map +1 -1
- package/dist/structure.js +4 -3
- package/dist/structure.js.map +1 -1
- 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/dist/types.d.ts +0 -2
- package/dist/types.d.ts.map +1 -1
- package/docs/configuration.md +18 -12
- package/docs/design/paper-sources.md +197 -0
- package/docs/e2e.md +4 -6
- package/docs/package-shape-options.md +1 -1
- package/docs/prior-art/paper-folder-scaffolding.md +24 -29
- 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/tex/claim-provenance.md +0 -1
- package/docs/rules.md +62 -61
- package/eslint-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.ts +10 -13
- package/eslint-rules/paper-context.ts +6 -5
- package/eslint-rules/paper-typography.test.mjs +29 -157
- package/eslint-rules/paper-typography.ts +35 -104
- package/eslint-rules/papers.harness.mjs +1 -1
- package/eslint-rules/papers.ts +2 -4
- package/eslint-rules/tex-build.harness.mjs +2 -2
- package/eslint-rules/tex-build.ts +8 -25
- package/eslint.config.mjs +22 -1
- 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/hooks/hooks.harness.ts +15 -15
- package/hooks/paper-edit-guard.hook.mjs +5 -8
- package/hooks/paper-edit-guard.test.ts +12 -3
- package/hooks/paper-skills-nudge.hook.mjs +2 -3
- package/hooks/paper-skills-nudge.test.ts +13 -3
- package/hooks/paper-status-gates.hook.mjs +2 -2
- package/hooks/paper-status-gates.test.ts +14 -2
- package/lib/markdown.test.mjs +5 -60
- package/lib/markdown.ts +9 -136
- package/lib/paper-config.ts +0 -2
- package/lib/skill-checks.test.mjs +4 -1
- package/lib/skill-eval-fixture.ts +25 -18
- package/package.json +2 -12
- package/presets/realm.jsonc +1 -1
- package/scripts/check.test.ts +23 -1
- package/scripts/check.ts +7 -3
- package/scripts/coverage-config.test.ts +19 -10
- package/scripts/harness-api.frozen.json +0 -5
- package/scripts/layer-legacy-frozen.test.ts +41 -2
- package/scripts/layer-legacy-frozen.ts +31 -2
- package/skills/cold-read-diff/SKILL.md +3 -3
- package/skills/cold-read-diff/SKILL.md.spec.ts +2 -2
- package/skills/grade-paper-writing/SKILL.md +15 -22
- package/skills/grade-paper-writing/SKILL.md.spec.ts +14 -21
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +40 -26
- package/skills/grade-paper-writing/prose-lint.mjs +117 -278
- package/skills/grade-paper-writing/prose-lint.test.ts +230 -79
- package/skills/paper-adversarial-review/SKILL.md +2 -2
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
- package/skills/paper-pipeline/SKILL.md +9 -12
- package/skills/paper-pipeline/SKILL.md.spec.ts +8 -11
- package/skills/paper-pipeline/description-language.eval.mjs +25 -15
- package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +25 -15
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +25 -15
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +27 -16
- package/skills/paper-pipeline/pipeline-language.eval.mjs +25 -15
- package/skills/paper-pipeline/references/acceptance-gate.md +16 -20
- package/skills/paper-pipeline/references/artifact-checklist.md +4 -5
- package/skills/paper-pipeline/references/pipeline-status-template.md +1 -1
- package/skills/paper-pipeline/scripts/README.md +0 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +91 -105
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +85 -241
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +166 -153
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +3 -3
- package/skills/paper-status/SKILL.md +2 -2
- package/skills/paper-status/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/SKILL.md +2 -2
- package/skills/render-paper/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/extract-pdf-facts.mjs +2 -2
- package/skills/tighten-paper/SKILL.md +29 -42
- package/skills/tighten-paper/SKILL.md.spec.ts +28 -41
- 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 -32
- package/skills/verify-citations/scripts/bib-authors.cli.test.ts +138 -43
- package/skills/verify-citations/scripts/bib-authors.d.mts +22 -2
- package/skills/verify-citations/scripts/bib-authors.mjs +54 -193
- package/skills/verify-citations/scripts/bib-authors.test.mjs +1 -44
- 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.harness.mjs +2 -2
- package/src/build.test.ts +211 -1
- package/src/build.ts +101 -41
- package/src/claim-provenance.test.ts +33 -7
- package/src/claim-provenance.ts +2 -2
- package/src/cli.harness.mjs +46 -56
- package/src/cli.test.ts +44 -48
- package/src/cli.ts +51 -82
- 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/folder-venue-rule.test.ts +1 -1
- package/src/heading-case.test.ts +24 -10
- package/src/init.test.ts +6 -7
- package/src/init.ts +3 -22
- package/src/latex-log.ts +20 -0
- package/src/latex-loop.harness.ts +1 -0
- package/src/latex-loop.ts +2 -0
- package/src/lint-fix.test.ts +10 -4
- package/src/linted-set.test.ts +9 -1
- package/src/new-paper.harness.mjs +12 -30
- package/src/new-paper.test.ts +15 -11
- package/src/new-paper.ts +11 -30
- package/src/package-dirs.ts +3 -2
- package/src/paper-files.test.ts +5 -4
- package/src/paper-files.ts +4 -4
- package/src/paper-includes.test.ts +157 -38
- package/src/paper-includes.ts +39 -35
- package/src/paper-prose.test.ts +78 -0
- package/src/paper-prose.ts +67 -0
- 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/parse-args.test.ts +0 -1
- 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/structure.harness.ts +5 -11
- package/src/structure.ts +4 -3
- 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/src/types.ts +0 -2
- 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
- package/fixtures/real-markdown-paper/baseline.json +0 -22
- package/fixtures/real-markdown-paper/baseline.ts +0 -70
- package/skills/paper-pipeline/PIPELINE-MAP.md +0 -381
- package/skills/paper-pipeline/pipeline-firing.baseline.json +0 -270
- package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +0 -585
- package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +0 -492
- package/skills/paper-pipeline/scripts/artifact-coverage.mjs +0 -397
- package/skills/paper-pipeline/scripts/artifact-coverage.test.ts +0 -86
- package/skills/paper-pipeline/scripts/check-provenance.mjs +0 -184
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +0 -135
- package/skills/paper-pipeline/scripts/population-map.mjs +0 -188
- package/skills/paper-pipeline/scripts/population-map.test.mjs +0 -154
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +0 -266
- package/skills/paper-pipeline/scripts/round-diff.harness.ts +0 -907
- package/skills/paper-pipeline/scripts/round-diff.mjs +0 -583
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +0 -159
- package/skills/tighten-paper/__snapshots__/structure.test.ts.snap +0 -133
- package/skills/tighten-paper/structure.mjs +0 -364
- package/skills/tighten-paper/structure.test.ts +0 -117
- package/templates/paper/paper.md +0 -4
package/src/references.test.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The references a build checked, end to end without a network: the `references` build step
|
|
2
|
+
* The references a build checked, end to end without a network: the `references` build step checks
|
|
3
|
+
* the databases the build's bibtex opened (`_build/sources.json`, docs/design/paper-sources.md §1),
|
|
3
4
|
* records what a (fake) checker answers, and the offline rules judge that record.
|
|
4
5
|
*
|
|
5
|
-
* a pass is recorded, with the hash of the
|
|
6
|
-
* one mismatched entry → paper/author-list on THAT entry
|
|
7
|
-
*
|
|
6
|
+
* a pass is recorded, with the hash of the databases checked
|
|
7
|
+
* one mismatched entry → paper/author-list on THAT entry, at the file, line and column it is in
|
|
8
|
+
* a database edited after the verdicts → paper/refs-fresh, and the per-entry rules go quiet
|
|
8
9
|
* offline → the step still succeeds (the build is not failed) and paper/refs-checked warns
|
|
10
|
+
* no record of the build, or a stale one → the step checks nothing and the rules are silent
|
|
11
|
+
*
|
|
12
|
+
* The planted papers of `fixtures/paper-sources/` supply the record: each one's `tex-truth.json` is
|
|
13
|
+
* what the build wrote for it, taken from TeX's own files.
|
|
9
14
|
*/
|
|
10
15
|
import { describe, expect, it } from "vitest";
|
|
11
16
|
import { ESLint } from "eslint";
|
|
@@ -14,42 +19,49 @@ import { join } from "node:path";
|
|
|
14
19
|
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
15
20
|
import { ok } from "./domain/result.ts";
|
|
16
21
|
import { absolutePath } from "./domain/paths.ts";
|
|
22
|
+
import { sha256Hex } from "./domain/sha256.ts";
|
|
23
|
+
import {
|
|
24
|
+
serializeSourcesRecord,
|
|
25
|
+
type SourcesRecord,
|
|
26
|
+
} from "./domain/sources-record.ts";
|
|
17
27
|
import type {
|
|
18
28
|
CheckReferences,
|
|
19
29
|
EntryVerdict,
|
|
20
30
|
} from "./ports/check-references.ts";
|
|
21
31
|
import { referencesStep } from "./build.ts";
|
|
32
|
+
import { notWiredRecord } from "./sources-record.ts";
|
|
22
33
|
import {
|
|
23
34
|
bibHash,
|
|
24
|
-
|
|
35
|
+
bibliographyReader,
|
|
25
36
|
lookupCachePath,
|
|
37
|
+
notWiredBibliography,
|
|
38
|
+
recordReferences,
|
|
26
39
|
readReferences,
|
|
27
40
|
referencesPath,
|
|
41
|
+
unseenKeys,
|
|
28
42
|
} from "./references.ts";
|
|
43
|
+
import { bibReader } from "./adapters/bibtex/index.ts";
|
|
44
|
+
import { latexReader } from "./adapters/latex/index.ts";
|
|
45
|
+
import { sourcesCodec } from "./adapters/sources-record/index.ts";
|
|
29
46
|
import {
|
|
30
47
|
parseLookupCache,
|
|
31
48
|
serializeLookupCache,
|
|
32
49
|
type CachedResponse,
|
|
33
50
|
type LookupCache,
|
|
34
51
|
} from "./domain/lookup-cache.ts";
|
|
52
|
+
import { bibtexRead } from "./latex-log.ts";
|
|
35
53
|
import { referenceRules, REFERENCE_RULE_LEVELS } from "./reference-rules.ts";
|
|
36
54
|
import { texLanguage } from "../eslint-rules/latex-language.ts";
|
|
37
55
|
import { present } from "../test/support.ts";
|
|
56
|
+
import { builtFixture } from "../test/recorded-fixture.ts";
|
|
38
57
|
|
|
39
58
|
const PAPER = "/work/papers/p";
|
|
40
|
-
const TEX = (entries: string) =>
|
|
41
|
-
[
|
|
42
|
-
"\\documentclass{acmart}",
|
|
43
|
-
"\\begin{filecontents*}{refs.bib}",
|
|
44
|
-
entries,
|
|
45
|
-
"\\end{filecontents*}",
|
|
46
|
-
"\\begin{document}",
|
|
47
|
-
"x",
|
|
48
|
-
"\\end{document}",
|
|
49
|
-
"",
|
|
50
|
-
].join("\n");
|
|
51
59
|
const ENTRIES =
|
|
52
|
-
"@inproceedings{schick2023,\n title = {Toolformer},\n author = {Schick, Timo},\n booktitle = {NeurIPS}\n}\n@misc{other,\n title = {Other},\n url = {https://x.org}\n}";
|
|
60
|
+
"@inproceedings{schick2023,\n title = {Toolformer},\n author = {Schick, Timo},\n booktitle = {NeurIPS}\n}\n@misc{other,\n title = {Other},\n url = {https://x.org}\n}\n";
|
|
61
|
+
const TEX = "\\documentclass{article}\n\\begin{document}x\\end{document}\n";
|
|
62
|
+
|
|
63
|
+
type Files = ReturnType<typeof memoryFiles>;
|
|
64
|
+
const bytes = (s: string) => new TextEncoder().encode(s);
|
|
53
65
|
|
|
54
66
|
const verdict = (
|
|
55
67
|
key: string,
|
|
@@ -74,9 +86,32 @@ const offline: CheckReferences = (_bib, cache) =>
|
|
|
74
86
|
},
|
|
75
87
|
cache,
|
|
76
88
|
});
|
|
89
|
+
/** A checker that hands back a passing verdict for each entry it is given, and records the keys. */
|
|
90
|
+
function passing() {
|
|
91
|
+
const handed: string[] = [];
|
|
92
|
+
const check: CheckReferences = (bib, cache) => {
|
|
93
|
+
handed.push(...bib.map((e) => e.key));
|
|
94
|
+
return Promise.resolve({
|
|
95
|
+
check: { kind: "checked", entries: bib.map((e) => verdict(e.key)) },
|
|
96
|
+
cache,
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
return { check, handed };
|
|
100
|
+
}
|
|
101
|
+
const never: CheckReferences = () => Promise.reject(new Error("never asked"));
|
|
102
|
+
|
|
103
|
+
/** What reads a paper's record and the databases it names: the disk, the schema, the bibtex reader. */
|
|
104
|
+
const depsOf = (files: Files) => ({
|
|
105
|
+
files,
|
|
106
|
+
codec: sourcesCodec,
|
|
107
|
+
latex: latexReader,
|
|
108
|
+
bib: bibReader,
|
|
109
|
+
});
|
|
77
110
|
|
|
78
111
|
/** The step's context over `files`, with every port it does not use stubbed. */
|
|
79
|
-
const ctx = (files:
|
|
112
|
+
const ctx = (files: Files) => ({
|
|
113
|
+
readBibliography: bibliographyReader(depsOf(files)),
|
|
114
|
+
recordSources: notWiredRecord,
|
|
80
115
|
paperDir: PAPER,
|
|
81
116
|
env: {},
|
|
82
117
|
run: () => ({ status: 0 }),
|
|
@@ -89,9 +124,15 @@ const ctx = (files: ReturnType<typeof memoryFiles>) => ({
|
|
|
89
124
|
files,
|
|
90
125
|
});
|
|
91
126
|
|
|
127
|
+
/** The bibliography the record in `files` names, read as the step reads it. */
|
|
128
|
+
const readIn = (files: Files) => {
|
|
129
|
+
const r = bibliographyReader(depsOf(files))(PAPER);
|
|
130
|
+
if (r.kind !== "read") throw new Error(`not read: ${r.kind}`);
|
|
131
|
+
return r.bibliography;
|
|
132
|
+
};
|
|
133
|
+
|
|
92
134
|
/** Run the build step on a paper held in memory; returns the files and the step's outcome. */
|
|
93
|
-
async function build(
|
|
94
|
-
const files = memoryFiles({ [`${PAPER}/paper.tex`]: tex });
|
|
135
|
+
async function build(files: Files, check: CheckReferences) {
|
|
95
136
|
const out = await referencesStep.run({
|
|
96
137
|
...ctx(files),
|
|
97
138
|
checkReferences: check,
|
|
@@ -99,11 +140,10 @@ async function build(tex: string, check: CheckReferences) {
|
|
|
99
140
|
return { files, out };
|
|
100
141
|
}
|
|
101
142
|
|
|
102
|
-
/** Lint `tex`
|
|
103
|
-
async function lint(files:
|
|
104
|
-
|
|
105
|
-
absolutePath(`${PAPER}/paper.tex`),
|
|
106
|
-
new TextEncoder().encode(tex),
|
|
143
|
+
/** Lint `paper.tex` of the paper in `files`. */
|
|
144
|
+
async function lint(files: Files) {
|
|
145
|
+
const tex = new TextDecoder().decode(
|
|
146
|
+
present(files.readBytes(absolutePath(`${PAPER}/paper.tex`)), "a paper.tex"),
|
|
107
147
|
);
|
|
108
148
|
const eslint = new ESLint({
|
|
109
149
|
cwd: "/work",
|
|
@@ -113,7 +153,7 @@ async function lint(files: ReturnType<typeof memoryFiles>, tex: string) {
|
|
|
113
153
|
files: ["**/paper.tex"],
|
|
114
154
|
plugins: {
|
|
115
155
|
tex: { languages: { latex: texLanguage } },
|
|
116
|
-
paper: { rules: referenceRules(
|
|
156
|
+
paper: { rules: referenceRules(depsOf(files)) },
|
|
117
157
|
},
|
|
118
158
|
language: "tex/latex",
|
|
119
159
|
rules: REFERENCE_RULE_LEVELS,
|
|
@@ -126,28 +166,102 @@ async function lint(files: ReturnType<typeof memoryFiles>, tex: string) {
|
|
|
126
166
|
return present(res, "one lint result").messages;
|
|
127
167
|
}
|
|
128
168
|
|
|
169
|
+
// ── a paper in memory, with the record its build would have written ────────────────────────
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A paper whose build opened `databases` (name → text), typeset `keys`, and wrote `written` itself. The
|
|
173
|
+
* record is what `paperlint build` writes for it: the bytes of paper.tex and of every database TeX did
|
|
174
|
+
* not write, hashed.
|
|
175
|
+
*/
|
|
176
|
+
function built(o: {
|
|
177
|
+
readonly databases: Readonly<Record<string, string>>;
|
|
178
|
+
readonly keys: readonly string[];
|
|
179
|
+
readonly written?: readonly string[];
|
|
180
|
+
}): Files {
|
|
181
|
+
const written = o.written ?? [];
|
|
182
|
+
const record: SourcesRecord = {
|
|
183
|
+
schema: 1,
|
|
184
|
+
inputs: [{ path: "paper.tex", role: "body" }],
|
|
185
|
+
written,
|
|
186
|
+
bibdata: Object.keys(o.databases).map((d) => d.replace(/\.bib$/, "")),
|
|
187
|
+
bibtex: {
|
|
188
|
+
ran: true,
|
|
189
|
+
databases: Object.keys(o.databases),
|
|
190
|
+
keys: o.keys,
|
|
191
|
+
exit: 0,
|
|
192
|
+
errors: [],
|
|
193
|
+
},
|
|
194
|
+
sha256: Object.fromEntries([
|
|
195
|
+
["paper.tex", sha256Hex(bytes(TEX))] as const,
|
|
196
|
+
...Object.entries(o.databases)
|
|
197
|
+
.filter(([name]) => !written.includes(name))
|
|
198
|
+
.map(([name, text]) => [name, sha256Hex(bytes(text))] as const),
|
|
199
|
+
]),
|
|
200
|
+
};
|
|
201
|
+
return memoryFiles({
|
|
202
|
+
[`${PAPER}/paper.tex`]: TEX,
|
|
203
|
+
[`${PAPER}/_build/sources.json`]: serializeSourcesRecord(record),
|
|
204
|
+
...Object.fromEntries(
|
|
205
|
+
Object.entries(o.databases).map(([name, text]) => [
|
|
206
|
+
`${PAPER}/${name}`,
|
|
207
|
+
text,
|
|
208
|
+
]),
|
|
209
|
+
),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The ordinary paper: refs.bib with two entries, both typeset. */
|
|
214
|
+
const ordinary = () =>
|
|
215
|
+
built({
|
|
216
|
+
databases: { "refs.bib": ENTRIES },
|
|
217
|
+
keys: ["schick2023", "other"],
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
/** Rewrite the paper's record with `patch` laid over it. */
|
|
221
|
+
function patchRecord(files: Files, patch: (r: SourcesRecord) => SourcesRecord) {
|
|
222
|
+
const path = `${PAPER}/_build/sources.json`;
|
|
223
|
+
const r = sourcesCodec.parse(
|
|
224
|
+
new TextDecoder().decode(present(files.map.get(path), "a record")),
|
|
225
|
+
);
|
|
226
|
+
if (!r.ok) throw new Error(r.why);
|
|
227
|
+
files.map.set(path, bytes(serializeSourcesRecord(patch(r.record))));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── the planted papers of fixtures/paper-sources, with the record TeX wrote for each ───────
|
|
231
|
+
|
|
232
|
+
/** A planted paper under PAPER, and its `tex-truth.json` as the build's record. */
|
|
233
|
+
const planted = (paper: string, extra: Readonly<Record<string, string>> = {}) =>
|
|
234
|
+
memoryFiles({
|
|
235
|
+
...builtFixture(paper, PAPER),
|
|
236
|
+
...Object.fromEntries(
|
|
237
|
+
Object.entries(extra).map(([name, text]) => [`${PAPER}/${name}`, text]),
|
|
238
|
+
),
|
|
239
|
+
});
|
|
240
|
+
|
|
129
241
|
describe("the references build step", () => {
|
|
130
|
-
it("records a pass, with the hash of the
|
|
131
|
-
const
|
|
132
|
-
const {
|
|
133
|
-
|
|
242
|
+
it("records a pass, with the hash of the databases it checked and no record of what bibtex read", async () => {
|
|
243
|
+
const files = ordinary();
|
|
244
|
+
const { out } = await build(
|
|
245
|
+
files,
|
|
134
246
|
checker([verdict("schick2023"), verdict("other")]),
|
|
135
247
|
);
|
|
136
248
|
expect(out.ok).toBe(true);
|
|
137
249
|
const doc = readReferences(files, PAPER);
|
|
138
250
|
expect(doc?.status).toBe("checked");
|
|
139
|
-
expect(doc?.bib
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
251
|
+
expect(doc?.bib).toEqual({
|
|
252
|
+
sources: ["refs.bib"],
|
|
253
|
+
sha256: bibHash(readIn(files)),
|
|
254
|
+
});
|
|
255
|
+
// What bibtex read lives in sources.json, once.
|
|
256
|
+
expect(doc).not.toHaveProperty("bibtex");
|
|
257
|
+
expect(await lint(files)).toEqual([]);
|
|
143
258
|
});
|
|
144
259
|
|
|
145
260
|
it("🔴 offline: the step succeeds — the build is not failed — and lint warns once", async () => {
|
|
146
|
-
const
|
|
147
|
-
const { files, out } = await build(tex, offline);
|
|
261
|
+
const { files, out } = await build(ordinary(), offline);
|
|
148
262
|
expect(out.ok).toBe(true);
|
|
149
263
|
expect(readReferences(files, PAPER)?.status).toBe("not-checked");
|
|
150
|
-
const msgs = await lint(files
|
|
264
|
+
const msgs = await lint(files);
|
|
151
265
|
expect(msgs.map((m) => [m.ruleId, m.severity])).toEqual([
|
|
152
266
|
["paper/refs-checked", 1],
|
|
153
267
|
]);
|
|
@@ -155,7 +269,7 @@ describe("the references build step", () => {
|
|
|
155
269
|
});
|
|
156
270
|
|
|
157
271
|
it("a checker that throws is recorded as not checked, not as a failed build", async () => {
|
|
158
|
-
const { files, out } = await build(
|
|
272
|
+
const { files, out } = await build(ordinary(), () =>
|
|
159
273
|
Promise.reject(new Error("DBLP exploded")),
|
|
160
274
|
);
|
|
161
275
|
expect(out.ok).toBe(true);
|
|
@@ -184,7 +298,7 @@ function fetching(key: string) {
|
|
|
184
298
|
};
|
|
185
299
|
return { check, seen };
|
|
186
300
|
}
|
|
187
|
-
const cacheText = (files:
|
|
301
|
+
const cacheText = (files: Files): string | null => {
|
|
188
302
|
const b = files.readBytes(absolutePath(lookupCachePath(PAPER)));
|
|
189
303
|
return b === null ? null : new TextDecoder().decode(b);
|
|
190
304
|
};
|
|
@@ -192,7 +306,7 @@ const cacheText = (files: ReturnType<typeof memoryFiles>): string | null => {
|
|
|
192
306
|
describe("the lookup cache — <paper>/repro/references-cache.json", () => {
|
|
193
307
|
it("a build that fetched writes the cache; the next build is handed it and writes nothing", async () => {
|
|
194
308
|
const f = fetching("crossref:doi:10.1/x");
|
|
195
|
-
const { files, out } = await build(
|
|
309
|
+
const { files, out } = await build(ordinary(), f.check);
|
|
196
310
|
expect(out).toEqual({
|
|
197
311
|
ok: true,
|
|
198
312
|
note: "references: 1 checked, 0 failing → _build/references.json; 1 new answer → repro/references-cache.json",
|
|
@@ -220,7 +334,7 @@ describe("the lookup cache — <paper>/repro/references-cache.json", () => {
|
|
|
220
334
|
});
|
|
221
335
|
});
|
|
222
336
|
|
|
223
|
-
describe("the lookup cache — counting
|
|
337
|
+
describe("the lookup cache — counting", () => {
|
|
224
338
|
it("two new answers are counted as two", async () => {
|
|
225
339
|
const two: CheckReferences = (_bib, cache) =>
|
|
226
340
|
Promise.resolve({
|
|
@@ -233,7 +347,7 @@ describe("the lookup cache — counting and refusing", () => {
|
|
|
233
347
|
dblp: cache.dblp,
|
|
234
348
|
},
|
|
235
349
|
});
|
|
236
|
-
const { out } = await build(
|
|
350
|
+
const { out } = await build(ordinary(), two);
|
|
237
351
|
expect(out).toEqual({
|
|
238
352
|
ok: true,
|
|
239
353
|
note: "references: 1 checked, 0 failing → _build/references.json; 2 new answers → repro/references-cache.json",
|
|
@@ -241,18 +355,46 @@ describe("the lookup cache — counting and refusing", () => {
|
|
|
241
355
|
});
|
|
242
356
|
|
|
243
357
|
it("no answer fetched and no cache on disk: no cache file is created", async () => {
|
|
244
|
-
const { files } = await build(
|
|
245
|
-
TEX(ENTRIES),
|
|
246
|
-
checker([verdict("schick2023")]),
|
|
247
|
-
);
|
|
358
|
+
const { files } = await build(ordinary(), checker([verdict("schick2023")]));
|
|
248
359
|
expect(cacheText(files)).toBeNull();
|
|
249
360
|
});
|
|
250
361
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
362
|
+
// Guards: an answer asked again after MAX_AGE_DAYS keeps its key, so the key count does not change.
|
|
363
|
+
// Counted by size, the refreshed answer was not written, and every later build asked it again.
|
|
364
|
+
it("an answer refreshed under the same key is counted and written", async () => {
|
|
365
|
+
const OLD: CachedResponse = { ...ANSWER, fetched: "2026-08-01" };
|
|
366
|
+
const put =
|
|
367
|
+
(answer: CachedResponse): CheckReferences =>
|
|
368
|
+
(_bib, cache) =>
|
|
369
|
+
Promise.resolve({
|
|
370
|
+
check: { kind: "checked", entries: [verdict("schick2023")] },
|
|
371
|
+
cache: {
|
|
372
|
+
citations: new Map([...cache.citations, ["a", answer]]),
|
|
373
|
+
dblp: cache.dblp,
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
const { files } = await build(ordinary(), put(OLD));
|
|
377
|
+
const again = await referencesStep.run({
|
|
378
|
+
...ctx(files),
|
|
379
|
+
checkReferences: put(ANSWER),
|
|
380
|
+
});
|
|
381
|
+
expect({ again, file: cacheText(files) }).toEqual({
|
|
382
|
+
again: {
|
|
383
|
+
ok: true,
|
|
384
|
+
note: "references: 1 checked, 0 failing → _build/references.json; 1 new answer → repro/references-cache.json",
|
|
385
|
+
},
|
|
386
|
+
file: serializeLookupCache({
|
|
387
|
+
citations: new Map([["a", ANSWER]]),
|
|
388
|
+
dblp: new Map(),
|
|
389
|
+
}),
|
|
255
390
|
});
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
describe("the lookup cache — refusing", () => {
|
|
395
|
+
it("🔴 a cache that does not parse is refused BY NAME and left alone — the check is not run over half a cache", async () => {
|
|
396
|
+
const files = ordinary();
|
|
397
|
+
files.map.set(lookupCachePath(PAPER), bytes('{"schema": 7}'));
|
|
256
398
|
const f = fetching("k");
|
|
257
399
|
const out = await referencesStep.run({
|
|
258
400
|
...ctx(files),
|
|
@@ -269,83 +411,161 @@ describe("the lookup cache — counting and refusing", () => {
|
|
|
269
411
|
});
|
|
270
412
|
|
|
271
413
|
describe("the reference rules", () => {
|
|
272
|
-
it("one mismatched entry → paper/author-list
|
|
273
|
-
const tex = TEX(ENTRIES);
|
|
414
|
+
it("one mismatched entry → paper/author-list at that entry, naming its file, line and column", async () => {
|
|
274
415
|
const { files } = await build(
|
|
275
|
-
|
|
416
|
+
ordinary(),
|
|
276
417
|
checker([verdict("schick2023", "mismatch"), verdict("other")]),
|
|
277
418
|
);
|
|
278
|
-
const msgs = await lint(files
|
|
279
|
-
expect(msgs.map((m) => m.ruleId)).toEqual([
|
|
280
|
-
|
|
281
|
-
|
|
419
|
+
const msgs = await lint(files);
|
|
420
|
+
expect(msgs.map((m) => [m.ruleId, m.line, m.column])).toEqual([
|
|
421
|
+
["paper/author-list", 1, 1],
|
|
422
|
+
]);
|
|
423
|
+
expect(msgs[0]?.message).toMatch(
|
|
424
|
+
/^refs\.bib:1:1: `schick2023`.*missing hambro/,
|
|
425
|
+
);
|
|
282
426
|
});
|
|
283
427
|
|
|
284
428
|
it("an identifier that provably fails → paper/cite-exists on that entry", async () => {
|
|
285
|
-
const tex = TEX(ENTRIES);
|
|
286
429
|
const { files } = await build(
|
|
287
|
-
|
|
430
|
+
ordinary(),
|
|
288
431
|
checker([verdict("schick2023"), verdict("other", "skipped", "false")]),
|
|
289
432
|
);
|
|
290
|
-
const msgs = await lint(files
|
|
291
|
-
expect(msgs.map((m) => [m.ruleId, m.
|
|
292
|
-
["paper/cite-exists",
|
|
433
|
+
const msgs = await lint(files);
|
|
434
|
+
expect(msgs.map((m) => [m.ruleId, m.message.split(": ")[0]])).toEqual([
|
|
435
|
+
["paper/cite-exists", "refs.bib:6:1"],
|
|
293
436
|
]);
|
|
294
437
|
});
|
|
295
438
|
|
|
296
|
-
it("
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
);
|
|
301
|
-
|
|
302
|
-
|
|
439
|
+
it("a database in a subfolder is named as bibtex names it", async () => {
|
|
440
|
+
const files = built({
|
|
441
|
+
databases: { "bibs/x.bib": ENTRIES },
|
|
442
|
+
keys: ["schick2023", "other"],
|
|
443
|
+
});
|
|
444
|
+
await build(
|
|
445
|
+
files,
|
|
446
|
+
checker([verdict("schick2023"), verdict("other", "skipped", "false")]),
|
|
303
447
|
);
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
["paper/refs-fresh", 2],
|
|
448
|
+
expect((await lint(files)).map((m) => m.message.split(": ")[0])).toEqual([
|
|
449
|
+
"bibs/x.bib:6:1",
|
|
307
450
|
]);
|
|
308
451
|
});
|
|
309
452
|
|
|
310
|
-
it("
|
|
311
|
-
const
|
|
312
|
-
const
|
|
313
|
-
|
|
453
|
+
it("only paper.tex is judged", () => {
|
|
454
|
+
const rules = referenceRules(depsOf(memoryFiles({})));
|
|
455
|
+
for (const rule of Object.values(rules))
|
|
456
|
+
expect(
|
|
457
|
+
rule.create({
|
|
458
|
+
filename: `${PAPER}/notes.tex`,
|
|
459
|
+
cwd: "/work",
|
|
460
|
+
sourceCode: {
|
|
461
|
+
raw: "",
|
|
462
|
+
getLocFromIndex: () => ({ line: 1, column: 0 }),
|
|
463
|
+
},
|
|
464
|
+
report: () => {
|
|
465
|
+
throw new Error("a file that is not paper.tex is never reported");
|
|
466
|
+
},
|
|
467
|
+
}),
|
|
468
|
+
).toEqual({});
|
|
314
469
|
});
|
|
470
|
+
});
|
|
315
471
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
472
|
+
describe("the references step and the rules, when there is no usable record of the build", () => {
|
|
473
|
+
it("🔴 never built: the step checks nothing and records nothing; the rules are silent — paper/sources-fresh speaks", async () => {
|
|
474
|
+
const files = memoryFiles({
|
|
475
|
+
[`${PAPER}/paper.tex`]: TEX,
|
|
476
|
+
[`${PAPER}/refs.bib`]: ENTRIES,
|
|
477
|
+
});
|
|
478
|
+
const { out } = await build(files, never);
|
|
479
|
+
expect(out).toEqual({
|
|
480
|
+
ok: true,
|
|
481
|
+
note: "references NOT checked — the build recorded no sources (_build/sources.json was not written); lint will say so",
|
|
482
|
+
});
|
|
483
|
+
expect(readReferences(files, PAPER)).toBeNull();
|
|
484
|
+
expect(await lint(files)).toEqual([]);
|
|
321
485
|
});
|
|
322
486
|
|
|
323
|
-
it("a
|
|
324
|
-
const files =
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
),
|
|
330
|
-
)
|
|
331
|
-
expect(referencesPath(PAPER)).toBe(`${PAPER}/_build/references.json`);
|
|
487
|
+
it("a record this paperlint cannot read is named", async () => {
|
|
488
|
+
const files = ordinary();
|
|
489
|
+
files.map.set(`${PAPER}/_build/sources.json`, bytes('{"schema":2}'));
|
|
490
|
+
const { out } = await build(files, never);
|
|
491
|
+
expect(out).toEqual({
|
|
492
|
+
ok: true,
|
|
493
|
+
note: "references NOT checked — _build/sources.json cannot be used (sources.json is schema 2, this paperlint reads schema 1); lint will say so",
|
|
494
|
+
});
|
|
332
495
|
});
|
|
333
|
-
});
|
|
334
496
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
497
|
+
it("🔴 a record older than the paper: the step checks nothing, and the verdicts already on disk are not judged", async () => {
|
|
498
|
+
const files = ordinary();
|
|
499
|
+
await build(
|
|
500
|
+
files,
|
|
501
|
+
checker([verdict("schick2023", "mismatch"), verdict("other")]),
|
|
502
|
+
);
|
|
503
|
+
files.map.set(
|
|
504
|
+
`${PAPER}/refs.bib`,
|
|
505
|
+
bytes(ENTRIES.replace("Schick, Timo", "Schick, Timo and Hambro, Eric")),
|
|
506
|
+
);
|
|
507
|
+
expect(await lint(files)).toEqual([]);
|
|
508
|
+
const { out } = await build(files, never);
|
|
509
|
+
expect(out).toEqual({
|
|
510
|
+
ok: true,
|
|
511
|
+
note: "references NOT checked — the paper changed since the build recorded it (refs.bib edited); lint will say so",
|
|
342
512
|
});
|
|
343
513
|
});
|
|
344
514
|
|
|
515
|
+
it("no reader wired into the build: it says so, and checks nothing", async () => {
|
|
516
|
+
const note = await recordReferences(
|
|
517
|
+
ordinary(),
|
|
518
|
+
PAPER,
|
|
519
|
+
notWiredBibliography(PAPER),
|
|
520
|
+
never,
|
|
521
|
+
);
|
|
522
|
+
expect(note).toBe(
|
|
523
|
+
"references NOT checked — no bibliography reader was wired into this build; lint will say so",
|
|
524
|
+
);
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
describe("the reference rules, when the verdicts are not about the databases the record names", () => {
|
|
529
|
+
it("🔴 a later build recorded the databases, but its references step did not run → refs-fresh, and the stale verdicts are not judged", async () => {
|
|
530
|
+
const files = ordinary();
|
|
531
|
+
await build(
|
|
532
|
+
files,
|
|
533
|
+
checker([verdict("schick2023", "mismatch"), verdict("other")]),
|
|
534
|
+
);
|
|
535
|
+
// The next build edited refs.bib and recorded it (sources.json is rewritten after every compile)
|
|
536
|
+
// and stopped before the references step: references.json is about the earlier refs.bib.
|
|
537
|
+
const edited = ENTRIES.replace("Schick, Timo", "Schick, Timo and Hambro");
|
|
538
|
+
files.map.set(`${PAPER}/refs.bib`, bytes(edited));
|
|
539
|
+
patchRecord(files, (r) => ({
|
|
540
|
+
...r,
|
|
541
|
+
sha256: { ...r.sha256, "refs.bib": sha256Hex(bytes(edited)) },
|
|
542
|
+
}));
|
|
543
|
+
const msgs = await lint(files);
|
|
544
|
+
expect(msgs.map((m) => [m.ruleId, m.severity])).toEqual([
|
|
545
|
+
["paper/refs-fresh", 2],
|
|
546
|
+
]);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("the same databases, unchanged → silent", async () => {
|
|
550
|
+
const files = ordinary();
|
|
551
|
+
await build(files, checker([verdict("schick2023"), verdict("other")]));
|
|
552
|
+
expect(await lint(files)).toEqual([]);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("a paper whose build ran no bibtex gets nothing", async () => {
|
|
556
|
+
const files = planted("v15-after-end");
|
|
557
|
+
expect(await lint(files)).toEqual([]);
|
|
558
|
+
expect(referencesPath(PAPER)).toBe(`${PAPER}/_build/references.json`);
|
|
559
|
+
});
|
|
560
|
+
|
|
345
561
|
it("a references.json of another schema, without entries, or not JSON reads as none", () => {
|
|
346
562
|
for (const body of [
|
|
347
563
|
JSON.stringify({ schema: 999, entries: [] }),
|
|
348
|
-
|
|
564
|
+
// Schema 1 named one file in `bib.source`; the next build rewrites it.
|
|
565
|
+
JSON.stringify({ schema: 1, entries: [] }),
|
|
566
|
+
// Schema 2 recorded what bibtex read here; sources.json holds it now.
|
|
567
|
+
JSON.stringify({ schema: 2, entries: [] }),
|
|
568
|
+
JSON.stringify({ schema: 3 }),
|
|
349
569
|
"null",
|
|
350
570
|
"{",
|
|
351
571
|
]) {
|
|
@@ -355,116 +575,239 @@ describe("reading what is on disk", () => {
|
|
|
355
575
|
});
|
|
356
576
|
});
|
|
357
577
|
|
|
358
|
-
/** A
|
|
359
|
-
const
|
|
360
|
-
files: ReturnType<typeof memoryFiles>,
|
|
361
|
-
body: Record<string, unknown>,
|
|
362
|
-
) => {
|
|
363
|
-
const bib = present(bibliographyOf(files, PAPER), "the bibliography");
|
|
578
|
+
/** A references.json for the paper's databases as the build would write it, with `body` laid over it. */
|
|
579
|
+
const writeVerdicts = (files: Files, body: Record<string, unknown>): void => {
|
|
364
580
|
files.writeAtomic(
|
|
365
581
|
absolutePath(referencesPath(PAPER)),
|
|
366
|
-
|
|
582
|
+
bytes(
|
|
367
583
|
JSON.stringify({
|
|
368
|
-
schema:
|
|
369
|
-
bib: {
|
|
584
|
+
schema: 3,
|
|
585
|
+
bib: { sources: ["refs.bib"], sha256: bibHash(readIn(files)) },
|
|
370
586
|
...body,
|
|
371
587
|
}),
|
|
372
588
|
),
|
|
373
589
|
);
|
|
374
590
|
};
|
|
375
591
|
|
|
376
|
-
describe("the reference rules over a
|
|
592
|
+
describe("the reference rules over a references.json written by hand", () => {
|
|
377
593
|
it("not-checked with no reason recorded says so", async () => {
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const msgs = await lint(files, tex);
|
|
594
|
+
const files = ordinary();
|
|
595
|
+
writeVerdicts(files, { status: "not-checked", entries: [] });
|
|
596
|
+
const msgs = await lint(files);
|
|
382
597
|
expect(msgs.map((m) => m.ruleId)).toEqual(["paper/refs-checked"]);
|
|
383
598
|
expect(msgs[0]?.message).toMatch(/no reason recorded/);
|
|
384
599
|
});
|
|
385
600
|
|
|
386
|
-
it("a
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
601
|
+
it("a record of the build and no verdicts yet: the references have not been checked", async () => {
|
|
602
|
+
const msgs = await lint(ordinary());
|
|
603
|
+
expect(msgs.map((m) => [m.ruleId, m.severity])).toEqual([
|
|
604
|
+
["paper/refs-checked", 1],
|
|
605
|
+
]);
|
|
606
|
+
expect(msgs[0]?.message).toMatch(/npx paperlint build/);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it("a verdict without a reason still reports, on the entry it is about", async () => {
|
|
610
|
+
const files = ordinary();
|
|
611
|
+
writeVerdicts(files, {
|
|
390
612
|
status: "checked",
|
|
391
613
|
entries: [
|
|
392
614
|
{ key: "schick2023", exists: "true", authors: "mismatch" },
|
|
393
|
-
{ key: "
|
|
615
|
+
{ key: "other", exists: "false", authors: "match" },
|
|
394
616
|
],
|
|
395
617
|
});
|
|
396
|
-
const msgs = await lint(files
|
|
397
|
-
expect(msgs.map((m) => [m.ruleId, m.
|
|
398
|
-
["paper/
|
|
399
|
-
["paper/
|
|
618
|
+
const msgs = await lint(files);
|
|
619
|
+
expect(msgs.map((m) => [m.ruleId, m.message.split(": ")[0]])).toEqual([
|
|
620
|
+
["paper/author-list", "refs.bib:1:1"],
|
|
621
|
+
["paper/cite-exists", "refs.bib:6:1"],
|
|
400
622
|
]);
|
|
401
623
|
});
|
|
624
|
+
|
|
625
|
+
it("verdicts that are not one per entry, in order, are not about these databases: refs-fresh", async () => {
|
|
626
|
+
const files = ordinary();
|
|
627
|
+
const judged = async (entries: readonly EntryVerdict[]) => {
|
|
628
|
+
writeVerdicts(files, { status: "checked", entries });
|
|
629
|
+
return (await lint(files)).map((m) => m.ruleId);
|
|
630
|
+
};
|
|
631
|
+
expect([
|
|
632
|
+
await judged([verdict("schick2023", "mismatch")]),
|
|
633
|
+
await judged([verdict("other"), verdict("schick2023", "mismatch")]),
|
|
634
|
+
]).toEqual([["paper/refs-fresh"], ["paper/refs-fresh"]]);
|
|
635
|
+
});
|
|
402
636
|
});
|
|
403
637
|
|
|
404
|
-
describe("the
|
|
405
|
-
it
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
638
|
+
describe("the references step checks the databases bibtex opened (tex-truth.json)", () => {
|
|
639
|
+
it.each([
|
|
640
|
+
["v1-stale", ["refs.bib"], ["stale2020"]],
|
|
641
|
+
["v3-declared", ["paper.bib"], ["declared2023"]],
|
|
642
|
+
// Two databases in one `\bibliography`, in the order bibtex opened them.
|
|
643
|
+
["v26-comment-in-names", ["one.bib", "two.bib"], ["onekey", "twokey"]],
|
|
644
|
+
// Both databases sit in the folder; the switch decided which one TeX read, and bibtex opened one.
|
|
645
|
+
["v10-ifthenelse", ["anon.bib"], ["anonkey"]],
|
|
646
|
+
// The comment lines are bibtex's entries, not the reader's: the post-build check names them.
|
|
647
|
+
["v13-percent-text", ["refs.bib"], ["ok1"]],
|
|
648
|
+
])("%s: checks %j, keys %j", async (paper, sources, keys) => {
|
|
649
|
+
const files = planted(paper);
|
|
650
|
+
const f = passing();
|
|
651
|
+
await build(files, f.check);
|
|
652
|
+
expect({
|
|
653
|
+
sources: readReferences(files, PAPER)?.bib.sources,
|
|
654
|
+
keys: f.handed,
|
|
655
|
+
}).toEqual({ sources, keys });
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it("a database TeX wrote that is on disk (the build's leftover) is checked as bibtex read it", async () => {
|
|
659
|
+
const files = planted("v8-jobname");
|
|
660
|
+
const f = passing();
|
|
661
|
+
await build(files, f.check);
|
|
662
|
+
expect(f.handed).toEqual(["jkey"]);
|
|
663
|
+
expect(readReferences(files, PAPER)?.bib.sources).toEqual(["paper.bib"]);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it("two entries sharing a key across databases each get a verdict, in order", async () => {
|
|
667
|
+
const copy = (title: string) =>
|
|
668
|
+
`@misc{k,\n title = {${title}},\n url = {https://x.org}\n}\n`;
|
|
669
|
+
const files = built({
|
|
670
|
+
databases: { "a.bib": copy("Good"), "b.bib": `\n\n${copy("Fake")}` },
|
|
671
|
+
keys: ["k"],
|
|
410
672
|
});
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
673
|
+
await referencesStep.run({
|
|
674
|
+
...ctx(files),
|
|
675
|
+
checkReferences: (bib, cache) =>
|
|
676
|
+
Promise.resolve({
|
|
677
|
+
check: {
|
|
678
|
+
kind: "checked",
|
|
679
|
+
entries: bib.map((e) => ({
|
|
680
|
+
key: e.key,
|
|
681
|
+
exists: e.fields["title"] === "Fake" ? "false" : "true",
|
|
682
|
+
authors: "match",
|
|
683
|
+
})),
|
|
684
|
+
},
|
|
685
|
+
cache,
|
|
686
|
+
}),
|
|
414
687
|
});
|
|
415
|
-
const msgs = await lint(files
|
|
416
|
-
expect(msgs.map((m) => [m.ruleId, m.
|
|
417
|
-
["paper/
|
|
688
|
+
const msgs = await lint(files);
|
|
689
|
+
expect(msgs.map((m) => [m.ruleId, m.message.split(": ")[0]])).toEqual([
|
|
690
|
+
["paper/cite-exists", "b.bib:3:1"],
|
|
418
691
|
]);
|
|
419
|
-
expect(msgs[0]?.message).toMatch(/schick2023/);
|
|
420
692
|
});
|
|
693
|
+
});
|
|
421
694
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
695
|
+
describe("when there is nothing bibtex opened, the step says why — and records nothing", () => {
|
|
696
|
+
it.each(["v15-after-end", "v22-parked-thebibliography"])(
|
|
697
|
+
"%s: the build ran no bibtex",
|
|
698
|
+
async (paper) => {
|
|
699
|
+
const files = planted(paper);
|
|
700
|
+
const { out } = await build(files, never);
|
|
701
|
+
expect(out).toEqual({
|
|
702
|
+
ok: true,
|
|
703
|
+
note: "no bibliography database — bibtex opened none, nothing to check",
|
|
704
|
+
});
|
|
705
|
+
expect(readReferences(files, PAPER)).toBeNull();
|
|
706
|
+
},
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
it("a bibtex that ran and opened no database leaves nothing to check either", async () => {
|
|
710
|
+
const files = ordinary();
|
|
711
|
+
patchRecord(files, (r) => ({
|
|
712
|
+
...r,
|
|
713
|
+
bibtex: r.bibtex.ran ? { ...r.bibtex, databases: [] } : r.bibtex,
|
|
714
|
+
}));
|
|
715
|
+
const { out } = await build(files, never);
|
|
716
|
+
expect(out).toEqual({
|
|
717
|
+
ok: true,
|
|
718
|
+
note: "no bibliography database — bibtex opened none, nothing to check",
|
|
719
|
+
});
|
|
720
|
+
expect(await lint(files)).toEqual([]);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it("🔴 a database TeX wrote that a clean removed after the build makes the record stale: the step says so", async () => {
|
|
724
|
+
const files = planted("v11-two-blocks");
|
|
725
|
+
files.map.delete(`${PAPER}/refs.bib`);
|
|
726
|
+
const { out } = await build(files, never);
|
|
727
|
+
expect(out).toEqual({
|
|
728
|
+
ok: true,
|
|
729
|
+
note: "references NOT checked — the paper changed since the build recorded it (refs.bib deleted); lint will say so",
|
|
730
|
+
});
|
|
731
|
+
expect(readReferences(files, PAPER)).toBeNull();
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
it("🔴 a database bibtex found outside the paper directory cannot be read: the step says which, and lint warns", async () => {
|
|
735
|
+
const files = planted("v11-two-blocks");
|
|
736
|
+
files.map.delete(`${PAPER}/refs.bib`);
|
|
737
|
+
patchRecord(files, (r) => ({
|
|
738
|
+
...r,
|
|
739
|
+
sha256: { ...r.sha256, "refs.bib": null },
|
|
740
|
+
}));
|
|
741
|
+
const { out } = await build(files, never);
|
|
742
|
+
expect(out).toEqual({
|
|
743
|
+
ok: true,
|
|
744
|
+
note: "references NOT checked — refs.bib, which bibtex opened, is not in the paper directory; lint will say so",
|
|
745
|
+
});
|
|
746
|
+
expect(readReferences(files, PAPER)).toBeNull();
|
|
747
|
+
const msgs = await lint(files);
|
|
748
|
+
expect(msgs.map((m) => [m.ruleId, m.severity])).toEqual([
|
|
749
|
+
["paper/refs-checked", 1],
|
|
750
|
+
]);
|
|
751
|
+
expect(msgs[0]?.message).toMatch(/refs\.bib.*not in the paper directory/);
|
|
436
752
|
});
|
|
437
753
|
});
|
|
438
754
|
|
|
439
|
-
describe("
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
755
|
+
describe("what bibtex typeset, against what paperlint read (the post-build check)", () => {
|
|
756
|
+
it("🔴 entries bibtex read that the reader does not see (behind `%`, inside `@comment`) → one refs-checked finding names them", async () => {
|
|
757
|
+
const files = planted("v13-percent-text");
|
|
758
|
+
await build(files, passing().check);
|
|
759
|
+
const msgs = await lint(files);
|
|
760
|
+
expect(msgs.map((m) => [m.ruleId, m.severity])).toEqual([
|
|
761
|
+
["paper/refs-checked", 1],
|
|
762
|
+
]);
|
|
763
|
+
expect(msgs[0]?.message).toMatch(/`pt1`, `k2inComment`/);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it("an entry the reader read and bibtex typeset is not named", async () => {
|
|
767
|
+
const files = planted("v1-stale");
|
|
768
|
+
await build(files, passing().check);
|
|
769
|
+
expect(await lint(files)).toEqual([]);
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it("an entry the reader read and bibtex did not typeset (not cited) is not named", async () => {
|
|
773
|
+
const files = ordinary();
|
|
774
|
+
patchRecord(files, (r) => ({
|
|
775
|
+
...r,
|
|
776
|
+
bibtex: r.bibtex.ran ? { ...r.bibtex, keys: ["schick2023"] } : r.bibtex,
|
|
777
|
+
}));
|
|
778
|
+
await build(files, passing().check);
|
|
779
|
+
expect(await lint(files)).toEqual([]);
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
it("🔴 bibtex's keys are case-insensitive: `\\cite{SMITH}` typesets `\\bibitem{SMITH}` from an entry `smith`", async () => {
|
|
783
|
+
// Measured: pdflatex and bibtex on `\cite{SMITH}` with `@misc{smith,…}` write `\bibitem{SMITH}`.
|
|
784
|
+
const files = built({
|
|
785
|
+
databases: { "refs.bib": "@misc{smith, url = {https://x.org}}\n" },
|
|
786
|
+
keys: ["SMITH"],
|
|
458
787
|
});
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
788
|
+
await build(files, passing().check);
|
|
789
|
+
expect(await lint(files)).toEqual([]);
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it("unseenKeys: the typeset keys no entry read has, in the order bibtex typeset them, each once", () => {
|
|
793
|
+
const files = built({
|
|
794
|
+
databases: { "refs.bib": "@misc{a, url = {https://x.org}}\n" },
|
|
795
|
+
keys: ["a", "x", "A", "y", "x"],
|
|
796
|
+
});
|
|
797
|
+
expect(unseenKeys(readIn(files))).toEqual(["x", "y"]);
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
describe("reading what bibtex left", () => {
|
|
802
|
+
it("bibtexRead: the databases the .blg names, the keys the .bbl typesets — natbib's labels too", () => {
|
|
803
|
+
expect(
|
|
804
|
+
bibtexRead(
|
|
805
|
+
"This is BibTeX\nDatabase file #1: refs.bib\nDatabase file #2: /lib/shared.bib\n",
|
|
806
|
+
"\\bibitem{plain}\n\\bibitem[{Smith et~al.(2020)}]{smith}\n\\bibitem[Doe(2019)]{doe}\n",
|
|
807
|
+
),
|
|
808
|
+
).toEqual({
|
|
809
|
+
databases: ["refs.bib", "/lib/shared.bib"],
|
|
810
|
+
bibitems: ["plain", "smith", "doe"],
|
|
468
811
|
});
|
|
469
812
|
});
|
|
470
813
|
});
|