paperlint 7.2.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +9 -25
- package/CONTRIBUTING.md +5 -4
- package/README.md +4 -3
- package/dist/adapters/bibtex/index.d.ts +3 -0
- package/dist/adapters/bibtex/index.d.ts.map +1 -0
- package/dist/adapters/bibtex/index.js +103 -0
- package/dist/adapters/bibtex/index.js.map +1 -0
- package/dist/adapters/latex/filecontents.d.ts +33 -0
- package/dist/adapters/latex/filecontents.d.ts.map +1 -0
- package/dist/adapters/latex/filecontents.js +65 -0
- package/dist/adapters/latex/filecontents.js.map +1 -0
- package/dist/adapters/latex/includes.d.ts +3 -3
- package/dist/adapters/latex/includes.d.ts.map +1 -1
- package/dist/adapters/latex/includes.js +18 -10
- package/dist/adapters/latex/includes.js.map +1 -1
- package/dist/adapters/latex/index.d.ts.map +1 -1
- package/dist/adapters/latex/index.js +2 -0
- package/dist/adapters/latex/index.js.map +1 -1
- package/dist/adapters/latex/nodes.d.ts +12 -0
- package/dist/adapters/latex/nodes.d.ts.map +1 -1
- package/dist/adapters/latex/nodes.js +39 -0
- package/dist/adapters/latex/nodes.js.map +1 -1
- package/dist/adapters/latex/parse.d.ts +7 -1
- package/dist/adapters/latex/parse.d.ts.map +1 -1
- package/dist/adapters/latex/parse.js +55 -6
- package/dist/adapters/latex/parse.js.map +1 -1
- package/dist/adapters/memory/index.d.ts +3 -0
- package/dist/adapters/memory/index.d.ts.map +1 -1
- package/dist/adapters/memory/index.js +9 -0
- package/dist/adapters/memory/index.js.map +1 -1
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +27 -24
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/adapters/sources-record/index.d.ts +9 -0
- package/dist/adapters/sources-record/index.d.ts.map +1 -0
- package/dist/adapters/sources-record/index.js +68 -0
- package/dist/adapters/sources-record/index.js.map +1 -0
- package/dist/adapters/tex-output/blg.d.ts +9 -0
- package/dist/adapters/tex-output/blg.d.ts.map +1 -0
- package/dist/adapters/tex-output/blg.js +20 -0
- package/dist/adapters/tex-output/blg.js.map +1 -0
- package/dist/adapters/tex-output/fls.d.ts +8 -0
- package/dist/adapters/tex-output/fls.d.ts.map +1 -0
- package/dist/adapters/tex-output/fls.js +16 -0
- package/dist/adapters/tex-output/fls.js.map +1 -0
- package/dist/adapters/tex-output/index.d.ts +7 -0
- package/dist/adapters/tex-output/index.d.ts.map +1 -0
- package/dist/adapters/tex-output/index.js +7 -0
- package/dist/adapters/tex-output/index.js.map +1 -0
- package/dist/bib-rules.d.ts +47 -0
- package/dist/bib-rules.d.ts.map +1 -0
- package/dist/bib-rules.js +127 -0
- package/dist/bib-rules.js.map +1 -0
- package/dist/build.d.ts +29 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +41 -30
- package/dist/build.js.map +1 -1
- package/dist/claim-provenance.d.ts +2 -2
- package/dist/cli.d.ts +9 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +43 -68
- package/dist/cli.js.map +1 -1
- package/dist/domain/paper-source.d.ts +1 -10
- package/dist/domain/paper-source.d.ts.map +1 -1
- package/dist/domain/paper-source.js +34 -24
- package/dist/domain/paper-source.js.map +1 -1
- package/dist/domain/paper-sources.d.ts +72 -0
- package/dist/domain/paper-sources.d.ts.map +1 -0
- package/dist/domain/paper-sources.js +18 -0
- package/dist/domain/paper-sources.js.map +1 -0
- package/dist/domain/sources-record.d.ts +102 -0
- package/dist/domain/sources-record.d.ts.map +1 -0
- package/dist/domain/sources-record.js +38 -0
- package/dist/domain/sources-record.js.map +1 -0
- package/dist/domain/tex-run.d.ts +25 -0
- package/dist/domain/tex-run.d.ts.map +1 -0
- package/dist/domain/tex-run.js +79 -0
- package/dist/domain/tex-run.js.map +1 -0
- package/dist/eslint-rules/latex-language.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.js +10 -13
- package/dist/eslint-rules/latex-language.js.map +1 -1
- package/dist/eslint-rules/paper-context.d.ts.map +1 -1
- package/dist/eslint-rules/paper-context.js +6 -5
- package/dist/eslint-rules/paper-context.js.map +1 -1
- package/dist/eslint-rules/paper-typography.d.ts +0 -9
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
- package/dist/eslint-rules/paper-typography.js +34 -96
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/eslint-rules/papers.d.ts +1 -3
- package/dist/eslint-rules/papers.d.ts.map +1 -1
- package/dist/eslint-rules/papers.js +2 -4
- package/dist/eslint-rules/papers.js.map +1 -1
- package/dist/eslint-rules/tex-build.d.ts.map +1 -1
- package/dist/eslint-rules/tex-build.js +8 -25
- package/dist/eslint-rules/tex-build.js.map +1 -1
- package/dist/init.d.ts +1 -4
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +3 -9
- package/dist/init.js.map +1 -1
- package/dist/latex-log.d.ts +10 -0
- package/dist/latex-log.d.ts.map +1 -1
- package/dist/latex-log.js +10 -0
- package/dist/latex-log.js.map +1 -1
- package/dist/latex-loop.d.ts +2 -0
- package/dist/latex-loop.d.ts.map +1 -1
- package/dist/latex-loop.js.map +1 -1
- package/dist/lib/markdown.d.ts +5 -81
- package/dist/lib/markdown.d.ts.map +1 -1
- package/dist/lib/markdown.js +5 -116
- package/dist/lib/markdown.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +0 -2
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/lib/skill-eval-fixture.d.ts +6 -4
- package/dist/lib/skill-eval-fixture.d.ts.map +1 -1
- package/dist/lib/skill-eval-fixture.js +25 -18
- package/dist/lib/skill-eval-fixture.js.map +1 -1
- package/dist/new-paper.d.ts +5 -11
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +12 -23
- package/dist/new-paper.js.map +1 -1
- package/dist/package-dirs.d.ts.map +1 -1
- package/dist/package-dirs.js +3 -2
- package/dist/package-dirs.js.map +1 -1
- package/dist/paper-files.d.ts +4 -4
- package/dist/paper-includes.d.ts +4 -7
- package/dist/paper-includes.d.ts.map +1 -1
- package/dist/paper-includes.js +29 -21
- package/dist/paper-includes.js.map +1 -1
- package/dist/paper-prose.d.ts +17 -0
- package/dist/paper-prose.d.ts.map +1 -0
- package/dist/paper-prose.js +46 -0
- package/dist/paper-prose.js.map +1 -0
- package/dist/paper-record.d.ts +25 -0
- package/dist/paper-record.d.ts.map +1 -0
- package/dist/paper-record.js +39 -0
- package/dist/paper-record.js.map +1 -0
- package/dist/paper-sources.d.ts +66 -0
- package/dist/paper-sources.d.ts.map +1 -0
- package/dist/paper-sources.js +92 -0
- package/dist/paper-sources.js.map +1 -0
- package/dist/ports/bib-reader.d.ts +18 -0
- package/dist/ports/bib-reader.d.ts.map +1 -0
- package/dist/ports/bib-reader.js +2 -0
- package/dist/ports/bib-reader.js.map +1 -0
- package/dist/ports/check-references.d.ts +9 -4
- package/dist/ports/check-references.d.ts.map +1 -1
- package/dist/ports/latex.d.ts +15 -0
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/ports/sources-codec.d.ts +11 -0
- package/dist/ports/sources-codec.d.ts.map +1 -0
- package/dist/ports/sources-codec.js +2 -0
- package/dist/ports/sources-codec.js.map +1 -0
- package/dist/ports/tex-output.d.ts +17 -0
- package/dist/ports/tex-output.d.ts.map +1 -0
- package/dist/ports/tex-output.js +2 -0
- package/dist/ports/tex-output.js.map +1 -0
- package/dist/recorded-bibliography.d.ts +89 -0
- package/dist/recorded-bibliography.d.ts.map +1 -0
- package/dist/recorded-bibliography.js +136 -0
- package/dist/recorded-bibliography.js.map +1 -0
- package/dist/reference-rules.d.ts +5 -5
- package/dist/reference-rules.d.ts.map +1 -1
- package/dist/reference-rules.js +150 -77
- package/dist/reference-rules.js.map +1 -1
- package/dist/references.d.ts +71 -16
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +110 -26
- package/dist/references.js.map +1 -1
- package/dist/register-bands.d.ts +3 -3
- package/dist/register-bands.d.ts.map +1 -1
- package/dist/register-bands.js.map +1 -1
- package/dist/register.d.ts +2 -2
- package/dist/sources-record.d.ts +38 -0
- package/dist/sources-record.d.ts.map +1 -0
- package/dist/sources-record.js +123 -0
- package/dist/sources-record.js.map +1 -0
- package/dist/sources-rules.d.ts +44 -0
- package/dist/sources-rules.d.ts.map +1 -0
- package/dist/sources-rules.js +76 -0
- package/dist/sources-rules.js.map +1 -0
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +4 -3
- package/dist/structure.js.map +1 -1
- package/dist/tex-paper.d.ts +10 -15
- package/dist/tex-paper.d.ts.map +1 -1
- package/dist/tex-paper.js +24 -40
- package/dist/tex-paper.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +3 -3
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/types.d.ts +0 -2
- package/dist/types.d.ts.map +1 -1
- package/docs/configuration.md +18 -12
- package/docs/design/paper-sources.md +197 -0
- package/docs/e2e.md +4 -6
- package/docs/package-shape-options.md +1 -1
- package/docs/prior-art/paper-folder-scaffolding.md +24 -29
- package/docs/references.md +34 -8
- package/docs/rules/bib/reachable-entry.md +64 -0
- package/docs/rules/paper/sources-fresh.md +81 -0
- package/docs/rules/tex/claim-provenance.md +0 -1
- package/docs/rules.md +62 -61
- package/eslint-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.ts +10 -13
- package/eslint-rules/paper-context.ts +6 -5
- package/eslint-rules/paper-typography.test.mjs +29 -157
- package/eslint-rules/paper-typography.ts +35 -104
- package/eslint-rules/papers.harness.mjs +1 -1
- package/eslint-rules/papers.ts +2 -4
- package/eslint-rules/tex-build.harness.mjs +2 -2
- package/eslint-rules/tex-build.ts +8 -25
- package/eslint.config.mjs +22 -1
- package/fixtures/build-e2e/aidc/paper.tex +1 -1
- package/fixtures/build-e2e/anon-ack/paper.tex +1 -1
- package/fixtures/build-e2e/anon-author/paper.tex +1 -1
- package/fixtures/build-e2e/anon-bib/paper.tex +1 -1
- package/fixtures/build-e2e/anon-href/paper.tex +1 -1
- package/fixtures/build-e2e/anon-macro/paper.tex +1 -1
- package/fixtures/build-e2e/anon-pdfauthor/paper.tex +1 -1
- package/fixtures/paper-sources/p1/paper.tex +30 -0
- package/fixtures/paper-sources/p1/refs.bib +6 -0
- package/fixtures/paper-sources/p1/sections/dead.tex +6 -0
- package/fixtures/paper-sources/p1/sections/intro.tex +7 -0
- package/fixtures/paper-sources/p1/sections/parked.tex +6 -0
- package/fixtures/paper-sources/p1/tex-truth.json +37 -0
- package/fixtures/paper-sources/p1/versions/2026-01-01-review.tex +4 -0
- package/fixtures/paper-sources/v1-stale/paper.tex +14 -0
- package/fixtures/paper-sources/v1-stale/refs.bib +6 -0
- package/fixtures/paper-sources/v1-stale/tex-truth.json +32 -0
- package/fixtures/paper-sources/v10-ifthenelse/anon.bib +1 -0
- package/fixtures/paper-sources/v10-ifthenelse/paper.tex +9 -0
- package/fixtures/paper-sources/v10-ifthenelse/real.bib +1 -0
- package/fixtures/paper-sources/v10-ifthenelse/tex-truth.json +28 -0
- package/fixtures/paper-sources/v11-two-blocks/paper.tex +12 -0
- package/fixtures/paper-sources/v11-two-blocks/tex-truth.json +30 -0
- package/fixtures/paper-sources/v13-percent-text/paper.tex +6 -0
- package/fixtures/paper-sources/v13-percent-text/refs.bib +3 -0
- package/fixtures/paper-sources/v13-percent-text/tex-truth.json +30 -0
- package/fixtures/paper-sources/v15-after-end/paper.tex +6 -0
- package/fixtures/paper-sources/v15-after-end/refs.bib +1 -0
- package/fixtures/paper-sources/v15-after-end/tex-truth.json +17 -0
- package/fixtures/paper-sources/v16-included-block/bibblock.tex +3 -0
- package/fixtures/paper-sources/v16-included-block/paper.tex +7 -0
- package/fixtures/paper-sources/v16-included-block/refs.bib +1 -0
- package/fixtures/paper-sources/v16-included-block/tex-truth.json +35 -0
- package/fixtures/paper-sources/v17-include-order/bibsetup.tex +1 -0
- package/fixtures/paper-sources/v17-include-order/first.bib +1 -0
- package/fixtures/paper-sources/v17-include-order/later.bib +1 -0
- package/fixtures/paper-sources/v17-include-order/paper.tex +7 -0
- package/fixtures/paper-sources/v17-include-order/tex-truth.json +40 -0
- package/fixtures/paper-sources/v2-overwrite/paper.tex +14 -0
- package/fixtures/paper-sources/v2-overwrite/refs.bib +6 -0
- package/fixtures/paper-sources/v2-overwrite/tex-truth.json +30 -0
- package/fixtures/paper-sources/v22-parked-thebibliography/paper.tex +7 -0
- package/fixtures/paper-sources/v22-parked-thebibliography/tex-truth.json +17 -0
- package/fixtures/paper-sources/v23-parked-include/old.bib +1 -0
- package/fixtures/paper-sources/v23-parked-include/paper.tex +7 -0
- package/fixtures/paper-sources/v23-parked-include/parked.tex +1 -0
- package/fixtures/paper-sources/v23-parked-include/refs.bib +1 -0
- package/fixtures/paper-sources/v23-parked-include/tex-truth.json +28 -0
- package/fixtures/paper-sources/v24-quoted-fields/paper.tex +6 -0
- package/fixtures/paper-sources/v24-quoted-fields/refs.bib +5 -0
- package/fixtures/paper-sources/v24-quoted-fields/tex-truth.json +32 -0
- package/fixtures/paper-sources/v25-string-differs/paper.tex +10 -0
- package/fixtures/paper-sources/v25-string-differs/refs.bib +2 -0
- package/fixtures/paper-sources/v25-string-differs/tex-truth.json +32 -0
- package/fixtures/paper-sources/v26-comment-in-names/one.bib +1 -0
- package/fixtures/paper-sources/v26-comment-in-names/paper.tex +7 -0
- package/fixtures/paper-sources/v26-comment-in-names/tex-truth.json +32 -0
- package/fixtures/paper-sources/v26-comment-in-names/two.bib +1 -0
- package/fixtures/paper-sources/v27-shared-string/abbreviations.bib +2 -0
- package/fixtures/paper-sources/v27-shared-string/paper.tex +6 -0
- package/fixtures/paper-sources/v27-shared-string/references.bib +6 -0
- package/fixtures/paper-sources/v27-shared-string/tex-truth.json +31 -0
- package/fixtures/paper-sources/v28-shared-string-block/abbreviations.bib +2 -0
- package/fixtures/paper-sources/v28-shared-string-block/paper.tex +14 -0
- package/fixtures/paper-sources/v28-shared-string-block/tex-truth.json +33 -0
- package/fixtures/paper-sources/v29-generated-input/paper.tex +9 -0
- package/fixtures/paper-sources/v29-generated-input/tex-truth.json +24 -0
- package/fixtures/paper-sources/v3-declared/paper.bib +6 -0
- package/fixtures/paper-sources/v3-declared/paper.tex +6 -0
- package/fixtures/paper-sources/v3-declared/tex-truth.json +28 -0
- package/fixtures/paper-sources/v30-name-next-line/paper.tex +14 -0
- package/fixtures/paper-sources/v30-name-next-line/tex-truth.json +30 -0
- package/fixtures/paper-sources/v4-commented/paper.tex +14 -0
- package/fixtures/paper-sources/v4-commented/refs.bib +6 -0
- package/fixtures/paper-sources/v4-commented/tex-truth.json +28 -0
- package/fixtures/paper-sources/v5-percent-entry/paper.tex +20 -0
- package/fixtures/paper-sources/v5-percent-entry/tex-truth.json +37 -0
- package/fixtures/paper-sources/v6-unclosed/paper.tex +6 -0
- package/fixtures/paper-sources/v6-unclosed/refs.bib +5 -0
- package/fixtures/paper-sources/v6-unclosed/tex-truth.json +37 -0
- package/fixtures/paper-sources/v8-jobname/paper.tex +9 -0
- package/fixtures/paper-sources/v8-jobname/tex-truth.json +30 -0
- package/fixtures/paper-sources/v9-two-bibliographies/one.bib +1 -0
- package/fixtures/paper-sources/v9-two-bibliographies/paper.tex +7 -0
- package/fixtures/paper-sources/v9-two-bibliographies/tex-truth.json +35 -0
- package/fixtures/paper-sources/v9-two-bibliographies/two.bib +1 -0
- package/fixtures/paper-typography/clean-paper/paper.tex +1 -1
- package/fixtures/paper-typography/messy-paper/paper.tex +1 -1
- package/hooks/hooks.harness.ts +15 -15
- package/hooks/paper-edit-guard.hook.mjs +5 -8
- package/hooks/paper-edit-guard.test.ts +12 -3
- package/hooks/paper-skills-nudge.hook.mjs +2 -3
- package/hooks/paper-skills-nudge.test.ts +13 -3
- package/hooks/paper-status-gates.hook.mjs +2 -2
- package/hooks/paper-status-gates.test.ts +14 -2
- package/lib/markdown.test.mjs +5 -60
- package/lib/markdown.ts +9 -136
- package/lib/paper-config.ts +0 -2
- package/lib/skill-checks.test.mjs +4 -1
- package/lib/skill-eval-fixture.ts +25 -18
- package/package.json +2 -12
- package/presets/realm.jsonc +1 -1
- package/scripts/check.test.ts +23 -1
- package/scripts/check.ts +7 -3
- package/scripts/coverage-config.test.ts +19 -10
- package/scripts/harness-api.frozen.json +0 -5
- package/scripts/layer-legacy-frozen.test.ts +41 -2
- package/scripts/layer-legacy-frozen.ts +31 -2
- package/skills/cold-read-diff/SKILL.md +3 -3
- package/skills/cold-read-diff/SKILL.md.spec.ts +2 -2
- package/skills/grade-paper-writing/SKILL.md +15 -22
- package/skills/grade-paper-writing/SKILL.md.spec.ts +14 -21
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +40 -26
- package/skills/grade-paper-writing/prose-lint.mjs +117 -278
- package/skills/grade-paper-writing/prose-lint.test.ts +230 -79
- package/skills/paper-adversarial-review/SKILL.md +2 -2
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
- package/skills/paper-pipeline/SKILL.md +9 -12
- package/skills/paper-pipeline/SKILL.md.spec.ts +8 -11
- package/skills/paper-pipeline/description-language.eval.mjs +25 -15
- package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +25 -15
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +25 -15
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +27 -16
- package/skills/paper-pipeline/pipeline-language.eval.mjs +25 -15
- package/skills/paper-pipeline/references/acceptance-gate.md +16 -20
- package/skills/paper-pipeline/references/artifact-checklist.md +4 -5
- package/skills/paper-pipeline/references/pipeline-status-template.md +1 -1
- package/skills/paper-pipeline/scripts/README.md +0 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +91 -105
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +85 -241
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +166 -153
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +3 -3
- package/skills/paper-status/SKILL.md +2 -2
- package/skills/paper-status/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/SKILL.md +2 -2
- package/skills/render-paper/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/extract-pdf-facts.mjs +2 -2
- package/skills/tighten-paper/SKILL.md +29 -42
- package/skills/tighten-paper/SKILL.md.spec.ts +28 -41
- package/skills/verify-citations/SKILL.md +7 -4
- package/skills/verify-citations/SKILL.md.spec.ts +6 -3
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +17 -32
- package/skills/verify-citations/scripts/bib-authors.cli.test.ts +138 -43
- package/skills/verify-citations/scripts/bib-authors.d.mts +22 -2
- package/skills/verify-citations/scripts/bib-authors.mjs +54 -193
- package/skills/verify-citations/scripts/bib-authors.test.mjs +1 -44
- package/skills/verify-citations/scripts/verify-cites.cli.test.ts +16 -0
- package/skills/verify-citations/scripts/verify-cites.d.mts +19 -3
- package/skills/verify-citations/scripts/verify-cites.mjs +42 -108
- package/skills/verify-citations/scripts/verify-cites.test.mjs +23 -36
- package/src/adapters/bibtex/index.test.ts +163 -0
- package/src/adapters/bibtex/index.ts +155 -0
- package/src/adapters/latex/filecontents.ts +114 -0
- package/src/adapters/latex/includes.ts +18 -10
- package/src/adapters/latex/index.test.ts +73 -0
- package/src/adapters/latex/index.ts +2 -0
- package/src/adapters/latex/nodes.ts +64 -0
- package/src/adapters/latex/parse.test.ts +38 -0
- package/src/adapters/latex/parse.ts +67 -6
- package/src/adapters/memory/index.ts +17 -0
- package/src/adapters/references/index.test.ts +75 -21
- package/src/adapters/references/index.ts +37 -32
- package/src/adapters/sources-record/index.test.ts +87 -0
- package/src/adapters/sources-record/index.ts +82 -0
- package/src/adapters/tex-output/blg.ts +27 -0
- package/src/adapters/tex-output/fls.ts +24 -0
- package/src/adapters/tex-output/index.test.ts +171 -0
- package/src/adapters/tex-output/index.ts +12 -0
- package/src/bib-rules.test.ts +356 -0
- package/src/bib-rules.ts +228 -0
- package/src/bibliography-at.test.ts +286 -0
- package/src/build.harness.mjs +2 -2
- package/src/build.test.ts +211 -1
- package/src/build.ts +101 -41
- package/src/claim-provenance.test.ts +33 -7
- package/src/claim-provenance.ts +2 -2
- package/src/cli.harness.mjs +46 -56
- package/src/cli.test.ts +44 -48
- package/src/cli.ts +51 -82
- package/src/domain/paper-source.test.ts +58 -20
- package/src/domain/paper-source.ts +38 -42
- package/src/domain/paper-sources.ts +94 -0
- package/src/domain/sources-record.test.ts +71 -0
- package/src/domain/sources-record.ts +136 -0
- package/src/domain/tex-run.test.ts +228 -0
- package/src/domain/tex-run.ts +139 -0
- package/src/folder-venue-rule.test.ts +1 -1
- package/src/heading-case.test.ts +24 -10
- package/src/init.test.ts +6 -7
- package/src/init.ts +3 -22
- package/src/latex-log.ts +20 -0
- package/src/latex-loop.harness.ts +1 -0
- package/src/latex-loop.ts +2 -0
- package/src/lint-fix.test.ts +10 -4
- package/src/linted-set.test.ts +9 -1
- package/src/new-paper.harness.mjs +12 -30
- package/src/new-paper.test.ts +15 -11
- package/src/new-paper.ts +11 -30
- package/src/package-dirs.ts +3 -2
- package/src/paper-files.test.ts +5 -4
- package/src/paper-files.ts +4 -4
- package/src/paper-includes.test.ts +157 -38
- package/src/paper-includes.ts +39 -35
- package/src/paper-prose.test.ts +78 -0
- package/src/paper-prose.ts +67 -0
- package/src/paper-record.test.ts +173 -0
- package/src/paper-record.ts +66 -0
- package/src/paper-settings-commands.test.ts +5 -2
- package/src/paper-sources.ts +151 -0
- package/src/parse-args.test.ts +0 -1
- package/src/ports/bib-reader.ts +27 -0
- package/src/ports/check-references.ts +7 -3
- package/src/ports/latex.ts +19 -0
- package/src/ports/sources-codec.ts +11 -0
- package/src/ports/tex-output.ts +17 -0
- package/src/recorded-bibliography.ts +297 -0
- package/src/reference-rules.ts +217 -104
- package/src/references.test.ts +520 -177
- package/src/references.ts +185 -37
- package/src/register-bands.test.ts +5 -1
- package/src/register-bands.ts +3 -5
- package/src/register.test.ts +6 -1
- package/src/register.ts +2 -2
- package/src/rule-cases.test.ts +68 -12
- package/src/rule-docs.test.ts +0 -1
- package/src/sources-record.test.ts +308 -0
- package/src/sources-record.ts +231 -0
- package/src/sources-rules.test.ts +152 -0
- package/src/sources-rules.ts +117 -0
- package/src/structure.harness.ts +5 -11
- package/src/structure.ts +4 -3
- package/src/tex-paper.test.ts +148 -78
- package/src/tex-paper.ts +31 -61
- package/src/tex-venue-rules.test.ts +36 -8
- package/src/tex-venue-rules.ts +3 -5
- package/src/types.ts +0 -2
- package/dist/eslint-rules/bib-reachable-entry.d.ts +0 -22
- package/dist/eslint-rules/bib-reachable-entry.d.ts.map +0 -1
- package/dist/eslint-rules/bib-reachable-entry.js +0 -75
- package/dist/eslint-rules/bib-reachable-entry.js.map +0 -1
- package/eslint-rules/bib-reachable-entry.ts +0 -76
- package/fixtures/real-markdown-paper/baseline.json +0 -22
- package/fixtures/real-markdown-paper/baseline.ts +0 -70
- package/skills/paper-pipeline/PIPELINE-MAP.md +0 -381
- package/skills/paper-pipeline/pipeline-firing.baseline.json +0 -270
- package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +0 -585
- package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +0 -492
- package/skills/paper-pipeline/scripts/artifact-coverage.mjs +0 -397
- package/skills/paper-pipeline/scripts/artifact-coverage.test.ts +0 -86
- package/skills/paper-pipeline/scripts/check-provenance.mjs +0 -184
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +0 -135
- package/skills/paper-pipeline/scripts/population-map.mjs +0 -188
- package/skills/paper-pipeline/scripts/population-map.test.mjs +0 -154
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +0 -266
- package/skills/paper-pipeline/scripts/round-diff.harness.ts +0 -907
- package/skills/paper-pipeline/scripts/round-diff.mjs +0 -583
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +0 -159
- package/skills/tighten-paper/__snapshots__/structure.test.ts.snap +0 -133
- package/skills/tighten-paper/structure.mjs +0 -364
- package/skills/tighten-paper/structure.test.ts +0 -117
- package/templates/paper/paper.md +0 -4
package/src/paper-includes.ts
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* THE FILES OF A PAPER'S BODY, handed to ESLint — so the rules over ESLint's LaTeX text read a paper
|
|
3
3
|
* whose body is in `sections/*.tex`, and report each finding at its own file and line (#144).
|
|
4
4
|
*
|
|
5
|
-
* The set is what TeX
|
|
6
|
-
* src/
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* The set is what TeX read, as the last build recorded it (`_build/sources.json`, `paperRecord` in
|
|
6
|
+
* src/paper-record.ts): `paper.tex` and the `.tex` files of the paper directory TeX read after
|
|
7
|
+
* `\\begin{document}`. Nothing here reads TeX source or looks for an include on disk. Not a glob over
|
|
8
|
+
* every `.tex`: a frozen version under `versions/`, a macro file the preamble includes and a scratch
|
|
9
|
+
* file nothing includes are not the paper. A paper with no record, or one it has changed since, is
|
|
10
|
+
* `paper.tex` alone — `paper/sources-fresh` (src/sources-rules.ts) says the rest went unlinted.
|
|
9
11
|
*
|
|
10
12
|
* Two blocks per paper, both relative to the paper's directory (`basePath`):
|
|
11
13
|
*
|
|
@@ -16,21 +18,32 @@
|
|
|
16
18
|
* for the rules whose verdict is the paper's (eslint-rules/paper-context.ts).
|
|
17
19
|
*
|
|
18
20
|
* ── ONLY `.tex` FILES ARE HANDED TO ESLint (#153) ────────────────────────────────
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
21
|
+
* TeX may read a body file of another extension: `\\input{paper.bbl}` in a paper that ships bibtex's
|
|
22
|
+
* output instead of a `.bib`, a plot exported as `.pgf`. Handed to ESLint, such a file got no
|
|
23
|
+
* language — the fragment block claims `.tex` only — and the JavaScript parser stopped at its first
|
|
24
|
+
* `%`. It is not given the LaTeX language either: the fragment rules judge and `--fix` the prose an
|
|
25
|
+
* author writes in that file, and a file of another extension is in practice a tool's output, whose
|
|
26
|
+
* words are someone else's (the titles of cited works) and whose fix the next run of the tool
|
|
27
|
+
* overwrites. TeX still reads it, and so does `readPaper`: the rules over the whole paper see it
|
|
28
|
+
* spliced into `paper.tex`, where TeX typesets it.
|
|
29
|
+
*
|
|
30
|
+
* ── NOR IS A `.tex` TeX WROTE ────────────────────────────────────────────────────
|
|
31
|
+
* A `filecontents` block may write a `.tex` the body `\\input`s (the record lists it as an input and
|
|
32
|
+
* as written, src/domain/tex-run.ts). Its words are the block's, which the author edits in the file
|
|
33
|
+
* holding the block; a fix made in the written copy is overwritten by an `[overwrite]` block on the next
|
|
34
|
+
* build, and splits the copy from the block without one. So it is read the way TeX reads it — spliced by
|
|
35
|
+
* `readPaper`, for the rules over the whole paper — and is not handed to ESLint as a file of its own.
|
|
27
36
|
*/
|
|
28
37
|
import { extname, join, relative } from "node:path";
|
|
29
38
|
import { PAPERLINT_SETTINGS } from "#eslint-rules/paper-context";
|
|
30
39
|
import { callerPath } from "./caller-path.ts";
|
|
31
40
|
import type { AbsolutePath } from "./domain/paths.ts";
|
|
41
|
+
import {
|
|
42
|
+
paperRecord,
|
|
43
|
+
recordedFiles,
|
|
44
|
+
type RecordReadDeps,
|
|
45
|
+
} from "./paper-record.ts";
|
|
32
46
|
import type { ConfigBlock } from "./rules-config.ts";
|
|
33
|
-
import { bodyFiles, type PaperDeps, type Unread } from "./tex-paper.ts";
|
|
34
47
|
|
|
35
48
|
/** The files the fragment block (`buildConfig`) claims: the only body files ESLint is handed. */
|
|
36
49
|
export const FRAGMENT_FILES: readonly string[] = ["**/*.tex"];
|
|
@@ -38,27 +51,31 @@ export const FRAGMENT_FILES: readonly string[] = ["**/*.tex"];
|
|
|
38
51
|
/** Whether a body file is one the fragment block claims — the extension its glob names. */
|
|
39
52
|
const isFragment = (file: string): boolean => extname(file) === ".tex";
|
|
40
53
|
|
|
41
|
-
/** One paper's main file and body files
|
|
54
|
+
/** One paper's main file and the body files of its last build. */
|
|
42
55
|
export interface PaperBody {
|
|
43
56
|
readonly dir: string;
|
|
44
57
|
readonly main: AbsolutePath;
|
|
45
|
-
/** The body files ESLint lints, each as a fragment: the `.tex` ones. */
|
|
58
|
+
/** The body files ESLint lints, each as a fragment: the `.tex` ones TeX did not write. None without a fresh record. */
|
|
46
59
|
readonly files: readonly AbsolutePath[];
|
|
47
|
-
readonly missing: readonly Unread[];
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
/** The body of each paper directory in `dirs` that has a `paper.tex`. */
|
|
51
63
|
export function paperBodies(
|
|
52
64
|
dirs: readonly string[],
|
|
53
|
-
deps:
|
|
65
|
+
deps: RecordReadDeps,
|
|
54
66
|
): readonly PaperBody[] {
|
|
55
67
|
return dirs.flatMap((dir) => {
|
|
56
68
|
const main = callerPath(join(dir, "paper.tex"));
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
69
|
+
if (!deps.files.isFile(main)) return [];
|
|
70
|
+
const record = paperRecord(dir, deps);
|
|
71
|
+
if (record.kind !== "fresh") return [{ dir, main, files: [] }];
|
|
72
|
+
const written = new Set(
|
|
73
|
+
record.record.written.map((w) => callerPath(join(dir, w))),
|
|
74
|
+
);
|
|
75
|
+
const files = recordedFiles(dir, record.record, "body").filter(
|
|
76
|
+
(f) => f !== main && isFragment(f) && !written.has(f),
|
|
77
|
+
);
|
|
78
|
+
return [{ dir, main, files }];
|
|
62
79
|
});
|
|
63
80
|
}
|
|
64
81
|
|
|
@@ -80,16 +97,3 @@ export const includeBlocks = (
|
|
|
80
97
|
},
|
|
81
98
|
];
|
|
82
99
|
});
|
|
83
|
-
|
|
84
|
-
/** One line per include that resolved nowhere: which text lint did not read, and why. */
|
|
85
|
-
export const unreadLines = (
|
|
86
|
-
bodies: readonly PaperBody[],
|
|
87
|
-
shown: (path: string) => string,
|
|
88
|
-
): readonly string[] =>
|
|
89
|
-
bodies.flatMap((b) =>
|
|
90
|
-
b.missing.map(
|
|
91
|
-
(m) =>
|
|
92
|
-
`${shown(b.main)}: lint did not read \`${m.target}\` (included by ${m.file}): ` +
|
|
93
|
-
"no such file in the paper's directory or paperlint's inputs",
|
|
94
|
-
),
|
|
95
|
-
);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { expect, it } from "vitest";
|
|
2
|
+
import { latexReader } from "./adapters/latex/index.ts";
|
|
3
|
+
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
4
|
+
import { sourcesCodec } from "./adapters/sources-record/index.ts";
|
|
5
|
+
import { paperProse } from "./paper-prose.ts";
|
|
6
|
+
import { builtFixture, builtPaper } from "../test/recorded-fixture.ts";
|
|
7
|
+
|
|
8
|
+
const SRC = [
|
|
9
|
+
"\\documentclass{article}",
|
|
10
|
+
"\\usepackage{amsmath} % a preamble word count of its own",
|
|
11
|
+
"\\title{A Title}",
|
|
12
|
+
"\\begin{document}",
|
|
13
|
+
"\\maketitle",
|
|
14
|
+
"\\section{Introduction}",
|
|
15
|
+
"We measure it~\\cite{a}. % a comment",
|
|
16
|
+
"",
|
|
17
|
+
"A second paragraph \\input{body}",
|
|
18
|
+
"\\begin{filecontents*}{refs.bib}",
|
|
19
|
+
"@misc{a, author = {Ada Lovelace}, title = {Never Prose}}",
|
|
20
|
+
"\\end{filecontents*}",
|
|
21
|
+
"\\end{document}",
|
|
22
|
+
].join("\n");
|
|
23
|
+
|
|
24
|
+
it("the body's prose, a blank line between passages, includes spliced; the preamble, comments, the inline .bib and the title stay out", () => {
|
|
25
|
+
// The build recorded TeX reading both files; the preamble holds no include.
|
|
26
|
+
const files = memoryFiles(
|
|
27
|
+
builtPaper(
|
|
28
|
+
"/p",
|
|
29
|
+
{ "paper.tex": SRC, "body.tex": "\\subsection{Method}\nends here." },
|
|
30
|
+
[
|
|
31
|
+
{ path: "paper.tex", role: "body" },
|
|
32
|
+
{ path: "body.tex", role: "body" },
|
|
33
|
+
],
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
expect(
|
|
37
|
+
paperProse("/p/paper.tex", SRC, {
|
|
38
|
+
files,
|
|
39
|
+
latex: latexReader,
|
|
40
|
+
codec: sourcesCodec,
|
|
41
|
+
}),
|
|
42
|
+
).toEqual({
|
|
43
|
+
// A citation stands as `[1]`, where it is set; a heading — here an included one — ends a passage.
|
|
44
|
+
body: "We measure it [1].\n\nA second paragraph\n\nends here.",
|
|
45
|
+
headings: ["Introduction", "Method"],
|
|
46
|
+
files: [
|
|
47
|
+
{ file: "paper.tex", text: SRC },
|
|
48
|
+
{ file: "body.tex", text: "\\subsection{Method}\nends here." },
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("a citation stands as `[1]` and a cross-reference as `1`, so the sentence still ends on them", () => {
|
|
54
|
+
const src =
|
|
55
|
+
"\\documentclass{article}\n\\begin{document}\nIt holds~\\cite{a}. It is shown (Section~\\ref{s}).\n\\end{document}\n";
|
|
56
|
+
expect(
|
|
57
|
+
paperProse("/p/paper.tex", src, {
|
|
58
|
+
files: memoryFiles({ "/p/paper.tex": src }),
|
|
59
|
+
latex: latexReader,
|
|
60
|
+
codec: sourcesCodec,
|
|
61
|
+
}).body,
|
|
62
|
+
).toBe("It holds [1]. It is shown (Section 1).");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("🔴 a .tex a filecontents block wrote and the body \\input's is prose once: spliced where TeX read it, the block's body no prose", () => {
|
|
66
|
+
// v29-generated-input: TeX recorded body.tex — written by the block, read after \begin{document}.
|
|
67
|
+
const files = builtFixture("v29-generated-input", "/p");
|
|
68
|
+
const src = files["/p/paper.tex"] ?? "";
|
|
69
|
+
const { body } = paperProse("/p/paper.tex", src, {
|
|
70
|
+
files: memoryFiles(files),
|
|
71
|
+
latex: latexReader,
|
|
72
|
+
codec: sourcesCodec,
|
|
73
|
+
});
|
|
74
|
+
expect(
|
|
75
|
+
body.match(/This sentence is typeset from a file a block wrote\./g),
|
|
76
|
+
).toHaveLength(1);
|
|
77
|
+
expect(body).toMatch(/^Before the generated file\. This sentence/);
|
|
78
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE PROSE OF A paper.tex AS PLAIN TEXT — what `grade-paper-writing/prose-lint.mjs` measures.
|
|
3
|
+
*
|
|
4
|
+
* prose-lint counts sentences and words, and every metric it prints is a fraction of the word
|
|
5
|
+
* count. Read as raw text, a `.tex` puts the preamble, the `%` comments and an inline `filecontents`
|
|
6
|
+
* bibliography into that count (8127 "words" against about 4636 real ones on one paper), so every
|
|
7
|
+
* rate comes out roughly halved. This reads the paper the way the `tex/register` rule does: the
|
|
8
|
+
* includes spliced in (`readPaper`), then the body's prose as passages (`bodyProse` — the abstract
|
|
9
|
+
* to the back matter, without headings, captions, floats or code), each passage's sentences
|
|
10
|
+
* (`bodySentences`). A blank line separates two passages, so no sentence runs across them.
|
|
11
|
+
*
|
|
12
|
+
* A citation stands as `[1]` and a cross-reference as `1`, where they are set — not left out:
|
|
13
|
+
* prose-lint judges a sentence that ENDS on one (a wasted stress position) and counts references
|
|
14
|
+
* to other parts of the paper (`(Section 1)`), and both are invisible in a sentence the marks
|
|
15
|
+
* were cut out of. Math is left out; prose-lint counts no formula.
|
|
16
|
+
*/
|
|
17
|
+
import { bodySentences } from "./domain/register.ts";
|
|
18
|
+
import { CITATION, MATH, REFERENCE } from "./domain/sentences.ts";
|
|
19
|
+
import { runText } from "./domain/tex-document.ts";
|
|
20
|
+
import { readPaper, type ProseDeps } from "./tex-paper.ts";
|
|
21
|
+
|
|
22
|
+
/** A sentence's text with each mark as a reader would see it set, and math left out. */
|
|
23
|
+
const rendered = (raw: string): string =>
|
|
24
|
+
raw
|
|
25
|
+
.replaceAll(CITATION, "[1]")
|
|
26
|
+
.replaceAll(REFERENCE, "1")
|
|
27
|
+
.replaceAll(MATH, "")
|
|
28
|
+
.replace(/\s+/gu, " ")
|
|
29
|
+
.trim();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A paper's body prose, one passage per paragraph, its section titles in document order, and the
|
|
33
|
+
* text of each file it is made of — `paper.tex` and every file it includes, once, named as the
|
|
34
|
+
* include names it — for what the body leaves out, such as captions.
|
|
35
|
+
*/
|
|
36
|
+
export interface PaperProse {
|
|
37
|
+
readonly body: string;
|
|
38
|
+
readonly headings: readonly string[];
|
|
39
|
+
readonly files: readonly { readonly file: string; readonly text: string }[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The prose and the headings of the paper whose main file is `filename` with the text `src`. */
|
|
43
|
+
export function paperProse(
|
|
44
|
+
filename: string,
|
|
45
|
+
src: string,
|
|
46
|
+
deps: ProseDeps,
|
|
47
|
+
): PaperProse {
|
|
48
|
+
const { text, segments } = readPaper(filename, src, deps);
|
|
49
|
+
const passages = deps.latex
|
|
50
|
+
.bodyProse(text)
|
|
51
|
+
.map((p) =>
|
|
52
|
+
bodySentences([p])
|
|
53
|
+
.map((s) => rendered(s.raw))
|
|
54
|
+
.join(" "),
|
|
55
|
+
)
|
|
56
|
+
.filter((p) => p !== "");
|
|
57
|
+
return {
|
|
58
|
+
body: passages.join("\n\n"),
|
|
59
|
+
headings: deps.latex
|
|
60
|
+
.headings(text)
|
|
61
|
+
.filter((h) => h.command.kind === "heading" && h.argument === "title")
|
|
62
|
+
.map((h) => runText(h.title).replace(/\s+/g, " ").trim()),
|
|
63
|
+
files: [...new Map(segments.map((s) => [s.file, s.source])).entries()].map(
|
|
64
|
+
([file, text]) => ({ file, text }),
|
|
65
|
+
),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `paperRecord(dir)` — the one answer to "what did the last build read, and is that still the paper?",
|
|
3
|
+
* over files held in memory — and the two things the rules take from a fresh record: the files of a
|
|
4
|
+
* role, and the databases bibtex opened as the bibtex reader reads them.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import { bibReader } from "./adapters/bibtex/index.ts";
|
|
8
|
+
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
9
|
+
import { sourcesCodec } from "./adapters/sources-record/index.ts";
|
|
10
|
+
import type { BibText } from "./domain/paper-sources.ts";
|
|
11
|
+
import { sha256Hex } from "./domain/sha256.ts";
|
|
12
|
+
import {
|
|
13
|
+
serializeSourcesRecord,
|
|
14
|
+
type SourcesRecord,
|
|
15
|
+
} from "./domain/sources-record.ts";
|
|
16
|
+
import { paperRecord, recordedFiles } from "./paper-record.ts";
|
|
17
|
+
import { recordedBibliography } from "./recorded-bibliography.ts";
|
|
18
|
+
|
|
19
|
+
const DIR = "/work/paper";
|
|
20
|
+
const bytes = (s: string) => new TextEncoder().encode(s);
|
|
21
|
+
const digest = (s: string) => sha256Hex(bytes(s));
|
|
22
|
+
|
|
23
|
+
const TEX = "\\documentclass{article}\n";
|
|
24
|
+
const BIB = "@misc{a, title={A}, author={Doe, Jane}, year={2020}}\n";
|
|
25
|
+
|
|
26
|
+
const RECORD: SourcesRecord = {
|
|
27
|
+
schema: 1,
|
|
28
|
+
inputs: [
|
|
29
|
+
{ path: "paper.tex", role: "body" },
|
|
30
|
+
{ path: "macros.tex", role: "preamble" },
|
|
31
|
+
{ path: "sections/intro.tex", role: "body" },
|
|
32
|
+
],
|
|
33
|
+
written: [],
|
|
34
|
+
bibdata: ["refs"],
|
|
35
|
+
bibtex: {
|
|
36
|
+
ran: true,
|
|
37
|
+
databases: ["refs.bib"],
|
|
38
|
+
keys: ["a"],
|
|
39
|
+
exit: 0,
|
|
40
|
+
errors: [],
|
|
41
|
+
},
|
|
42
|
+
sha256: {
|
|
43
|
+
"paper.tex": digest(TEX),
|
|
44
|
+
"macros.tex": digest("m"),
|
|
45
|
+
"sections/intro.tex": digest("i"),
|
|
46
|
+
"refs.bib": digest(BIB),
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The paper directory after a build wrote `record`: every file as it was hashed. */
|
|
51
|
+
const built = (record: SourcesRecord = RECORD) =>
|
|
52
|
+
memoryFiles({
|
|
53
|
+
[`${DIR}/_build/sources.json`]: serializeSourcesRecord(record),
|
|
54
|
+
[`${DIR}/paper.tex`]: TEX,
|
|
55
|
+
[`${DIR}/macros.tex`]: "m",
|
|
56
|
+
[`${DIR}/sections/intro.tex`]: "i",
|
|
57
|
+
[`${DIR}/refs.bib`]: BIB,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("paperRecord", () => {
|
|
61
|
+
it("a paper no build has recorded has none", () => {
|
|
62
|
+
expect(
|
|
63
|
+
paperRecord(DIR, {
|
|
64
|
+
files: memoryFiles({ [`${DIR}/paper.tex`]: TEX }),
|
|
65
|
+
codec: sourcesCodec,
|
|
66
|
+
}),
|
|
67
|
+
).toEqual({ kind: "none", unreadable: null });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("a record that cannot be read has none, and says why", () => {
|
|
71
|
+
const files = built();
|
|
72
|
+
files.map.set(`${DIR}/_build/sources.json`, bytes("{"));
|
|
73
|
+
expect(paperRecord(DIR, { files, codec: sourcesCodec })).toEqual({
|
|
74
|
+
kind: "none",
|
|
75
|
+
unreadable: "sources.json is not JSON",
|
|
76
|
+
});
|
|
77
|
+
files.map.set(`${DIR}/_build/sources.json`, bytes('{"schema":2}'));
|
|
78
|
+
expect(paperRecord(DIR, { files, codec: sourcesCodec })).toEqual({
|
|
79
|
+
kind: "none",
|
|
80
|
+
unreadable: "sources.json is schema 2, this paperlint reads schema 1",
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("a record whose every file is as hashed is fresh", () => {
|
|
85
|
+
expect(paperRecord(DIR, { files: built(), codec: sourcesCodec })).toEqual({
|
|
86
|
+
kind: "fresh",
|
|
87
|
+
record: RECORD,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("a file edited since the build makes it stale, and names the file", () => {
|
|
92
|
+
const files = built();
|
|
93
|
+
files.map.set(`${DIR}/sections/intro.tex`, bytes("edited"));
|
|
94
|
+
expect(paperRecord(DIR, { files, codec: sourcesCodec })).toEqual({
|
|
95
|
+
kind: "stale",
|
|
96
|
+
changed: [{ path: "sections/intro.tex", change: "edited" }],
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("a file deleted, a database edited: every change is named, in the record's order", () => {
|
|
101
|
+
const files = built();
|
|
102
|
+
files.map.delete(`${DIR}/macros.tex`);
|
|
103
|
+
files.map.set(`${DIR}/refs.bib`, bytes("@misc{b}"));
|
|
104
|
+
expect(paperRecord(DIR, { files, codec: sourcesCodec })).toEqual({
|
|
105
|
+
kind: "stale",
|
|
106
|
+
changed: [
|
|
107
|
+
{ path: "macros.tex", change: "deleted" },
|
|
108
|
+
{ path: "refs.bib", change: "edited" },
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const keysOf = (bib: BibText): readonly string[] =>
|
|
115
|
+
bib.entries.map((e) => e.key);
|
|
116
|
+
|
|
117
|
+
const DEPS = { codec: sourcesCodec, bib: bibReader };
|
|
118
|
+
|
|
119
|
+
describe("what a rule takes from a record", () => {
|
|
120
|
+
it("the files of a role, as absolute paths, in the order TeX first read them", () => {
|
|
121
|
+
expect(recordedFiles(DIR, RECORD, "body")).toEqual([
|
|
122
|
+
`${DIR}/paper.tex`,
|
|
123
|
+
`${DIR}/sections/intro.tex`,
|
|
124
|
+
]);
|
|
125
|
+
expect(recordedFiles(DIR, RECORD, "preamble")).toEqual([
|
|
126
|
+
`${DIR}/macros.tex`,
|
|
127
|
+
]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("the databases bibtex opened, read as the bibtex reader reads them, a block's copy marked written, a missing one named", () => {
|
|
131
|
+
const record: SourcesRecord = {
|
|
132
|
+
...RECORD,
|
|
133
|
+
written: ["gen.bib"],
|
|
134
|
+
bibtex: {
|
|
135
|
+
ran: true,
|
|
136
|
+
databases: ["refs.bib", "gen.bib", "gone.bib", "./refs.bib"],
|
|
137
|
+
keys: [],
|
|
138
|
+
exit: 0,
|
|
139
|
+
errors: [],
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
const files = built(record);
|
|
143
|
+
files.map.set(`${DIR}/gen.bib`, bytes("@misc{g, title={G}}"));
|
|
144
|
+
const r = recordedBibliography(DIR, { ...DEPS, files });
|
|
145
|
+
expect(
|
|
146
|
+
r.kind === "recorded"
|
|
147
|
+
? {
|
|
148
|
+
databases: r.databases.map((d) => [
|
|
149
|
+
d.name,
|
|
150
|
+
d.written,
|
|
151
|
+
keysOf(d.bib),
|
|
152
|
+
]),
|
|
153
|
+
unread: r.unread,
|
|
154
|
+
path: r.databases[0]?.bib.path,
|
|
155
|
+
}
|
|
156
|
+
: r,
|
|
157
|
+
).toEqual({
|
|
158
|
+
databases: [
|
|
159
|
+
["refs.bib", false, ["a"]],
|
|
160
|
+
["gen.bib", true, ["g"]],
|
|
161
|
+
],
|
|
162
|
+
unread: ["gone.bib"],
|
|
163
|
+
path: `${DIR}/refs.bib`,
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("a build that ran no bibtex opened no database", () => {
|
|
168
|
+
const record: SourcesRecord = { ...RECORD, bibtex: { ran: false } };
|
|
169
|
+
expect(
|
|
170
|
+
recordedBibliography(DIR, { ...DEPS, files: built(record) }),
|
|
171
|
+
).toMatchObject({ kind: "recorded", databases: [], unread: [] });
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `paperRecord(dir)` — what every consumer of the build's record asks first: is there a record of
|
|
3
|
+
* this paper, and is it still about the files on disk? The answer is one of three states, and the
|
|
4
|
+
* rules that depend on the record are silent in two of them (docs/design/paper-sources.md §1):
|
|
5
|
+
*
|
|
6
|
+
* none no `_build/sources.json`, or one this paperlint cannot read — the paper was not built
|
|
7
|
+
* stale a file the build read has other bytes now (`changed` names them)
|
|
8
|
+
* fresh every file is as the build hashed it; the record is TeX's answer for this paper
|
|
9
|
+
*
|
|
10
|
+
* Reads through the `Files` port alone; the one paperlint-specific question — which files make a
|
|
11
|
+
* record stale — is `changesSince` (`src/domain/sources-record.ts`).
|
|
12
|
+
*/
|
|
13
|
+
import { resolve } from "node:path";
|
|
14
|
+
import { callerPath } from "./caller-path.ts";
|
|
15
|
+
import type { AbsolutePath } from "./domain/paths.ts";
|
|
16
|
+
import { sha256Hex, type Sha256 } from "./domain/sha256.ts";
|
|
17
|
+
import {
|
|
18
|
+
changesSince,
|
|
19
|
+
type Change,
|
|
20
|
+
type InputRole,
|
|
21
|
+
type SourcesRecord,
|
|
22
|
+
} from "./domain/sources-record.ts";
|
|
23
|
+
import type { Files } from "./ports/files.ts";
|
|
24
|
+
import type { SourcesCodec } from "./ports/sources-codec.ts";
|
|
25
|
+
import { sourcesPath } from "./sources-record.ts";
|
|
26
|
+
|
|
27
|
+
export type PaperRecord =
|
|
28
|
+
/** No record, or one that cannot be read (`unreadable` says why; null for no file). */
|
|
29
|
+
| { readonly kind: "none"; readonly unreadable: string | null }
|
|
30
|
+
| { readonly kind: "stale"; readonly changed: readonly Change[] }
|
|
31
|
+
| { readonly kind: "fresh"; readonly record: SourcesRecord };
|
|
32
|
+
|
|
33
|
+
/** The digest of the file at `path` in the paper directory now, or null when there is none. */
|
|
34
|
+
const digestOf =
|
|
35
|
+
(dir: string, files: Files) =>
|
|
36
|
+
(path: string): Sha256 | null => {
|
|
37
|
+
const b = files.readBytes(callerPath(resolve(dir, path)));
|
|
38
|
+
return b === null ? null : sha256Hex(b);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** What reading a record needs: the disk, and the schema its text is parsed with. */
|
|
42
|
+
export interface RecordReadDeps {
|
|
43
|
+
readonly files: Files;
|
|
44
|
+
readonly codec: SourcesCodec;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function paperRecord(dir: string, deps: RecordReadDeps): PaperRecord {
|
|
48
|
+
const raw = deps.files.readBytes(callerPath(sourcesPath(dir)));
|
|
49
|
+
if (raw === null) return { kind: "none", unreadable: null };
|
|
50
|
+
const parsed = deps.codec.parse(new TextDecoder().decode(raw));
|
|
51
|
+
if (!parsed.ok) return { kind: "none", unreadable: parsed.why };
|
|
52
|
+
const changed = changesSince(parsed.record, digestOf(dir, deps.files));
|
|
53
|
+
return changed.length === 0
|
|
54
|
+
? { kind: "fresh", record: parsed.record }
|
|
55
|
+
: { kind: "stale", changed };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The files of `role` the record lists, as absolute paths, in the order TeX first read them. */
|
|
59
|
+
export const recordedFiles = (
|
|
60
|
+
dir: string,
|
|
61
|
+
record: SourcesRecord,
|
|
62
|
+
role: InputRole,
|
|
63
|
+
): readonly AbsolutePath[] =>
|
|
64
|
+
record.inputs
|
|
65
|
+
.filter((i) => i.role === role)
|
|
66
|
+
.map((i) => callerPath(resolve(dir, i.path)));
|
|
@@ -233,7 +233,7 @@ describe("paperlint lint — the venue preset's rules", () => {
|
|
|
233
233
|
});
|
|
234
234
|
});
|
|
235
235
|
|
|
236
|
-
it("🔴 an unbuilt agenticdev paper:
|
|
236
|
+
it("🔴 an unbuilt agenticdev paper: two warnings (pdf/measured, paper/sources-fresh), and no balance error", async () => {
|
|
237
237
|
const root = project({
|
|
238
238
|
"papers/a/paperlint.json": JSON.stringify({
|
|
239
239
|
extends: "paperlint:agenticdev",
|
|
@@ -245,7 +245,10 @@ describe("paperlint lint — the venue preset's rules", () => {
|
|
|
245
245
|
});
|
|
246
246
|
const r = await lint(root);
|
|
247
247
|
expect(r.code).toBe(0);
|
|
248
|
-
expect(rulesIn(r.out, "a")).toEqual([
|
|
248
|
+
expect(rulesIn(r.out, "a")).toEqual([
|
|
249
|
+
"pdf/measured",
|
|
250
|
+
"paper/sources-fresh",
|
|
251
|
+
]);
|
|
249
252
|
});
|
|
250
253
|
});
|
|
251
254
|
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bibliographyAt(path)` — the one answer to "a path a person gave → the bibliography to read", for
|
|
3
|
+
* every script that takes one (`extract-ref-facts`, `bib-authors`, `verify-cites`). A `.bib` is read
|
|
4
|
+
* alone, as named; a `.tex`, or a directory holding `paper.tex`, is the bibliography the last build's
|
|
5
|
+
* bibtex opened for that paper — TeX's answer, from `_build/sources.json` (docs/design/paper-sources.md
|
|
6
|
+
* §1), never read out of TeX source.
|
|
7
|
+
*/
|
|
8
|
+
import { dirname, extname, join } from "node:path";
|
|
9
|
+
import { callerPath } from "./caller-path.ts";
|
|
10
|
+
import { MAIN_FILE, type BibText } from "./domain/paper-sources.ts";
|
|
11
|
+
import type { AbsolutePath } from "./domain/paths.ts";
|
|
12
|
+
import { err, ok, type Result } from "./domain/result.ts";
|
|
13
|
+
import { describeChanges } from "./domain/sources-record.ts";
|
|
14
|
+
import type { PaperRecord } from "./paper-record.ts";
|
|
15
|
+
import {
|
|
16
|
+
authoredTexts,
|
|
17
|
+
recordedBibliography,
|
|
18
|
+
type RecordedDeps,
|
|
19
|
+
} from "./recorded-bibliography.ts";
|
|
20
|
+
|
|
21
|
+
const decoded = (b: Uint8Array): string => new TextDecoder().decode(b);
|
|
22
|
+
|
|
23
|
+
// ── a path a person gave → the bibliography to read ─────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/** What a path a person gave comes to: where the paper is, and the texts to read. */
|
|
26
|
+
export interface PathBibliography {
|
|
27
|
+
/** The `.bib`'s directory, the `.tex`'s, or the directory named. */
|
|
28
|
+
readonly paperDir: AbsolutePath;
|
|
29
|
+
readonly texts: readonly [BibText, ...BibText[]];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Why a path a person gave has no bibliography to read. One vocabulary for every caller. */
|
|
33
|
+
export type BibliographyUnread =
|
|
34
|
+
/** A `.bib` or `.tex` named that is not there. */
|
|
35
|
+
| { readonly kind: "missing"; readonly path: AbsolutePath }
|
|
36
|
+
/** A file that is neither a `.bib` nor a `.tex`. */
|
|
37
|
+
| { readonly kind: "not-bib-or-tex"; readonly path: AbsolutePath }
|
|
38
|
+
/** Not a file, and no `paper.tex` in it. */
|
|
39
|
+
| { readonly kind: "no-paper"; readonly path: AbsolutePath }
|
|
40
|
+
/** A paper with no current record of a build: what bibtex read is not known (`state` says why). */
|
|
41
|
+
| {
|
|
42
|
+
readonly kind: "not-built";
|
|
43
|
+
readonly dir: AbsolutePath;
|
|
44
|
+
readonly state: Exclude<PaperRecord, { readonly kind: "fresh" }>;
|
|
45
|
+
}
|
|
46
|
+
/** A paper whose last build's bibtex read no database, or whose databases are not on disk now. */
|
|
47
|
+
| {
|
|
48
|
+
readonly kind: "no-database";
|
|
49
|
+
readonly dir: AbsolutePath;
|
|
50
|
+
/** The databases bibtex opened: none when it ran none. */
|
|
51
|
+
readonly opened: readonly string[];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A paper whose last build's bibtex also opened databases the paper directory does not hold — found
|
|
55
|
+
* on its search path (BIBINPUTS). Reading the others alone would leave every entry of these out.
|
|
56
|
+
*/
|
|
57
|
+
| {
|
|
58
|
+
readonly kind: "outside-paper";
|
|
59
|
+
readonly dir: AbsolutePath;
|
|
60
|
+
/** As bibtex names them. */
|
|
61
|
+
readonly unread: readonly [string, ...string[]];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** The texts of the paper in `dir` the last build's bibtex read, or why there are none. */
|
|
65
|
+
function ofPaper(
|
|
66
|
+
dir: AbsolutePath,
|
|
67
|
+
deps: RecordedDeps,
|
|
68
|
+
): Result<PathBibliography, BibliographyUnread> {
|
|
69
|
+
const r = recordedBibliography(dir, deps);
|
|
70
|
+
if (r.kind === "unrecorded")
|
|
71
|
+
return err({ kind: "not-built", dir, state: r.record });
|
|
72
|
+
const [first, ...rest] = authoredTexts(dir, r, deps);
|
|
73
|
+
if (first === undefined)
|
|
74
|
+
return err({
|
|
75
|
+
kind: "no-database",
|
|
76
|
+
dir,
|
|
77
|
+
opened: r.record.bibtex.ran ? r.record.bibtex.databases : [],
|
|
78
|
+
});
|
|
79
|
+
// Some databases read and some not is no bibliography: a script would report on part of it as on all.
|
|
80
|
+
const [outside, ...more] = r.unread;
|
|
81
|
+
return outside === undefined
|
|
82
|
+
? ok({ paperDir: dir, texts: [first, ...rest] })
|
|
83
|
+
: err({ kind: "outside-paper", dir, unread: [outside, ...more] });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* THE answer to "a path a person gave → the bibliography to read", for every script that takes one: a
|
|
88
|
+
* `.bib` is read alone, as named; a `.tex`, or a directory holding `paper.tex`, is the bibliography the
|
|
89
|
+
* last build's bibtex opened for that paper (`_build/sources.json`) — never a guess by file name, and
|
|
90
|
+
* never read out of TeX source. A paper with no current record is refused: run `npx paperlint build`
|
|
91
|
+
* first, and so is one whose bibtex also opened a database outside the paper directory — a part of the
|
|
92
|
+
* bibliography is not given as the whole. Anything else is refused in the words `bibliographyUnreadWhy`
|
|
93
|
+
* gives.
|
|
94
|
+
*/
|
|
95
|
+
export function bibliographyAt(
|
|
96
|
+
path: AbsolutePath,
|
|
97
|
+
deps: RecordedDeps,
|
|
98
|
+
): Result<PathBibliography, BibliographyUnread> {
|
|
99
|
+
const ext = extname(path).toLowerCase();
|
|
100
|
+
if (ext === ".bib" || ext === ".tex") {
|
|
101
|
+
const bytes = deps.files.readBytes(path);
|
|
102
|
+
if (bytes === null) return err({ kind: "missing", path });
|
|
103
|
+
return ext === ".bib"
|
|
104
|
+
? ok({
|
|
105
|
+
paperDir: callerPath(dirname(path)),
|
|
106
|
+
texts: [deps.bib.readFile(path, decoded(bytes))],
|
|
107
|
+
})
|
|
108
|
+
: ofPaper(callerPath(dirname(path)), deps);
|
|
109
|
+
}
|
|
110
|
+
if (deps.files.isFile(path)) return err({ kind: "not-bib-or-tex", path });
|
|
111
|
+
return deps.files.readBytes(callerPath(join(path, MAIN_FILE))) === null
|
|
112
|
+
? err({ kind: "no-paper", path })
|
|
113
|
+
: ofPaper(path, deps);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const BUILD = "run `npx paperlint build` first";
|
|
117
|
+
|
|
118
|
+
/** Why a paper has no current record, in words a person can act on. */
|
|
119
|
+
function notBuiltWhy(
|
|
120
|
+
dir: AbsolutePath,
|
|
121
|
+
s: Extract<BibliographyUnread, { readonly kind: "not-built" }>["state"],
|
|
122
|
+
): string {
|
|
123
|
+
switch (s.kind) {
|
|
124
|
+
case "none":
|
|
125
|
+
return s.unreadable === null
|
|
126
|
+
? `${dir} has not been built — ${BUILD}, which records the databases bibtex reads`
|
|
127
|
+
: `the last build's record of ${dir} cannot be used (${s.unreadable}) — ${BUILD}`;
|
|
128
|
+
case "stale":
|
|
129
|
+
return `${dir} changed since the last build (${describeChanges(s.changed)}) — ${BUILD}`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** One sentence: why `bibliographyAt` refused, naming the path and what to give instead. */
|
|
134
|
+
export function bibliographyUnreadWhy(e: BibliographyUnread): string {
|
|
135
|
+
switch (e.kind) {
|
|
136
|
+
case "missing":
|
|
137
|
+
return `${e.path} does not exist — nowhere to take a bibliography from`;
|
|
138
|
+
case "not-bib-or-tex":
|
|
139
|
+
return `${e.path} is neither a .bib nor a .tex — name the paper's directory, its .tex, or a .bib`;
|
|
140
|
+
case "no-paper":
|
|
141
|
+
return `no paper.tex in ${e.path} — name the paper's .tex, or a .bib to read it alone`;
|
|
142
|
+
case "not-built":
|
|
143
|
+
return notBuiltWhy(e.dir, e.state);
|
|
144
|
+
case "no-database":
|
|
145
|
+
return e.opened.length === 0
|
|
146
|
+
? `bibtex read no database in the last build of ${e.dir} — there is nothing to read`
|
|
147
|
+
: `bibtex opened ${e.opened.join(", ")} in the last build of ${e.dir}, and none of them is on disk now`;
|
|
148
|
+
case "outside-paper":
|
|
149
|
+
return `bibtex opened ${e.unread.join(", ")} in the last build of ${e.dir} from outside it (its search path, BIBINPUTS), and paperlint reads only the paper's own databases — copy it into ${e.dir}, or name each .bib to read it alone`;
|
|
150
|
+
}
|
|
151
|
+
}
|