paperlint 8.0.0 → 9.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/CLAUDE.md +22 -0
- package/.github/CLAUDE.md.spec.ts +20 -0
- package/CLAUDE.md +59 -531
- package/CONTRIBUTING.md +7 -6
- package/README.md +4 -3
- package/dist/CLAUDE.md.spec.d.ts +3 -0
- package/dist/CLAUDE.md.spec.d.ts.map +1 -0
- package/dist/CLAUDE.md.spec.js +62 -0
- package/dist/CLAUDE.md.spec.js.map +1 -0
- 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 -25
- 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/CLAUDE.md.spec.d.ts +3 -0
- package/dist/eslint-rules/CLAUDE.md.spec.d.ts.map +1 -0
- package/dist/eslint-rules/CLAUDE.md.spec.js +39 -0
- package/dist/eslint-rules/CLAUDE.md.spec.js.map +1 -0
- package/dist/eslint-rules/install-path-literals.d.ts +1 -1
- package/dist/eslint-rules/install-path-literals.js +4 -4
- package/dist/eslint-rules/install-path-literals.js.map +1 -1
- 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/eslint-rules/pdf-last-page-balance.js +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/agent-cli-version.js +1 -1
- package/dist/lib/agent-cli-version.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-requirements.js +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/toolchain.js +1 -1
- package/docs/configuration.md +12 -4
- package/docs/design/paper-sources.md +197 -0
- package/docs/e2e.md +1 -1
- package/docs/incidents.md +5 -5
- 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/CLAUDE.md +44 -0
- package/eslint-rules/CLAUDE.md.spec.ts +43 -0
- package/eslint-rules/install-path-literals.harness.mjs +1 -1
- package/eslint-rules/install-path-literals.ts +4 -4
- 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-rules/pdf-last-page-balance.ts +1 -1
- package/eslint.config.mjs +28 -3
- 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/CLAUDE.md +34 -0
- package/hooks/CLAUDE.md.spec.ts +30 -0
- package/lib/agent-cli-version.ts +1 -1
- package/package.json +3 -11
- package/plugin/CLAUDE.md +22 -0
- package/plugin/CLAUDE.md.spec.ts +22 -0
- package/scripts/check.test.ts +23 -1
- package/scripts/check.ts +7 -3
- package/scripts/coverage-config.test.ts +21 -11
- package/scripts/layer-legacy-frozen.test.ts +41 -2
- package/scripts/layer-legacy-frozen.ts +31 -2
- package/skills/CLAUDE.md +19 -0
- package/skills/CLAUDE.md.spec.ts +18 -0
- 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/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +21 -102
- 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/CLAUDE.md +23 -1
- package/src/CLAUDE.md.spec.ts +72 -0
- 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 +101 -36
- 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-requirements.ts +1 -1
- package/src/tex-venue-rules.test.ts +36 -8
- package/src/tex-venue-rules.ts +3 -5
- package/src/toolchain.ts +1 -1
- 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
|
@@ -60,16 +60,15 @@
|
|
|
60
60
|
* registry's response, not by a comparison. If the cache kept parsed entries, those readers would
|
|
61
61
|
* stay uncovered while looking covered.
|
|
62
62
|
*/
|
|
63
|
-
import {
|
|
64
|
-
readFileSync,
|
|
65
|
-
writeFileSync,
|
|
66
|
-
existsSync,
|
|
67
|
-
mkdirSync,
|
|
68
|
-
statSync,
|
|
69
|
-
} from "node:fs";
|
|
63
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
70
64
|
import { createHash } from "node:crypto";
|
|
71
65
|
import { join, resolve, dirname, basename } from "node:path";
|
|
72
|
-
import {
|
|
66
|
+
import { latexReader } from "#src/adapters/latex/index";
|
|
67
|
+
import { nodeFiles } from "#src/adapters/node/index";
|
|
68
|
+
import { sourcesCodec } from "#src/adapters/sources-record/index";
|
|
69
|
+
import { absolutePath } from "#src/domain/paths";
|
|
70
|
+
import { bibReader } from "#src/adapters/bibtex/index";
|
|
71
|
+
import { bibliographyAt, bibliographyUnreadWhy } from "#src/paper-sources";
|
|
73
72
|
import { isMain } from "./consumer.mjs";
|
|
74
73
|
|
|
75
74
|
const ROOT = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
@@ -111,47 +110,15 @@ const joinName = (a) =>
|
|
|
111
110
|
.trim();
|
|
112
111
|
|
|
113
112
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* call that only whoever extracts bibliography facts needs. The import here is dynamic anyway, so
|
|
118
|
-
* the laziness was already there; the manifest merely stopped lying about it being required.
|
|
119
|
-
*
|
|
120
|
-
* ⚠️ A silent skip here would be the worst of the options: a missing checker and a passing one look
|
|
121
|
-
* the same, and "the bibliography was not checked" reads as "the bibliography is fine". That is why
|
|
122
|
-
* the message names the command, not the fact.
|
|
113
|
+
* The facts' entries of one database, as the bibtex reader read them (src/ports/bib-reader.ts): each
|
|
114
|
+
* with its line in the file that holds it, so a finding points where the author edits (the block's
|
|
115
|
+
* lines in paper.tex, or the `.bib`'s).
|
|
123
116
|
*/
|
|
124
|
-
export
|
|
125
|
-
text,
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
({ parse } = await load());
|
|
131
|
-
} catch (e) {
|
|
132
|
-
if (e?.code !== "ERR_MODULE_NOT_FOUND") throw e;
|
|
133
|
-
throw new Error(
|
|
134
|
-
"parsing .bib requires @retorquere/bibtex-parser — it is declared OPTIONAL because it " +
|
|
135
|
-
"weighs 15 MB and is needed only for bibliography facts.\n" +
|
|
136
|
-
" Install: npm i -D @retorquere/bibtex-parser\n" +
|
|
137
|
-
" Why not our own regex: measured 26.08 — the regex gave 0 entries on both real " +
|
|
138
|
-
"files, four libraries gave the correct 27 and 51.",
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
// `sentenceCase: false` — the title is needed as written. Our normalisation folds the case
|
|
142
|
-
// anyway, but a fact has to be a fact: a paraphrased title cannot be shown to a human.
|
|
143
|
-
const res = parse(text, { sentenceCase: false, verbatimFields: [] });
|
|
144
|
-
// The entry's line in the file — found by its key. The parser gives no positions, and a finding
|
|
145
|
-
// needs an address; a key in `.bib` is unique by definition of the format, so the search is
|
|
146
|
-
// unambiguous.
|
|
147
|
-
const lines = text.split("\n");
|
|
148
|
-
const lineOfKey = (key) => {
|
|
149
|
-
const i = lines.findIndex((l) => l.includes(`{${key},`));
|
|
150
|
-
return i === -1 ? 0 : i + 1;
|
|
151
|
-
};
|
|
152
|
-
return res.entries.map((e, i) => {
|
|
153
|
-
const f = e.fields ?? {};
|
|
154
|
-
const names = Array.isArray(f.author) ? f.author : [];
|
|
117
|
+
export function factEntries(bib) {
|
|
118
|
+
const lineOf = (at) => bib.text.slice(0, at).split("\n").length;
|
|
119
|
+
return bib.entries.map((e, i) => {
|
|
120
|
+
const f = e.fields;
|
|
121
|
+
const names = e.names.author ?? [];
|
|
155
122
|
// `and others` is BibTeX's `et al.`. The parser returns it as an author with no first name.
|
|
156
123
|
const isOthers = (a) => !a.firstName && /^others$/i.test(a.lastName ?? "");
|
|
157
124
|
const truncated = names.some(isOthers);
|
|
@@ -159,29 +126,20 @@ export async function parseBib(
|
|
|
159
126
|
.filter((a) => !isOthers(a))
|
|
160
127
|
.map(joinName)
|
|
161
128
|
.filter(Boolean);
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
// printed rather than dropped.
|
|
165
|
-
const str = (v) =>
|
|
166
|
-
Array.isArray(v)
|
|
167
|
-
? v.join(" ")
|
|
168
|
-
: typeof v === "string"
|
|
169
|
-
? v
|
|
170
|
-
: v == null
|
|
171
|
-
? ""
|
|
172
|
-
: String(v);
|
|
129
|
+
// Every field the reader gives is text; a field the entry lacks is "".
|
|
130
|
+
const str = (v) => v ?? "";
|
|
173
131
|
const venueText = [f.booktitle, f.journal, f.note, f.howpublished]
|
|
174
132
|
.map(str)
|
|
175
133
|
.filter(Boolean)
|
|
176
134
|
.join(" ");
|
|
177
135
|
return {
|
|
178
136
|
n: i + 1,
|
|
179
|
-
line:
|
|
137
|
+
line: lineOf(e.span.start),
|
|
180
138
|
key: e.key,
|
|
181
139
|
// `raw` is where identifiers are looked for: they hide in `note`, in `journal`
|
|
182
140
|
// (`arXiv preprint arXiv:2107.03374`), and in a separate `doi` field.
|
|
183
141
|
raw: [
|
|
184
|
-
|
|
142
|
+
authors.join(" and "),
|
|
185
143
|
str(f.title),
|
|
186
144
|
venueText,
|
|
187
145
|
str(f.doi),
|
|
@@ -398,40 +356,39 @@ export function recordFrom(key, cached) {
|
|
|
398
356
|
// ── assembling the facts ─────────────────────────────────────────────────────
|
|
399
357
|
|
|
400
358
|
/**
|
|
401
|
-
*
|
|
402
|
-
* `
|
|
403
|
-
* (
|
|
404
|
-
*
|
|
359
|
+
* WHICH BIBLIOGRAPHY: the one the last build's bibtex opened, as `paperlint build` recorded it
|
|
360
|
+
* (`_build/sources.json`, docs/design/paper-sources.md §1) and `bibliographyAt` reads it
|
|
361
|
+
* (src/paper-sources.ts) — never an order of file names of our own, and never read out of TeX source.
|
|
362
|
+
*
|
|
363
|
+
* a directory the databases bibtex opened for its paper.tex
|
|
364
|
+
* a .tex the same, for that file's folder
|
|
365
|
+
* a .bib that file, as named
|
|
366
|
+
*
|
|
367
|
+
* A paper with no current record is refused: run `npx paperlint build` first. A database TeX wrote from
|
|
368
|
+
* a `filecontents` block is read from the block, where the author edits it.
|
|
405
369
|
*/
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
export function resolveSource(target) {
|
|
413
|
-
const t = resolve(target);
|
|
414
|
-
if (existsSync(t) && statSync(t).isFile()) return t;
|
|
415
|
-
for (const c of SOURCE_ORDER) {
|
|
416
|
-
const p = join(t, c);
|
|
417
|
-
if (existsSync(p) && isSource(p)) return p;
|
|
418
|
-
}
|
|
419
|
-
return null;
|
|
420
|
-
}
|
|
370
|
+
const DEPS = {
|
|
371
|
+
files: nodeFiles,
|
|
372
|
+
codec: sourcesCodec,
|
|
373
|
+
latex: latexReader,
|
|
374
|
+
bib: bibReader,
|
|
375
|
+
};
|
|
421
376
|
|
|
422
377
|
/**
|
|
423
|
-
* The
|
|
424
|
-
*
|
|
378
|
+
* The bibliography of `target`: the texts to read, or why there are none — `bibliographyAt`
|
|
379
|
+
* (src/paper-sources.ts) answers, in its words. `paperDir` is where the cache and the facts go.
|
|
425
380
|
*/
|
|
426
|
-
export
|
|
427
|
-
const
|
|
428
|
-
if (!
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
381
|
+
export function bibliographyFrom(target) {
|
|
382
|
+
const r = bibliographyAt(absolutePath(resolve(target)), DEPS);
|
|
383
|
+
if (!r.ok) return { ok: false, why: bibliographyUnreadWhy(r.error) };
|
|
384
|
+
const { paperDir, texts } = r.value;
|
|
385
|
+
return { ok: true, paperDir, texts };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** The entries of one text TeX reads, each with its file. */
|
|
389
|
+
export function loadEntries(bib) {
|
|
390
|
+
const file = rel(bib.path);
|
|
391
|
+
return factEntries(bib).map((e) => ({ ...e, file }));
|
|
435
392
|
}
|
|
436
393
|
|
|
437
394
|
/** Every registry key a list of entries needs. Computed once so the requests go in one batch. */
|
|
@@ -443,7 +400,11 @@ export function keysFor(entries) {
|
|
|
443
400
|
|
|
444
401
|
const rel = (p) => (p.startsWith(ROOT) ? p.slice(ROOT.length + 1) : p);
|
|
445
402
|
|
|
446
|
-
|
|
403
|
+
/**
|
|
404
|
+
* The facts of a run. `source` is the file the first database was read from (the only one, for a
|
|
405
|
+
* paper with one), `sources` every one; `source_sha256` hashes those files' bytes, in order.
|
|
406
|
+
*/
|
|
407
|
+
export function buildFacts({ texts, entries, cache, cachePath }) {
|
|
447
408
|
const withIds = entries.map((e) => {
|
|
448
409
|
const ids = extractIds(e);
|
|
449
410
|
return { ...e, ids, primary: primaryOf(ids), keys: allKeys(ids) };
|
|
@@ -454,9 +415,12 @@ export function buildFacts({ source, text, entries, cache, cachePath }) {
|
|
|
454
415
|
if (!(k in records)) records[k] = recordFrom(k, cache[k]);
|
|
455
416
|
return {
|
|
456
417
|
schema: SCHEMA,
|
|
457
|
-
source: rel(
|
|
418
|
+
source: rel(texts[0].path),
|
|
419
|
+
sources: [...new Set(texts.map((b) => rel(b.path)))],
|
|
458
420
|
source_kind: "bibtex",
|
|
459
|
-
source_sha256:
|
|
421
|
+
source_sha256: [...new Set(texts.map((b) => b.text))]
|
|
422
|
+
.reduce((h, t) => h.update(t), createHash("sha256"))
|
|
423
|
+
.digest("hex"),
|
|
460
424
|
cache: cachePath ? rel(cachePath) : null,
|
|
461
425
|
generated: new Date().toISOString().slice(0, 10),
|
|
462
426
|
entries: withIds,
|
|
@@ -478,24 +442,12 @@ async function main(argv) {
|
|
|
478
442
|
.slice(1)
|
|
479
443
|
.join("=");
|
|
480
444
|
|
|
481
|
-
const
|
|
482
|
-
if (!
|
|
483
|
-
console.error(
|
|
484
|
-
`🛑 no ${SOURCE_ORDER.join(", ")} under ${resolve(target)} — nowhere to take a bibliography from.`,
|
|
485
|
-
);
|
|
486
|
-
return 1;
|
|
487
|
-
}
|
|
488
|
-
// A paper's references are read from a `.bib`, or the one a `.tex` embeds: any other file
|
|
489
|
-
// parsed as BibTeX would report zero entries for the wrong reason.
|
|
490
|
-
if (!src.endsWith(".bib") && !src.endsWith(".tex")) {
|
|
491
|
-
console.error(
|
|
492
|
-
`🛑 ${rel(src)} is neither a .bib nor a .tex — the bibliography is read from ${SOURCE_ORDER.join(", ")}.`,
|
|
493
|
-
);
|
|
445
|
+
const found = bibliographyFrom(target);
|
|
446
|
+
if (!found.ok) {
|
|
447
|
+
console.error(`🛑 ${found.why}.`);
|
|
494
448
|
return 1;
|
|
495
449
|
}
|
|
496
|
-
const paperDir =
|
|
497
|
-
? dirname(src)
|
|
498
|
-
: resolve(target);
|
|
450
|
+
const { paperDir, texts } = found;
|
|
499
451
|
const cachePath = opt("cache")
|
|
500
452
|
? resolve(opt("cache"))
|
|
501
453
|
: join(paperDir, "repro", "refs-cache.json");
|
|
@@ -503,13 +455,13 @@ async function main(argv) {
|
|
|
503
455
|
? resolve(opt("out"))
|
|
504
456
|
: join(paperDir, "_build", "refs.facts.json");
|
|
505
457
|
|
|
506
|
-
const
|
|
507
|
-
const
|
|
458
|
+
const entries = texts.flatMap(loadEntries);
|
|
459
|
+
const names = [...new Set(texts.map((b) => basename(b.path)))].join(", ");
|
|
508
460
|
if (!entries.length) {
|
|
509
461
|
// 🔴 Zero entries is a suspect, not a success. Facts with an empty list would look like a
|
|
510
462
|
// clean bibliography, so we do not write them at all and exit with a non-zero code.
|
|
511
463
|
console.error(
|
|
512
|
-
`🛑 parsed 0 entries out of ${rel(
|
|
464
|
+
`🛑 parsed 0 entries out of ${[...new Set(texts.map((b) => rel(b.path)))].join(", ")}. Silence here would look like a clean bibliography.`,
|
|
513
465
|
);
|
|
514
466
|
return 1;
|
|
515
467
|
}
|
|
@@ -536,12 +488,12 @@ async function main(argv) {
|
|
|
536
488
|
if (fetched) saveCache(cachePath, cache);
|
|
537
489
|
}
|
|
538
490
|
|
|
539
|
-
const facts = buildFacts({
|
|
491
|
+
const facts = buildFacts({ texts, entries, cache, cachePath });
|
|
540
492
|
mkdirSync(dirname(out), { recursive: true });
|
|
541
493
|
writeFileSync(out, `${JSON.stringify(facts, null, 2)}\n`);
|
|
542
494
|
const resolved = Object.values(facts.records).filter((r) => r.found).length;
|
|
543
495
|
console.log(
|
|
544
|
-
`📚 ${
|
|
496
|
+
`📚 ${names} → ${rel(out)} (${entries.length} entries, ${Object.keys(facts.records).length} identifiers, ` +
|
|
545
497
|
`${resolved} resolved${offline ? ", offline" : fetched ? `, +${fetched} fetched` : ", all from cache"})`,
|
|
546
498
|
);
|
|
547
499
|
return 0;
|
|
@@ -9,17 +9,18 @@ import { dirname, join } from "node:path";
|
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { test } from "vitest";
|
|
11
11
|
import { runNode, useTempDir, writeTree } from "../../../test/support.ts";
|
|
12
|
+
import { plantedOnDisk, recordOnDisk } from "../../../test/recorded-fixture.ts";
|
|
13
|
+
import { bibliographyUnreadWhy } from "#src/paper-sources";
|
|
12
14
|
import {
|
|
15
|
+
bibliographyFrom,
|
|
13
16
|
buildFacts,
|
|
14
17
|
extractIds,
|
|
15
18
|
loadCache,
|
|
16
19
|
loadEntries,
|
|
17
|
-
parseBib,
|
|
18
20
|
primaryOf,
|
|
19
21
|
readArxiv,
|
|
20
22
|
readCrossref,
|
|
21
23
|
recordFrom,
|
|
22
|
-
resolveSource,
|
|
23
24
|
} from "./extract-ref-facts.mjs";
|
|
24
25
|
|
|
25
26
|
const SCRIPT = join(
|
|
@@ -28,10 +29,13 @@ const SCRIPT = join(
|
|
|
28
29
|
);
|
|
29
30
|
const root = useTempDir("extract-ref-facts-");
|
|
30
31
|
|
|
31
|
-
test(".bib:
|
|
32
|
-
const
|
|
33
|
-
|
|
32
|
+
test(".bib: an entry's line is where its `@` stands; a missing title or year is null, not empty", () => {
|
|
33
|
+
const bib = join(root, "lines.bib");
|
|
34
|
+
writeFileSync(
|
|
35
|
+
bib,
|
|
36
|
+
"% a comment\n\n@misc{ spaced ,\n note={see arXiv:2101.00001}\n}\n",
|
|
34
37
|
);
|
|
38
|
+
const [entry] = loadEntries(bibliographyFrom(bib).texts[0]);
|
|
35
39
|
assert.deepEqual(
|
|
36
40
|
{
|
|
37
41
|
line: entry.line,
|
|
@@ -41,7 +45,7 @@ test(".bib: a key the text spells with spaces has no line; a missing title or ye
|
|
|
41
45
|
raw: entry.raw,
|
|
42
46
|
},
|
|
43
47
|
{
|
|
44
|
-
line:
|
|
48
|
+
line: 3,
|
|
45
49
|
title: null,
|
|
46
50
|
year: null,
|
|
47
51
|
authors: [],
|
|
@@ -50,52 +54,6 @@ test(".bib: a key the text spells with spaces has no line; a missing title or ye
|
|
|
50
54
|
);
|
|
51
55
|
});
|
|
52
56
|
|
|
53
|
-
test(".bib from a parser that returns no fields, or list and number fields: joined and printed, not dropped", async () => {
|
|
54
|
-
const load = async () => ({
|
|
55
|
-
parse: () => ({
|
|
56
|
-
entries: [
|
|
57
|
-
{ key: "bare" },
|
|
58
|
-
{
|
|
59
|
-
key: "odd",
|
|
60
|
-
fields: {
|
|
61
|
-
title: ["Split", "Title"],
|
|
62
|
-
year: 2021,
|
|
63
|
-
note: null,
|
|
64
|
-
author: [{ firstName: "Ada", lastName: "Lovelace" }],
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
],
|
|
68
|
-
}),
|
|
69
|
-
});
|
|
70
|
-
const [bare, odd] = await parseBib("@misc{bare,}\n@misc{odd,}\n", { load });
|
|
71
|
-
assert.deepEqual(
|
|
72
|
-
[
|
|
73
|
-
bare,
|
|
74
|
-
{ title: odd.title, year: odd.year, authors: odd.authors, raw: odd.raw },
|
|
75
|
-
],
|
|
76
|
-
[
|
|
77
|
-
{
|
|
78
|
-
n: 1,
|
|
79
|
-
line: 1,
|
|
80
|
-
key: "bare",
|
|
81
|
-
raw: "",
|
|
82
|
-
authors: [],
|
|
83
|
-
truncated: false,
|
|
84
|
-
title: null,
|
|
85
|
-
year: null,
|
|
86
|
-
venue_text: "",
|
|
87
|
-
doi_field: null,
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
title: "Split Title",
|
|
91
|
-
year: "2021",
|
|
92
|
-
authors: ["Ada Lovelace"],
|
|
93
|
-
raw: "Ada Lovelace Split Title 2021",
|
|
94
|
-
},
|
|
95
|
-
],
|
|
96
|
-
);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
57
|
test("identifiers: a declared DOI field comes first; an entry without text fields has none", () => {
|
|
100
58
|
const ids = extractIds({
|
|
101
59
|
doi_field: "10.1111/field",
|
|
@@ -181,19 +139,22 @@ test("cache: no path and a corrupt file both read as empty", () => {
|
|
|
181
139
|
assert.deepEqual([loadCache(undefined), loadCache(corrupt)], [{}, {}]);
|
|
182
140
|
});
|
|
183
141
|
|
|
184
|
-
test("sources: a
|
|
142
|
+
test("sources: a .bib named is read as itself, as BibTeX; facts without a cache say so", async () => {
|
|
185
143
|
const bib = join(root, "one.bib");
|
|
186
144
|
writeFileSync(bib, "@misc{k, title={T}, year={2020}}\n");
|
|
187
|
-
|
|
188
|
-
|
|
145
|
+
const found = bibliographyFrom(bib);
|
|
146
|
+
assert.deepEqual([found.ok, found.texts.map((t) => t.path)], [true, [bib]]);
|
|
147
|
+
const entries = loadEntries(found.texts[0]);
|
|
189
148
|
const facts = buildFacts({
|
|
190
|
-
|
|
191
|
-
text: "",
|
|
149
|
+
texts: found.texts,
|
|
192
150
|
entries,
|
|
193
151
|
cache: {},
|
|
194
152
|
cachePath: null,
|
|
195
153
|
});
|
|
196
|
-
assert.deepEqual(
|
|
154
|
+
assert.deepEqual(
|
|
155
|
+
[facts.source_kind, facts.cache, facts.sources],
|
|
156
|
+
["bibtex", null, [bib]],
|
|
157
|
+
);
|
|
197
158
|
});
|
|
198
159
|
|
|
199
160
|
// ── the command ─────────────────────────────────────────────────────────────
|
|
@@ -205,10 +166,15 @@ const CROSSREF_OK = JSON.stringify({
|
|
|
205
166
|
issued: { "date-parts": [[2020]] },
|
|
206
167
|
},
|
|
207
168
|
});
|
|
169
|
+
/** A paper.tex declaring `\bibliography{refs}`, with `preamble` before its document. */
|
|
170
|
+
const declaring = (preamble = "") =>
|
|
171
|
+
`\\documentclass{article}\n${preamble}\\begin{document}x\\bibliography{refs}\\end{document}\n`;
|
|
208
172
|
writeTree(root, {
|
|
173
|
+
"paper/paper.tex": declaring(),
|
|
209
174
|
"paper/refs.bib":
|
|
210
175
|
"@misc{a, author={A. A}, title={Found}, note={doi:10.1234/ok}}\n" +
|
|
211
176
|
"@misc{b, author={B. B}, title={Lost}, note={doi:10.1234/down}}\n",
|
|
177
|
+
"empty/paper.tex": declaring(),
|
|
212
178
|
"empty/refs.bib": "% nothing cited\n",
|
|
213
179
|
"notes/notes.txt": "A. A. Found. doi:10.1234/ok\n",
|
|
214
180
|
// `fetch` in the child: 10.1234/down fails at the transport, anything else is a CrossRef hit.
|
|
@@ -217,26 +183,42 @@ writeTree(root, {
|
|
|
217
183
|
` if (url.includes("down")) throw new Error("network down");\n` +
|
|
218
184
|
` return { status: 200, text: async () => ${JSON.stringify(CROSSREF_OK)} };\n` +
|
|
219
185
|
`};\n`,
|
|
220
|
-
|
|
221
|
-
"
|
|
222
|
-
"
|
|
223
|
-
|
|
224
|
-
"
|
|
225
|
-
"import { register } from 'node:module';\n" +
|
|
226
|
-
"register('data:text/javascript,' + encodeURIComponent(\"export async function resolve(s, c, next) { if (s === '@retorquere/bibtex-parser') throw Object.assign(new Error('broken install'), { code: 'EACCES' }); return next(s, c); }\"));\n",
|
|
186
|
+
"bib/paper.tex": declaring(),
|
|
187
|
+
"switched/paper.tex":
|
|
188
|
+
"\\documentclass{article}\\newif\\ifanon\\begin{document}x\\ifanon\\bibliography{anon}\\else\\bibliography{refs}\\fi\\end{document}\n",
|
|
189
|
+
"switched/anon.bib": "@misc{anon1, title={A}, note={doi:10.1234/a}}\n",
|
|
190
|
+
"switched/refs.bib": "@misc{refs1, title={R}, note={doi:10.1234/r}}\n",
|
|
227
191
|
"bib/refs.bib": "@misc{k, title={T}, note={doi:10.1234/ok}}\n",
|
|
228
|
-
// The bibliography is embedded in paper.tex; the refs.bib beside it is
|
|
229
|
-
"inline/paper.tex":
|
|
230
|
-
"\\documentclass{article}\n" +
|
|
192
|
+
// The bibliography is embedded in paper.tex; the refs.bib beside it is what TeX wrote from the block.
|
|
193
|
+
"inline/paper.tex": declaring(
|
|
231
194
|
"\\begin{filecontents*}[overwrite]{refs.bib}\n" +
|
|
195
|
+
"@misc{a, author={A. A}, title={Found}, note={doi:10.1234/ok}}\n" +
|
|
196
|
+
"@misc{b, author={B. B}, title={Lost}, note={doi:10.1234/down}}\n" +
|
|
197
|
+
"\\end{filecontents*}\n",
|
|
198
|
+
),
|
|
199
|
+
"inline/refs.bib":
|
|
232
200
|
"@misc{a, author={A. A}, title={Found}, note={doi:10.1234/ok}}\n" +
|
|
233
|
-
"@misc{b, author={B. B}, title={Lost}, note={doi:10.1234/down}}\n"
|
|
234
|
-
|
|
235
|
-
"
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
201
|
+
"@misc{b, author={B. B}, title={Lost}, note={doi:10.1234/down}}\n",
|
|
202
|
+
// A paper that was never built has no record: nothing says which bibliography TeX reads.
|
|
203
|
+
"unbuilt/paper.tex": declaring(),
|
|
204
|
+
"unbuilt/refs.bib": "@misc{k, title={T}, note={doi:10.1234/ok}}\n",
|
|
205
|
+
});
|
|
206
|
+
/** What a build records for a paper whose bibliography is the file `db` the author keeps. */
|
|
207
|
+
const keeps = (db = "refs.bib") => ({
|
|
208
|
+
inputs: [
|
|
209
|
+
["paper.tex", "body"],
|
|
210
|
+
[db, "preamble"],
|
|
211
|
+
],
|
|
212
|
+
databases: [db],
|
|
213
|
+
});
|
|
214
|
+
for (const dir of ["paper", "empty", "bib"])
|
|
215
|
+
recordOnDisk(join(root, dir), keeps());
|
|
216
|
+
// bibtex opened refs.bib only: the switch took its `\else`.
|
|
217
|
+
recordOnDisk(join(root, "switched"), keeps());
|
|
218
|
+
recordOnDisk(join(root, "inline"), {
|
|
219
|
+
inputs: [["paper.tex", "body"]],
|
|
220
|
+
written: ["refs.bib"],
|
|
221
|
+
databases: ["refs.bib"],
|
|
240
222
|
});
|
|
241
223
|
const cli = (args, { cwd = root, nodeArgs = [] } = {}) =>
|
|
242
224
|
runNode(SCRIPT, args, {
|
|
@@ -299,11 +281,14 @@ test("no target: the current directory", () => {
|
|
|
299
281
|
assert.equal(r.status, 0, r.stderr);
|
|
300
282
|
});
|
|
301
283
|
|
|
302
|
-
test("refusals:
|
|
303
|
-
|
|
284
|
+
test("refusals: no bibliography to read (in bibliographyAt's words), and zero entries (not written as a clean bibliography)", () => {
|
|
285
|
+
// The path → bibliography vocabulary is tested once, beside bibliographyAt (src/paper-sources.ts).
|
|
286
|
+
// Here: that the command speaks it, and stops.
|
|
287
|
+
const notes = join(root, "notes/notes.txt");
|
|
288
|
+
assert.deepEqual(cli([notes, "--offline"]), {
|
|
304
289
|
status: 1,
|
|
305
290
|
stdout: "",
|
|
306
|
-
stderr: `🛑
|
|
291
|
+
stderr: `🛑 ${bibliographyUnreadWhy({ kind: "not-bib-or-tex", path: notes })}.\n`,
|
|
307
292
|
});
|
|
308
293
|
assert.deepEqual(cli(["empty", "--offline"]), {
|
|
309
294
|
status: 1,
|
|
@@ -311,33 +296,9 @@ test("refusals: nowhere to read from, a file that is not a .bib, and zero entrie
|
|
|
311
296
|
stderr:
|
|
312
297
|
"🛑 parsed 0 entries out of empty/refs.bib. Silence here would look like a clean bibliography.\n",
|
|
313
298
|
});
|
|
314
|
-
// Guards: a file that is neither a .bib nor a .tex is named, not parsed as BibTeX, and nothing is written.
|
|
315
|
-
assert.deepEqual(cli([join(root, "notes/notes.txt"), "--offline"]), {
|
|
316
|
-
status: 1,
|
|
317
|
-
stdout: "",
|
|
318
|
-
stderr:
|
|
319
|
-
"🛑 notes/notes.txt is neither a .bib nor a .tex — the bibliography is read from paper.tex, refs.bib, build/custom.bib.\n",
|
|
320
|
-
});
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
test(".bib without its optional parser: the error names the install; any other failure is not masked", () => {
|
|
324
|
-
const missing = cli(["bib", "--offline"], {
|
|
325
|
-
nodeArgs: ["--import", join(root, "no-parser.mjs")],
|
|
326
|
-
});
|
|
327
|
-
assert.equal(missing.status, 1);
|
|
328
|
-
assert.match(
|
|
329
|
-
missing.stderr,
|
|
330
|
-
/parsing \.bib requires @retorquere\/bibtex-parser[\s\S]*Install: {2}npm i -D @retorquere\/bibtex-parser/,
|
|
331
|
-
);
|
|
332
|
-
const broken = cli(["bib", "--offline"], {
|
|
333
|
-
nodeArgs: ["--import", join(root, "bad-parser.mjs")],
|
|
334
|
-
});
|
|
335
|
-
assert.equal(broken.status, 1);
|
|
336
|
-
assert.match(broken.stderr, /broken install/);
|
|
337
|
-
assert.doesNotMatch(broken.stderr, /npm i -D/);
|
|
338
299
|
});
|
|
339
300
|
|
|
340
|
-
test("🔴 a paper directory whose paper.tex embeds its .bib: the
|
|
301
|
+
test("🔴 a paper directory whose paper.tex embeds its .bib: the entries are read from the block that wrote it, where the author edits them", () => {
|
|
341
302
|
const r = cli(["inline", "--offline"]);
|
|
342
303
|
// Guards: only materialized .bib files were looked for, so a paper with its bibliography in
|
|
343
304
|
// `filecontents` was refused before a build and read from a stale refs.bib after an edit.
|
|
@@ -352,35 +313,81 @@ test("🔴 a paper directory whose paper.tex embeds its .bib: the embedded one i
|
|
|
352
313
|
);
|
|
353
314
|
// Each entry's line is its line in paper.tex, where the author edits it.
|
|
354
315
|
assert.deepEqual(
|
|
355
|
-
facts.entries.map((e) => [e.key, e.line]),
|
|
316
|
+
facts.entries.map((e) => [e.file, e.key, e.line]),
|
|
356
317
|
[
|
|
357
|
-
["a", 3],
|
|
358
|
-
["b", 4],
|
|
318
|
+
["inline/paper.tex", "a", 3],
|
|
319
|
+
["inline/paper.tex", "b", 4],
|
|
359
320
|
],
|
|
360
321
|
);
|
|
361
322
|
});
|
|
362
323
|
|
|
363
|
-
test("a
|
|
364
|
-
assert.deepEqual(
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
324
|
+
test("a bibliography behind a switch: the database bibtex opened is read, not every candidate", () => {
|
|
325
|
+
assert.deepEqual(cli(["switched", "--offline"]), {
|
|
326
|
+
status: 0,
|
|
327
|
+
stdout:
|
|
328
|
+
"📚 refs.bib → switched/_build/refs.facts.json (1 entries, 1 identifiers, 0 resolved, offline)\n",
|
|
329
|
+
stderr: "",
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("🔴 a paper that was not built is refused — run `npx paperlint build` first — never read from its source", () => {
|
|
334
|
+
assert.deepEqual(cli(["unbuilt", "--offline"]), {
|
|
369
335
|
status: 1,
|
|
370
336
|
stdout: "",
|
|
371
|
-
stderr:
|
|
372
|
-
"🛑 parsed 0 entries out of plain/paper.tex. Silence here would look like a clean bibliography.\n",
|
|
337
|
+
stderr: `🛑 ${join(root, "unbuilt")} has not been built — run \`npx paperlint build\` first, which records the databases bibtex reads.\n`,
|
|
373
338
|
});
|
|
374
339
|
});
|
|
375
340
|
|
|
376
|
-
test("an embedded entry
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
tex
|
|
380
|
-
|
|
341
|
+
test("an embedded entry's line is its line in paper.tex", () => {
|
|
342
|
+
const block = "@misc{ spaced ,\n note={arXiv:2101.00001}\n}\n";
|
|
343
|
+
writeTree(root, {
|
|
344
|
+
"spaced/paper.tex": declaring(
|
|
345
|
+
`\\begin{filecontents*}{refs.bib}\n${block}\\end{filecontents*}\n`,
|
|
346
|
+
),
|
|
347
|
+
"spaced/refs.bib": block,
|
|
348
|
+
});
|
|
349
|
+
recordOnDisk(join(root, "spaced"), {
|
|
350
|
+
inputs: [["paper.tex", "body"]],
|
|
351
|
+
written: ["refs.bib"],
|
|
352
|
+
databases: ["refs.bib"],
|
|
353
|
+
});
|
|
354
|
+
const found = bibliographyFrom(join(root, "spaced", "paper.tex"));
|
|
355
|
+
assert.deepEqual(
|
|
356
|
+
loadEntries(found.texts[0]).map((e) => e.line),
|
|
357
|
+
[3],
|
|
381
358
|
);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// ── the planted papers of fixtures/paper-sources, laid out as a build leaves them ─────
|
|
362
|
+
|
|
363
|
+
test.each([
|
|
364
|
+
// [paper, the file read, the keys read] — tex-truth.json beside each paper is TeX's own answer: the
|
|
365
|
+
// databases bibtex opened. A `.bib` TeX wrote from a block is read from the block (paper.tex).
|
|
366
|
+
["v1-stale", "refs.bib", ["stale2020"]],
|
|
367
|
+
["v2-overwrite", "paper.tex", ["inline2024"]],
|
|
368
|
+
["v3-declared", "paper.bib", ["declared2023"]],
|
|
369
|
+
["v4-commented", "refs.bib", ["stale2020"]],
|
|
370
|
+
// What bibtex reads beyond these — a malformed entry, or one behind `%` or inside `@comment{…}` —
|
|
371
|
+
// the build's post-build check names (src/references.ts).
|
|
372
|
+
["v5-percent-entry", "paper.tex", ["inline2024"]],
|
|
373
|
+
["v6-unclosed", "refs.bib", ["a1", "a3"]],
|
|
374
|
+
["v13-percent-text", "refs.bib", ["ok1"]],
|
|
375
|
+
["v8-jobname", "paper.tex", ["jkey"]],
|
|
376
|
+
])("%s: the bibliography bibtex opened", (paper, file, keys) => {
|
|
377
|
+
const dir = join(root, "planted", paper);
|
|
378
|
+
plantedOnDisk(dir, paper);
|
|
379
|
+
const out = join(root, "planted", `${paper}.json`);
|
|
380
|
+
const r = cli([
|
|
381
|
+
dir,
|
|
382
|
+
"--offline",
|
|
383
|
+
`--out=${out}`,
|
|
384
|
+
`--cache=${join(root, "planted", "cache.json")}`,
|
|
385
|
+
]);
|
|
386
|
+
assert.deepEqual([r.status, r.stderr], [0, ""]);
|
|
387
|
+
assert.match(r.stdout, new RegExp(`^📚 ${file} → `));
|
|
388
|
+
const facts = JSON.parse(readFileSync(out, "utf8"));
|
|
382
389
|
assert.deepEqual(
|
|
383
|
-
|
|
384
|
-
|
|
390
|
+
facts.entries.map((e) => e.key),
|
|
391
|
+
keys,
|
|
385
392
|
);
|
|
386
393
|
});
|