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
|
@@ -697,3 +697,76 @@ describe("bodyEmphasis — each style, and edge cases", () => {
|
|
|
697
697
|
expect(e && src.slice(e.span.start, e.span.end)).toBe("\\textbf{see it}");
|
|
698
698
|
});
|
|
699
699
|
});
|
|
700
|
+
|
|
701
|
+
describe("latexReader.filecontents — the blocks that write a file, where they stand", () => {
|
|
702
|
+
const SRC =
|
|
703
|
+
"\\documentclass{x}\n" +
|
|
704
|
+
"\\begin{filecontents*}[overwrite]{refs.bib}\n@misc{a, title={A}}\n\\end{filecontents*}\n" +
|
|
705
|
+
"\\begin{document}\n" +
|
|
706
|
+
"\\begin{filecontents}{\\jobname.bib}\n@misc{b, title={B}}\n\\end{filecontents}\n" +
|
|
707
|
+
"\\end{document}\n" +
|
|
708
|
+
"\\begin{filecontents}{late.bib}\n@misc{c}\n\\end{filecontents}\n";
|
|
709
|
+
|
|
710
|
+
it("each live block, in order: the file it writes (\\jobname expanded), [overwrite], its span and body", () => {
|
|
711
|
+
const blocks = latexReader.filecontents(SRC, "paper");
|
|
712
|
+
expect(
|
|
713
|
+
blocks.map((b) => [
|
|
714
|
+
b.writes,
|
|
715
|
+
b.overwrite,
|
|
716
|
+
SRC.slice(b.body.start, b.body.end),
|
|
717
|
+
]),
|
|
718
|
+
).toEqual([
|
|
719
|
+
["refs.bib", true, "@misc{a, title={A}}\n"],
|
|
720
|
+
["paper.bib", false, "@misc{b, title={B}}\n"],
|
|
721
|
+
]);
|
|
722
|
+
expect(SRC.slice(blocks[0]?.span.start, blocks[0]?.span.end)).toMatch(
|
|
723
|
+
/^\\begin\{filecontents\*\}.*\\end\{filecontents\*\}$/s,
|
|
724
|
+
);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it("a block inside a comment, or after \\end{document}, is not one TeX runs", () => {
|
|
728
|
+
expect(
|
|
729
|
+
latexReader.filecontents(
|
|
730
|
+
"% \\begin{filecontents}{x.bib}\n% @misc{z}\n% \\end{filecontents}\n\\begin{document}\n\\end{document}\n",
|
|
731
|
+
"paper",
|
|
732
|
+
),
|
|
733
|
+
).toEqual([]);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it("a block that names no file writes none: a group after its body's first text is no name, and a body may hold none", () => {
|
|
737
|
+
expect(
|
|
738
|
+
[
|
|
739
|
+
"\\begin{filecontents}\n@misc{z}\n\\end{filecontents}\n",
|
|
740
|
+
"\\begin{filecontents}\nplain text\n\\end{filecontents}\n",
|
|
741
|
+
].flatMap((block) =>
|
|
742
|
+
latexReader.filecontents(
|
|
743
|
+
`${block}\\begin{document}\n\\end{document}\n`,
|
|
744
|
+
"paper",
|
|
745
|
+
),
|
|
746
|
+
),
|
|
747
|
+
).toEqual([]);
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
describe("latexReader.filecontents — a file name on a line of its own", () => {
|
|
752
|
+
it("🔴 a name on the line after \\begin, the option before it or not: TeX takes it, and the body starts on the line after the name", () => {
|
|
753
|
+
// Measured with pdflatex: `\\begin{filecontents*}` then `{refs.bib}` on the next line writes
|
|
754
|
+
// refs.bib from the lines after the name (fixtures/paper-sources/v30-name-next-line).
|
|
755
|
+
const src =
|
|
756
|
+
"\\begin{filecontents*}\n{refs.bib}\n@misc{a, title={A}}\n\\end{filecontents*}\n" +
|
|
757
|
+
"\\begin{filecontents}[overwrite]\n {two.bib}\n@misc{b}\n\\end{filecontents}\n" +
|
|
758
|
+
"\\begin{document}\n\\end{document}\n";
|
|
759
|
+
expect(
|
|
760
|
+
latexReader
|
|
761
|
+
.filecontents(src, "paper")
|
|
762
|
+
.map((b) => [
|
|
763
|
+
b.writes,
|
|
764
|
+
b.overwrite,
|
|
765
|
+
src.slice(b.body.start, b.body.end),
|
|
766
|
+
]),
|
|
767
|
+
).toEqual([
|
|
768
|
+
["refs.bib", false, "@misc{a, title={A}}\n"],
|
|
769
|
+
["two.bib", true, "@misc{b}\n"],
|
|
770
|
+
]);
|
|
771
|
+
});
|
|
772
|
+
});
|
|
@@ -33,6 +33,7 @@ import { parseLatex } from "./parse.ts";
|
|
|
33
33
|
import { bodyEmphasis, bodyProse } from "./prose.ts";
|
|
34
34
|
import { renderedRuns } from "./rendered.ts";
|
|
35
35
|
import { layoutOverridesOf } from "./layout.ts";
|
|
36
|
+
import { filecontentsOf } from "./filecontents.ts";
|
|
36
37
|
|
|
37
38
|
export {
|
|
38
39
|
documentClassOf,
|
|
@@ -69,4 +70,5 @@ export const latexReader: LatexReader = {
|
|
|
69
70
|
template: parseTemplate,
|
|
70
71
|
withDocumentClass: (src, want) => replaceDocumentClass(parseLatex(src), want),
|
|
71
72
|
layoutOverrides: (src) => layoutOverridesOf(parseLatex(src)),
|
|
73
|
+
filecontents: (src, jobname) => filecontentsOf(parseLatex(src), jobname),
|
|
72
74
|
};
|
|
@@ -156,3 +156,67 @@ export const isArgument = (v: Visited): v is Argument =>
|
|
|
156
156
|
/** A node, as opposed to a node list or an argument (which visit also reports). */
|
|
157
157
|
export const isNode = (v: Visited): v is Node =>
|
|
158
158
|
!isList(v) && v.type !== "argument" && v.type !== "root";
|
|
159
|
+
|
|
160
|
+
/** A piece of an argument as TeX reads it: text, or a macro this reader leaves as written (`\x`). */
|
|
161
|
+
export interface ArgumentPiece {
|
|
162
|
+
readonly text: string;
|
|
163
|
+
readonly macro: boolean;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* An argument's nodes as TeX reads them, off the tree: a comment is no part of it, a group's braces
|
|
168
|
+
* are none, and white space is one space. `\jobname` is the main file's name when `jobname` is given;
|
|
169
|
+
* every other macro — `\jobnamebib` included, a control sequence of its own — is a piece of its own,
|
|
170
|
+
* written as it stands, and so is anything else (math, verbatim) a name cannot hold.
|
|
171
|
+
*/
|
|
172
|
+
export function argumentPieces(
|
|
173
|
+
nodes: readonly Node[],
|
|
174
|
+
jobname: string | null,
|
|
175
|
+
): readonly ArgumentPiece[] {
|
|
176
|
+
return nodes.flatMap((n) => pieceOf(n, jobname));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The nodes a name cannot hold as text: an environment, math, verbatim. */
|
|
180
|
+
type Nameless = Extract<
|
|
181
|
+
Node,
|
|
182
|
+
{
|
|
183
|
+
readonly type:
|
|
184
|
+
| "root"
|
|
185
|
+
| "environment"
|
|
186
|
+
| "mathenv"
|
|
187
|
+
| "inlinemath"
|
|
188
|
+
| "displaymath"
|
|
189
|
+
| "verb"
|
|
190
|
+
| "verbatim";
|
|
191
|
+
}
|
|
192
|
+
>;
|
|
193
|
+
const NAMELESS: ReadonlySet<string> = new Set([
|
|
194
|
+
"root",
|
|
195
|
+
"environment",
|
|
196
|
+
"mathenv",
|
|
197
|
+
"inlinemath",
|
|
198
|
+
"displaymath",
|
|
199
|
+
"verb",
|
|
200
|
+
"verbatim",
|
|
201
|
+
]);
|
|
202
|
+
const isNameless = (n: Node): n is Nameless => NAMELESS.has(n.type);
|
|
203
|
+
|
|
204
|
+
/** One node of an argument as `argumentPieces` reads it. */
|
|
205
|
+
function pieceOf(n: Node, jobname: string | null): readonly ArgumentPiece[] {
|
|
206
|
+
if (isNameless(n)) return [{ text: `\\${n.type}`, macro: true }];
|
|
207
|
+
switch (n.type) {
|
|
208
|
+
case "comment":
|
|
209
|
+
return [];
|
|
210
|
+
case "string":
|
|
211
|
+
return [{ text: n.content, macro: false }];
|
|
212
|
+
case "whitespace":
|
|
213
|
+
case "parbreak":
|
|
214
|
+
return [{ text: " ", macro: false }];
|
|
215
|
+
case "group":
|
|
216
|
+
return argumentPieces(n.content, jobname);
|
|
217
|
+
case "macro":
|
|
218
|
+
return n.content === "jobname" && jobname !== null
|
|
219
|
+
? [{ text: jobname, macro: false }]
|
|
220
|
+
: [{ text: `\\${n.content}`, macro: true }];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `parseLatex` keeps the trees of a bounded amount of source text: an editor session must not hold a
|
|
3
|
+
* tree for every version of the paper it has seen.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { parseLatex } from "./index.ts";
|
|
7
|
+
|
|
8
|
+
describe("parseLatex's memo", () => {
|
|
9
|
+
/** A distinct source of `kib` KiB that parses quickly: one long comment. */
|
|
10
|
+
const source = (tag: string, kib: number) =>
|
|
11
|
+
`${tag}\n%${"x".repeat(kib * 1024)}`;
|
|
12
|
+
|
|
13
|
+
it("returns the same tree for the same text while it is kept", () => {
|
|
14
|
+
const s = source("same", 1);
|
|
15
|
+
expect(parseLatex(s)).toBe(parseLatex(s));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("keeps up to 512 KiB of source: a paper's ten files stay, read in a cycle", () => {
|
|
19
|
+
const files = Array.from({ length: 11 }, (_, i) =>
|
|
20
|
+
source(`f${String(i)}`, 30),
|
|
21
|
+
);
|
|
22
|
+
const first = files.map((f) => parseLatex(f));
|
|
23
|
+
expect(files.map((f) => parseLatex(f))).toEqual(first);
|
|
24
|
+
expect(files.every((f, i) => parseLatex(f) === first[i])).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("past 512 KiB the text used least recently goes first, a recently used one stays", () => {
|
|
28
|
+
const big = Array.from({ length: 6 }, (_, i) =>
|
|
29
|
+
source(`b${String(i)}`, 100),
|
|
30
|
+
);
|
|
31
|
+
const trees = big.slice(0, 5).map((f) => parseLatex(f));
|
|
32
|
+
// Touch the oldest: it becomes the most recent, so the second is the one evicted.
|
|
33
|
+
expect(parseLatex(big[0] ?? "")).toBe(trees[0]);
|
|
34
|
+
parseLatex(big[5] ?? "");
|
|
35
|
+
expect(parseLatex(big[0] ?? "")).toBe(trees[0]);
|
|
36
|
+
expect(parseLatex(big[1] ?? "")).not.toBe(trees[1]);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -59,10 +59,13 @@ export const KEY_SIGNATURES: Readonly<
|
|
|
59
59
|
"subfile",
|
|
60
60
|
"bibliography",
|
|
61
61
|
"bibliographystyle",
|
|
62
|
-
"addbibresource",
|
|
63
62
|
],
|
|
64
63
|
"m",
|
|
65
64
|
),
|
|
65
|
+
// biblatex's resources take options (`[location=remote]`) before the name.
|
|
66
|
+
...signed(["addbibresource", "addglobalbib", "addsectionbib"], "o m"),
|
|
67
|
+
// bibentry's: writes `\bibdata` as `\bibliography` does, and typesets nothing.
|
|
68
|
+
...signed(["nobibliography"], "m"),
|
|
66
69
|
...signed(["includegraphics"], "s o o m"),
|
|
67
70
|
...signed(["usepackage", "RequirePackage", "documentclass"], "o m o"),
|
|
68
71
|
]);
|
|
@@ -134,9 +137,67 @@ const parser = once(() =>
|
|
|
134
137
|
}),
|
|
135
138
|
);
|
|
136
139
|
|
|
140
|
+
/**
|
|
141
|
+
* The trees parsed last, by their source: a tree is a pure function of its text, and one lint of a
|
|
142
|
+
* paper asks for the same texts many times — every rule that reads the paper (`readPaper`)
|
|
143
|
+
* parses `paper.tex`, its includes and the assembled whole again. Measured 2026-10-06 on the
|
|
144
|
+
* accepted ACM paper of the corpus: 0.9 s per reading of the paper's sources, seven of them per lint,
|
|
145
|
+
* 50–76 ms with the memo.
|
|
146
|
+
*
|
|
147
|
+
* Bounded by the SOURCE TEXT it holds (UTF-16 units, about bytes for TeX), least recently used out.
|
|
148
|
+
* A count bound is wrong both ways: 64 trees held 64 versions of one file in an editor (186 MiB for a
|
|
149
|
+
* 48 KiB paper), and 8 trees thrashed on a paper with ten includes — the rules read them in a cycle,
|
|
150
|
+
* so every read missed and the corpus lint timed out. `MEMO_BYTES` holds the largest paper of the corpus
|
|
151
|
+
* (150 KB of sources, and the assembled whole) and about ten versions of a 48 KiB file.
|
|
152
|
+
*/
|
|
153
|
+
const MEMO_BYTES = 512 * 1024;
|
|
154
|
+
const memo = new Map<string, ParsedTex>();
|
|
155
|
+
|
|
156
|
+
/** The texts to evict, least recent first, so that `adding` more fits under `MEMO_BYTES`. */
|
|
157
|
+
const evicted = (adding: number): readonly string[] => {
|
|
158
|
+
const keys = [...memo.keys()];
|
|
159
|
+
const total = keys.reduce((n, k) => n + k.length, adding);
|
|
160
|
+
const { out } = keys.reduce<{
|
|
161
|
+
readonly left: number;
|
|
162
|
+
readonly out: readonly string[];
|
|
163
|
+
}>(
|
|
164
|
+
(acc, k) =>
|
|
165
|
+
acc.left > MEMO_BYTES
|
|
166
|
+
? { left: acc.left - k.length, out: [...acc.out, k] }
|
|
167
|
+
: acc,
|
|
168
|
+
{ left: total, out: [] },
|
|
169
|
+
);
|
|
170
|
+
return out;
|
|
171
|
+
};
|
|
172
|
+
|
|
137
173
|
/** A LaTeX source → its tree, without the branches `\iffalse … \fi` hides (`conditionals.ts`). */
|
|
138
|
-
export
|
|
139
|
-
src
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
174
|
+
export function parseLatex(src: string): ParsedTex {
|
|
175
|
+
const hit = memo.get(src);
|
|
176
|
+
const parsed: ParsedTex = hit ?? {
|
|
177
|
+
src,
|
|
178
|
+
root: withoutFalseBranches(src, parser().parse(src)),
|
|
179
|
+
[PARSED]: true,
|
|
180
|
+
};
|
|
181
|
+
// Most recent last: a hit moves to the end, and the first keys are the ones used least recently.
|
|
182
|
+
// eslint-disable-next-line functional/immutable-data -- the bounded memo, see above
|
|
183
|
+
memo.delete(src);
|
|
184
|
+
// eslint-disable-next-line functional/immutable-data -- the bounded memo, see above
|
|
185
|
+
evicted(src.length).forEach((k) => memo.delete(k));
|
|
186
|
+
// eslint-disable-next-line functional/immutable-data -- the bounded memo, see above
|
|
187
|
+
memo.set(src, parsed);
|
|
188
|
+
return parsed;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The part of a source's tree TeX reads: everything up to and with the `document` environment.
|
|
193
|
+
* TeX stops at `\end{document}`, so a `\bibliography`, a `thebibliography`, a block or an `\input`
|
|
194
|
+
* parked after it is never read. A source with no `document` environment is read whole.
|
|
195
|
+
*/
|
|
196
|
+
export function liveRoot(t: ParsedTex): Readonly<Ast.Root> {
|
|
197
|
+
const end = t.root.content.findIndex(
|
|
198
|
+
(n) => n.type === "environment" && n.env === "document",
|
|
199
|
+
);
|
|
200
|
+
return end < 0
|
|
201
|
+
? t.root
|
|
202
|
+
: { ...t.root, content: t.root.content.slice(0, end + 1) };
|
|
203
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { absolutePath } from "../../domain/paths.ts";
|
|
6
6
|
import type { Download } from "../../ports/download.ts";
|
|
7
7
|
import type { Files } from "../../ports/files.ts";
|
|
8
|
+
import type { ListDir } from "../../ports/talk-media.ts";
|
|
8
9
|
import type { Command, ProcessExit, RunProcess } from "../../ports/process.ts";
|
|
9
10
|
import type { NotAPromise, Scratch, Workspace } from "../../ports/workspace.ts";
|
|
10
11
|
import { err, ok } from "../../domain/result.ts";
|
|
@@ -55,6 +56,22 @@ export function memoryFiles(
|
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
/** `ListDir` over `memoryFiles`: the names directly under a directory, none when it holds no file. */
|
|
60
|
+
export const memoryListDir =
|
|
61
|
+
(files: MemoryFiles): ListDir =>
|
|
62
|
+
(dir) => [
|
|
63
|
+
...new Set(
|
|
64
|
+
[...files.map.keys()]
|
|
65
|
+
.filter((k) => k.startsWith(`${dir}/`))
|
|
66
|
+
.map((k) =>
|
|
67
|
+
k
|
|
68
|
+
.slice(dir.length + 1)
|
|
69
|
+
.split("/", 1)
|
|
70
|
+
.join(""),
|
|
71
|
+
),
|
|
72
|
+
),
|
|
73
|
+
];
|
|
74
|
+
|
|
58
75
|
export interface MemoryWorkspace extends Workspace {
|
|
59
76
|
/** Every file written into a scratch directory, in order. */
|
|
60
77
|
readonly written: {
|
|
@@ -11,10 +11,16 @@ import {
|
|
|
11
11
|
type LookupCache,
|
|
12
12
|
} from "../../domain/lookup-cache.ts";
|
|
13
13
|
import * as cites from "../../../skills/verify-citations/scripts/verify-cites.mjs";
|
|
14
|
+
import { absolutePath } from "../../domain/paths.ts";
|
|
15
|
+
import { bibReader } from "../bibtex/index.ts";
|
|
16
|
+
|
|
17
|
+
/** A BibTeX text's entries, as the bibtex reader reads them. */
|
|
18
|
+
const read = (bib: string) =>
|
|
19
|
+
bibReader.readFile(absolutePath("/p/refs.bib"), bib).entries;
|
|
14
20
|
|
|
15
21
|
/** The verdicts of a cold run — an empty cache, the adapter the CLI wires. */
|
|
16
22
|
const checkOnly = async (bib: string) =>
|
|
17
|
-
(await onlineReferences(bib, EMPTY_LOOKUP_CACHE)).check;
|
|
23
|
+
(await onlineReferences(read(bib), EMPTY_LOOKUP_CACHE)).check;
|
|
18
24
|
|
|
19
25
|
afterEach(() => {
|
|
20
26
|
vi.unstubAllGlobals();
|
|
@@ -131,23 +137,75 @@ test("each entry gets its existence and its authors: confirmed, fabricated, mism
|
|
|
131
137
|
);
|
|
132
138
|
});
|
|
133
139
|
|
|
134
|
-
test("
|
|
140
|
+
test("one key in two databases bibtex opened: each entry gets its own verdict, in the order given", async () => {
|
|
141
|
+
vi.useFakeTimers();
|
|
142
|
+
fakeFetch({
|
|
143
|
+
"https://api.crossref.org/works/10.1%2Fgood": () =>
|
|
144
|
+
json(200, {
|
|
145
|
+
message: { title: ["Good Paper"], issued: { "date-parts": [[2024]] } },
|
|
146
|
+
}),
|
|
147
|
+
"https://doi.org/api/handles/10.1/good": () =>
|
|
148
|
+
json(200, { responseCode: 1 }),
|
|
149
|
+
"https://doi.org/api/handles/10.1/fake": () => json(404),
|
|
150
|
+
// Crossref knows nothing of the fake: a 404, not a refusal that would stop the run asking it.
|
|
151
|
+
"https://api.crossref.org/works/10.1%2Ffake": () => json(404),
|
|
152
|
+
"https://dblp.org/search/publ/api/?q=Good%20Paper": () =>
|
|
153
|
+
dblp("Good Paper", ["Ada Lovelace"]),
|
|
154
|
+
"https://api.crossref.org/": () => json(200),
|
|
155
|
+
"https://dblp.org/": () => json(200, {}),
|
|
156
|
+
});
|
|
157
|
+
// Two databases of one `\bibliography`: both define `k` (bibtex takes the first), and only one
|
|
158
|
+
// copy fails — its existence, or its authors. The failing verdict is the failing copy's own.
|
|
159
|
+
const good =
|
|
160
|
+
"@inproceedings{k, author={Ada Lovelace}, title={Good Paper}, booktitle={ICSE}, doi={10.1/good}}";
|
|
161
|
+
const fake =
|
|
162
|
+
"@inproceedings{k, author={Ada Lovelace}, title={Fake Paper}, booktitle={ICSE}, doi={10.1/fake}}";
|
|
163
|
+
const drift =
|
|
164
|
+
"@inproceedings{k, author={Ada Lovelace and Alan Turing}, title={Good Paper}, booktitle={ICSE}, doi={10.1/good}}";
|
|
165
|
+
const verdicts = async (bib: string) => {
|
|
166
|
+
const r = await settle(checkOnly(bib));
|
|
167
|
+
return (r.kind === "checked" ? r.entries : []).map((e) => [
|
|
168
|
+
e.key,
|
|
169
|
+
e.exists,
|
|
170
|
+
e.authors,
|
|
171
|
+
]);
|
|
172
|
+
};
|
|
173
|
+
assert.deepEqual(
|
|
174
|
+
[
|
|
175
|
+
await verdicts(`${good}\n${fake}`),
|
|
176
|
+
await verdicts(`${fake}\n${good}`),
|
|
177
|
+
await verdicts(`${good}\n${drift}`),
|
|
178
|
+
],
|
|
179
|
+
[
|
|
180
|
+
[
|
|
181
|
+
["k", "true", "match"],
|
|
182
|
+
["k", "false", "skipped"],
|
|
183
|
+
],
|
|
184
|
+
[
|
|
185
|
+
["k", "false", "skipped"],
|
|
186
|
+
["k", "true", "match"],
|
|
187
|
+
],
|
|
188
|
+
[
|
|
189
|
+
["k", "true", "match"],
|
|
190
|
+
["k", "true", "mismatch"],
|
|
191
|
+
],
|
|
192
|
+
],
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("an author finding names what is extra and what is out of order", async () => {
|
|
135
197
|
vi.useFakeTimers();
|
|
136
198
|
fakeFetch({
|
|
137
199
|
"https://dblp.org/search/publ/api/?q=Extra%20Paper": () =>
|
|
138
200
|
dblp("Extra Paper", ["Ada Lovelace"]),
|
|
139
201
|
"https://dblp.org/search/publ/api/?q=Swapped%20Paper": () =>
|
|
140
202
|
dblp("Swapped Paper", ["Ada Lovelace", "Alan Turing"]),
|
|
141
|
-
"https://dblp.org/search/publ/api/?q=Comment%20Paper": () =>
|
|
142
|
-
dblp("Comment Paper", ["Grace Hopper"]),
|
|
143
203
|
"https://dblp.org/": () => json(200, {}),
|
|
144
204
|
"https://api.crossref.org/": () => json(200, { message: { items: [] } }),
|
|
145
205
|
});
|
|
146
206
|
const bib = [
|
|
147
207
|
"@inproceedings{extra, author={Ada Lovelace and Grace Hopper}, title={Extra Paper}, booktitle={ICSE}}",
|
|
148
208
|
"@inproceedings{swapped, author={Alan Turing and Ada Lovelace}, title={Swapped Paper}, booktitle={ICSE}}",
|
|
149
|
-
// verify-cites skips @comment blocks; bib-authors reads them — so only one checker sees this key.
|
|
150
|
-
"@comment{commented, author={Ada Lovelace}, title={Comment Paper}, booktitle={ICSE}}",
|
|
151
209
|
].join("\n");
|
|
152
210
|
const r = await settle(checkOnly(bib));
|
|
153
211
|
const why = Object.fromEntries(
|
|
@@ -165,10 +223,6 @@ test("an author finding names what is extra and what is out of order; an entry o
|
|
|
165
223
|
"unresolvable",
|
|
166
224
|
"not found by title in any database (no resolvable id to disprove) — could be a legit unindexed/regional/pre-digital work, NOT fabrication; order differs (DBLP: ICSE 2024)",
|
|
167
225
|
],
|
|
168
|
-
commented: [
|
|
169
|
-
"unresolvable",
|
|
170
|
-
"missing hopper; extra lovelace (DBLP: ICSE 2024)",
|
|
171
|
-
],
|
|
172
226
|
});
|
|
173
227
|
});
|
|
174
228
|
|
|
@@ -208,7 +262,7 @@ test(
|
|
|
208
262
|
slowFetch(40);
|
|
209
263
|
const bib = manyBib(20);
|
|
210
264
|
const serial: string[] = [];
|
|
211
|
-
for (const c of cites.
|
|
265
|
+
for (const c of cites.citationsOf(read(bib)))
|
|
212
266
|
serial.push((await cites.verifyCitationLive(c, { cache: {} })).verdict);
|
|
213
267
|
const t0 = Date.now();
|
|
214
268
|
const r = await checkOnly(bib);
|
|
@@ -265,7 +319,7 @@ const answering = () =>
|
|
|
265
319
|
async function cold(bib: string) {
|
|
266
320
|
vi.useFakeTimers();
|
|
267
321
|
answering();
|
|
268
|
-
return settle(cached(bib, EMPTY_LOOKUP_CACHE));
|
|
322
|
+
return settle(cached(read(bib), EMPTY_LOOKUP_CACHE));
|
|
269
323
|
}
|
|
270
324
|
|
|
271
325
|
test("cold: every answer is stored with the day it was fetched — responses, never verdicts", async () => {
|
|
@@ -324,7 +378,7 @@ test("🔴 warm: a fully cached bibliography asks NOTHING — no lookup, no reac
|
|
|
324
378
|
// Fake timers with NO advancing: a single DBLP pause would never resolve and time the test out.
|
|
325
379
|
vi.useFakeTimers();
|
|
326
380
|
const calls = fakeFetch({});
|
|
327
|
-
const second = await cached(bib, first.cache);
|
|
381
|
+
const second = await cached(read(bib), first.cache);
|
|
328
382
|
assert.deepEqual(
|
|
329
383
|
// Nothing fetched, so the cache comes back as the very object passed in — nothing to write.
|
|
330
384
|
{ calls, check: second.check, sameCache: second.cache === first.cache },
|
|
@@ -338,7 +392,7 @@ test("🔴 one entry edited: only that entry is asked again, and the old answers
|
|
|
338
392
|
vi.useFakeTimers();
|
|
339
393
|
const calls = answering();
|
|
340
394
|
const edited = PAPERS.join("\n").replace("Other Paper", "Renamed Paper");
|
|
341
|
-
const second = await settle(cached(edited, first.cache));
|
|
395
|
+
const second = await settle(cached(read(edited), first.cache));
|
|
342
396
|
assert.deepEqual(
|
|
343
397
|
{
|
|
344
398
|
// The lookups run concurrently, so the order of requests is not the subject.
|
|
@@ -373,14 +427,14 @@ test("an answer older than 30 days is asked again and re-dated; at 30 days it is
|
|
|
373
427
|
vi.useFakeTimers();
|
|
374
428
|
const quiet = fakeFetch({});
|
|
375
429
|
const at30 = await referencesChecker({ today: () => "2026-10-27" })(
|
|
376
|
-
bib,
|
|
430
|
+
read(bib),
|
|
377
431
|
first.cache,
|
|
378
432
|
);
|
|
379
433
|
vi.useRealTimers();
|
|
380
434
|
vi.useFakeTimers();
|
|
381
435
|
const calls = answering();
|
|
382
436
|
const at31 = await settle(
|
|
383
|
-
referencesChecker({ today: () => "2026-10-28" })(bib, first.cache),
|
|
437
|
+
referencesChecker({ today: () => "2026-10-28" })(read(bib), first.cache),
|
|
384
438
|
);
|
|
385
439
|
assert.deepEqual(
|
|
386
440
|
{
|
|
@@ -408,7 +462,7 @@ test("a failed lookup is not cached: the next run asks it again", async () => {
|
|
|
408
462
|
"https://dblp.org/": () => json(500),
|
|
409
463
|
});
|
|
410
464
|
const bib = PAPERS[1];
|
|
411
|
-
const r = await settle(cached(bib, EMPTY_LOOKUP_CACHE));
|
|
465
|
+
const r = await settle(cached(read(bib), EMPTY_LOOKUP_CACHE));
|
|
412
466
|
const failed: LookupCache = r.cache;
|
|
413
467
|
assert.deepEqual(
|
|
414
468
|
{
|
|
@@ -423,7 +477,7 @@ test("offline with an incomplete cache: not-checked, and the cache comes back un
|
|
|
423
477
|
vi.stubGlobal("fetch", () =>
|
|
424
478
|
Promise.reject(new Error("getaddrinfo ENOTFOUND api.crossref.org")),
|
|
425
479
|
);
|
|
426
|
-
const r = await cached(PAPERS[0], EMPTY_LOOKUP_CACHE);
|
|
480
|
+
const r = await cached(read(PAPERS[0]), EMPTY_LOOKUP_CACHE);
|
|
427
481
|
assert.deepEqual(r, {
|
|
428
482
|
check: {
|
|
429
483
|
kind: "not-checked",
|
|
@@ -461,7 +515,7 @@ test("🔴 #120: DBLP answering HTML is asked ONCE — no retries, no pause —
|
|
|
461
515
|
() => new Response("<!doctype html><title>429</title>"),
|
|
462
516
|
() => json(429),
|
|
463
517
|
);
|
|
464
|
-
const { check } = await cached(PUBLISHED, EMPTY_LOOKUP_CACHE);
|
|
518
|
+
const { check } = await cached(read(PUBLISHED), EMPTY_LOOKUP_CACHE);
|
|
465
519
|
const entries = check.kind === "checked" ? check.entries : [];
|
|
466
520
|
assert.deepEqual(
|
|
467
521
|
{
|
|
@@ -507,14 +561,14 @@ test("#120: a DBLP 429 trips it as well; DBLP answering keeps its 900 ms pace be
|
|
|
507
561
|
() => json(429),
|
|
508
562
|
() => json(200, { data: [] }),
|
|
509
563
|
);
|
|
510
|
-
await cached(PUBLISHED, EMPTY_LOOKUP_CACHE);
|
|
564
|
+
await cached(read(PUBLISHED), EMPTY_LOOKUP_CACHE);
|
|
511
565
|
vi.useRealTimers();
|
|
512
566
|
vi.useFakeTimers();
|
|
513
567
|
const answering = refusing(
|
|
514
568
|
() => dblp("First Paper", ["Ada Lovelace"]),
|
|
515
569
|
() => json(200, { data: [] }),
|
|
516
570
|
);
|
|
517
|
-
const pending = cached(PUBLISHED, EMPTY_LOOKUP_CACHE);
|
|
571
|
+
const pending = cached(read(PUBLISHED), EMPTY_LOOKUP_CACHE);
|
|
518
572
|
// One compared entry, then the pause: without advancing time, the run cannot finish.
|
|
519
573
|
let done = false;
|
|
520
574
|
void pending.finally(() => (done = true));
|
|
@@ -43,6 +43,7 @@ import type {
|
|
|
43
43
|
AuthorFinding,
|
|
44
44
|
BibEntry,
|
|
45
45
|
} from "../../../skills/verify-citations/scripts/bib-authors.mjs";
|
|
46
|
+
import { zip } from "remeda";
|
|
46
47
|
import { messageOf } from "../../domain/text.ts";
|
|
47
48
|
import { isRecord } from "../../domain/record.ts";
|
|
48
49
|
|
|
@@ -55,12 +56,13 @@ const describeAuthors = (f: AuthorFinding): string =>
|
|
|
55
56
|
.filter(Boolean)
|
|
56
57
|
.join("; ") + ` (DBLP: ${f.venue})`;
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
/** bib-authors' answer for one entry, from its buckets for that entry alone. */
|
|
60
|
+
const authorsOf = (a: AuthorBuckets): EntryVerdict["authors"] =>
|
|
61
|
+
a.findings.length > 0
|
|
60
62
|
? "mismatch"
|
|
61
|
-
: a.unchecked.
|
|
63
|
+
: a.unchecked.length > 0
|
|
62
64
|
? "unchecked"
|
|
63
|
-
: a.matched.
|
|
65
|
+
: a.matched.length > 0
|
|
64
66
|
? "match"
|
|
65
67
|
: "skipped";
|
|
66
68
|
|
|
@@ -69,36 +71,45 @@ const authorsOf = (key: string, a: AuthorBuckets): EntryVerdict["authors"] =>
|
|
|
69
71
|
* one, so "unresolvable" is not read as "every registry looked and found nothing". Nothing for a
|
|
70
72
|
* confirmed work: a refusal after a confirmation changed nothing.
|
|
71
73
|
*/
|
|
72
|
-
const unconfirmed = (
|
|
73
|
-
c
|
|
74
|
-
): readonly (string | undefined)[] =>
|
|
75
|
-
c && c.verdict !== "true"
|
|
74
|
+
const unconfirmed = (c: CiteVerdict): readonly (string | undefined)[] =>
|
|
75
|
+
c.verdict !== "true"
|
|
76
76
|
? [c.reason, ...(c.refused ?? []).map((r) => `not asked: ${r}`)]
|
|
77
77
|
: [];
|
|
78
78
|
|
|
79
|
-
/** One entry's verdict
|
|
80
|
-
function entryVerdict(
|
|
81
|
-
key: string,
|
|
82
|
-
found: readonly CiteVerdict[],
|
|
83
|
-
a: AuthorBuckets,
|
|
84
|
-
): EntryVerdict {
|
|
85
|
-
const c = found.find((x) => x.id === key);
|
|
86
|
-
const mismatch = a.findings.find((f) => f.key === key);
|
|
79
|
+
/** One entry's verdict: verify-cites' answer for it, and bib-authors' buckets for it alone. */
|
|
80
|
+
function entryVerdict(c: CiteVerdict, a: AuthorBuckets): EntryVerdict {
|
|
87
81
|
const why = [
|
|
88
82
|
...unconfirmed(c),
|
|
89
|
-
|
|
90
|
-
a.unchecked.
|
|
83
|
+
...a.findings.map(describeAuthors),
|
|
84
|
+
...a.unchecked.map((u) => u.why),
|
|
91
85
|
]
|
|
92
86
|
.filter(Boolean)
|
|
93
87
|
.join("; ");
|
|
94
88
|
return {
|
|
95
|
-
key,
|
|
96
|
-
exists: c
|
|
97
|
-
authors: authorsOf(
|
|
89
|
+
key: c.id,
|
|
90
|
+
exists: c.verdict,
|
|
91
|
+
authors: authorsOf(a),
|
|
98
92
|
...(why ? { why } : {}),
|
|
99
93
|
};
|
|
100
94
|
}
|
|
101
95
|
|
|
96
|
+
/**
|
|
97
|
+
* bib-authors over each entry on its own, one after another as it would run them: its buckets name
|
|
98
|
+
* an entry by key, and two entries of a bibliography may share a key (two databases bibtex
|
|
99
|
+
* opened may each define it, with other metadata).
|
|
100
|
+
*/
|
|
101
|
+
const authorsOfEach = (
|
|
102
|
+
parsed: readonly BibEntry[],
|
|
103
|
+
d: Pick<ReturnType<typeof cachedDblp>, "lookup" | "pause">,
|
|
104
|
+
): Promise<readonly AuthorBuckets[]> =>
|
|
105
|
+
parsed.reduce<Promise<readonly AuthorBuckets[]>>(
|
|
106
|
+
async (done, e) => [
|
|
107
|
+
...(await done),
|
|
108
|
+
await authors.checkAuthors([e], { lookup: d.lookup, pause: d.pause }),
|
|
109
|
+
],
|
|
110
|
+
Promise.resolve([]),
|
|
111
|
+
);
|
|
112
|
+
|
|
102
113
|
/** Whether bib-authors will ask DBLP about this entry — the filter its `checkAuthors` applies. */
|
|
103
114
|
const asksDblp = (e: BibEntry): boolean =>
|
|
104
115
|
Boolean(e.title && e.author) &&
|
|
@@ -212,8 +223,8 @@ export const referencesChecker =
|
|
|
212
223
|
async (bib, cache) => {
|
|
213
224
|
// Answers past MAX_AGE_DAYS are left out, so the run asks them again (and `grown` re-dates them).
|
|
214
225
|
const usable = freshPart(cache, today());
|
|
215
|
-
const citations = cites.
|
|
216
|
-
const parsed = authors.
|
|
226
|
+
const citations = cites.citationsOf(bib);
|
|
227
|
+
const parsed = authors.authorEntries(bib);
|
|
217
228
|
const store: Record<string, unknown> = Object.fromEntries(
|
|
218
229
|
[...usable.citations].map(([k, v]) => [k, v.response]),
|
|
219
230
|
);
|
|
@@ -237,16 +248,10 @@ export const referencesChecker =
|
|
|
237
248
|
breaker,
|
|
238
249
|
}),
|
|
239
250
|
),
|
|
240
|
-
|
|
241
|
-
lookup: d.lookup,
|
|
242
|
-
pause: d.pause,
|
|
243
|
-
}),
|
|
244
|
-
]);
|
|
245
|
-
const keys = new Set([
|
|
246
|
-
...found.map((c) => c.id),
|
|
247
|
-
...a.findings.map((f) => f.key),
|
|
251
|
+
authorsOfEach(parsed, d),
|
|
248
252
|
]);
|
|
249
|
-
|
|
253
|
+
// Both lists are one per entry, in its order: so is the verdict list.
|
|
254
|
+
const entries = zip(found, a).map(([c, b]) => entryVerdict(c, b));
|
|
250
255
|
return {
|
|
251
256
|
check: { kind: "checked", entries },
|
|
252
257
|
cache: grown({ cache, usable }, { store, dblp: d.dblp }, today),
|