paperlint 8.0.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +9 -25
- package/CONTRIBUTING.md +5 -4
- package/README.md +4 -3
- package/dist/adapters/bibtex/index.d.ts +3 -0
- package/dist/adapters/bibtex/index.d.ts.map +1 -0
- package/dist/adapters/bibtex/index.js +103 -0
- package/dist/adapters/bibtex/index.js.map +1 -0
- package/dist/adapters/latex/filecontents.d.ts +33 -0
- package/dist/adapters/latex/filecontents.d.ts.map +1 -0
- package/dist/adapters/latex/filecontents.js +65 -0
- package/dist/adapters/latex/filecontents.js.map +1 -0
- package/dist/adapters/latex/includes.d.ts +3 -3
- package/dist/adapters/latex/includes.d.ts.map +1 -1
- package/dist/adapters/latex/includes.js +18 -10
- package/dist/adapters/latex/includes.js.map +1 -1
- package/dist/adapters/latex/index.d.ts.map +1 -1
- package/dist/adapters/latex/index.js +2 -0
- package/dist/adapters/latex/index.js.map +1 -1
- package/dist/adapters/latex/nodes.d.ts +12 -0
- package/dist/adapters/latex/nodes.d.ts.map +1 -1
- package/dist/adapters/latex/nodes.js +39 -0
- package/dist/adapters/latex/nodes.js.map +1 -1
- package/dist/adapters/latex/parse.d.ts +7 -1
- package/dist/adapters/latex/parse.d.ts.map +1 -1
- package/dist/adapters/latex/parse.js +55 -6
- package/dist/adapters/latex/parse.js.map +1 -1
- package/dist/adapters/memory/index.d.ts +3 -0
- package/dist/adapters/memory/index.d.ts.map +1 -1
- package/dist/adapters/memory/index.js +9 -0
- package/dist/adapters/memory/index.js.map +1 -1
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +27 -24
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/adapters/sources-record/index.d.ts +9 -0
- package/dist/adapters/sources-record/index.d.ts.map +1 -0
- package/dist/adapters/sources-record/index.js +68 -0
- package/dist/adapters/sources-record/index.js.map +1 -0
- package/dist/adapters/tex-output/blg.d.ts +9 -0
- package/dist/adapters/tex-output/blg.d.ts.map +1 -0
- package/dist/adapters/tex-output/blg.js +20 -0
- package/dist/adapters/tex-output/blg.js.map +1 -0
- package/dist/adapters/tex-output/fls.d.ts +8 -0
- package/dist/adapters/tex-output/fls.d.ts.map +1 -0
- package/dist/adapters/tex-output/fls.js +16 -0
- package/dist/adapters/tex-output/fls.js.map +1 -0
- package/dist/adapters/tex-output/index.d.ts +7 -0
- package/dist/adapters/tex-output/index.d.ts.map +1 -0
- package/dist/adapters/tex-output/index.js +7 -0
- package/dist/adapters/tex-output/index.js.map +1 -0
- package/dist/bib-rules.d.ts +47 -0
- package/dist/bib-rules.d.ts.map +1 -0
- package/dist/bib-rules.js +127 -0
- package/dist/bib-rules.js.map +1 -0
- package/dist/build.d.ts +29 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +40 -24
- package/dist/build.js.map +1 -1
- package/dist/claim-provenance.d.ts +2 -2
- package/dist/cli.d.ts +6 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +30 -26
- package/dist/cli.js.map +1 -1
- package/dist/domain/paper-source.d.ts +1 -10
- package/dist/domain/paper-source.d.ts.map +1 -1
- package/dist/domain/paper-source.js +34 -24
- package/dist/domain/paper-source.js.map +1 -1
- package/dist/domain/paper-sources.d.ts +72 -0
- package/dist/domain/paper-sources.d.ts.map +1 -0
- package/dist/domain/paper-sources.js +18 -0
- package/dist/domain/paper-sources.js.map +1 -0
- package/dist/domain/sources-record.d.ts +102 -0
- package/dist/domain/sources-record.d.ts.map +1 -0
- package/dist/domain/sources-record.js +38 -0
- package/dist/domain/sources-record.js.map +1 -0
- package/dist/domain/tex-run.d.ts +25 -0
- package/dist/domain/tex-run.d.ts.map +1 -0
- package/dist/domain/tex-run.js +79 -0
- package/dist/domain/tex-run.js.map +1 -0
- package/dist/eslint-rules/paper-context.d.ts.map +1 -1
- package/dist/eslint-rules/paper-context.js +4 -3
- package/dist/eslint-rules/paper-context.js.map +1 -1
- package/dist/eslint-rules/paper-typography.d.ts +0 -9
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
- package/dist/eslint-rules/paper-typography.js +12 -5
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/latex-log.d.ts +10 -0
- package/dist/latex-log.d.ts.map +1 -1
- package/dist/latex-log.js +10 -0
- package/dist/latex-log.js.map +1 -1
- package/dist/latex-loop.d.ts +2 -0
- package/dist/latex-loop.d.ts.map +1 -1
- package/dist/latex-loop.js.map +1 -1
- package/dist/package-dirs.d.ts.map +1 -1
- package/dist/package-dirs.js +3 -2
- package/dist/package-dirs.js.map +1 -1
- package/dist/paper-includes.d.ts +4 -7
- package/dist/paper-includes.d.ts.map +1 -1
- package/dist/paper-includes.js +29 -21
- package/dist/paper-includes.js.map +1 -1
- package/dist/paper-prose.d.ts +2 -2
- package/dist/paper-record.d.ts +25 -0
- package/dist/paper-record.d.ts.map +1 -0
- package/dist/paper-record.js +39 -0
- package/dist/paper-record.js.map +1 -0
- package/dist/paper-sources.d.ts +66 -0
- package/dist/paper-sources.d.ts.map +1 -0
- package/dist/paper-sources.js +92 -0
- package/dist/paper-sources.js.map +1 -0
- package/dist/ports/bib-reader.d.ts +18 -0
- package/dist/ports/bib-reader.d.ts.map +1 -0
- package/dist/ports/bib-reader.js +2 -0
- package/dist/ports/bib-reader.js.map +1 -0
- package/dist/ports/check-references.d.ts +9 -4
- package/dist/ports/check-references.d.ts.map +1 -1
- package/dist/ports/latex.d.ts +15 -0
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/ports/sources-codec.d.ts +11 -0
- package/dist/ports/sources-codec.d.ts.map +1 -0
- package/dist/ports/sources-codec.js +2 -0
- package/dist/ports/sources-codec.js.map +1 -0
- package/dist/ports/tex-output.d.ts +17 -0
- package/dist/ports/tex-output.d.ts.map +1 -0
- package/dist/ports/tex-output.js +2 -0
- package/dist/ports/tex-output.js.map +1 -0
- package/dist/recorded-bibliography.d.ts +89 -0
- package/dist/recorded-bibliography.d.ts.map +1 -0
- package/dist/recorded-bibliography.js +136 -0
- package/dist/recorded-bibliography.js.map +1 -0
- package/dist/reference-rules.d.ts +5 -5
- package/dist/reference-rules.d.ts.map +1 -1
- package/dist/reference-rules.js +150 -77
- package/dist/reference-rules.js.map +1 -1
- package/dist/references.d.ts +71 -16
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +110 -26
- package/dist/references.js.map +1 -1
- package/dist/register-bands.d.ts +3 -3
- package/dist/register-bands.d.ts.map +1 -1
- package/dist/register-bands.js.map +1 -1
- package/dist/register.d.ts +2 -2
- package/dist/sources-record.d.ts +38 -0
- package/dist/sources-record.d.ts.map +1 -0
- package/dist/sources-record.js +123 -0
- package/dist/sources-record.js.map +1 -0
- package/dist/sources-rules.d.ts +44 -0
- package/dist/sources-rules.d.ts.map +1 -0
- package/dist/sources-rules.js +76 -0
- package/dist/sources-rules.js.map +1 -0
- package/dist/tex-paper.d.ts +10 -15
- package/dist/tex-paper.d.ts.map +1 -1
- package/dist/tex-paper.js +24 -40
- package/dist/tex-paper.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +3 -3
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js.map +1 -1
- package/docs/configuration.md +12 -4
- package/docs/design/paper-sources.md +197 -0
- package/docs/package-shape-options.md +1 -1
- package/docs/references.md +34 -8
- package/docs/rules/bib/reachable-entry.md +64 -0
- package/docs/rules/paper/sources-fresh.md +81 -0
- package/docs/rules.md +62 -57
- package/eslint-rules/paper-context.ts +4 -3
- package/eslint-rules/paper-typography.test.mjs +12 -72
- package/eslint-rules/paper-typography.ts +13 -6
- package/eslint.config.mjs +22 -0
- package/fixtures/build-e2e/aidc/paper.tex +1 -1
- package/fixtures/build-e2e/anon-ack/paper.tex +1 -1
- package/fixtures/build-e2e/anon-author/paper.tex +1 -1
- package/fixtures/build-e2e/anon-bib/paper.tex +1 -1
- package/fixtures/build-e2e/anon-href/paper.tex +1 -1
- package/fixtures/build-e2e/anon-macro/paper.tex +1 -1
- package/fixtures/build-e2e/anon-pdfauthor/paper.tex +1 -1
- package/fixtures/paper-sources/p1/paper.tex +30 -0
- package/fixtures/paper-sources/p1/refs.bib +6 -0
- package/fixtures/paper-sources/p1/sections/dead.tex +6 -0
- package/fixtures/paper-sources/p1/sections/intro.tex +7 -0
- package/fixtures/paper-sources/p1/sections/parked.tex +6 -0
- package/fixtures/paper-sources/p1/tex-truth.json +37 -0
- package/fixtures/paper-sources/p1/versions/2026-01-01-review.tex +4 -0
- package/fixtures/paper-sources/v1-stale/paper.tex +14 -0
- package/fixtures/paper-sources/v1-stale/refs.bib +6 -0
- package/fixtures/paper-sources/v1-stale/tex-truth.json +32 -0
- package/fixtures/paper-sources/v10-ifthenelse/anon.bib +1 -0
- package/fixtures/paper-sources/v10-ifthenelse/paper.tex +9 -0
- package/fixtures/paper-sources/v10-ifthenelse/real.bib +1 -0
- package/fixtures/paper-sources/v10-ifthenelse/tex-truth.json +28 -0
- package/fixtures/paper-sources/v11-two-blocks/paper.tex +12 -0
- package/fixtures/paper-sources/v11-two-blocks/tex-truth.json +30 -0
- package/fixtures/paper-sources/v13-percent-text/paper.tex +6 -0
- package/fixtures/paper-sources/v13-percent-text/refs.bib +3 -0
- package/fixtures/paper-sources/v13-percent-text/tex-truth.json +30 -0
- package/fixtures/paper-sources/v15-after-end/paper.tex +6 -0
- package/fixtures/paper-sources/v15-after-end/refs.bib +1 -0
- package/fixtures/paper-sources/v15-after-end/tex-truth.json +17 -0
- package/fixtures/paper-sources/v16-included-block/bibblock.tex +3 -0
- package/fixtures/paper-sources/v16-included-block/paper.tex +7 -0
- package/fixtures/paper-sources/v16-included-block/refs.bib +1 -0
- package/fixtures/paper-sources/v16-included-block/tex-truth.json +35 -0
- package/fixtures/paper-sources/v17-include-order/bibsetup.tex +1 -0
- package/fixtures/paper-sources/v17-include-order/first.bib +1 -0
- package/fixtures/paper-sources/v17-include-order/later.bib +1 -0
- package/fixtures/paper-sources/v17-include-order/paper.tex +7 -0
- package/fixtures/paper-sources/v17-include-order/tex-truth.json +40 -0
- package/fixtures/paper-sources/v2-overwrite/paper.tex +14 -0
- package/fixtures/paper-sources/v2-overwrite/refs.bib +6 -0
- package/fixtures/paper-sources/v2-overwrite/tex-truth.json +30 -0
- package/fixtures/paper-sources/v22-parked-thebibliography/paper.tex +7 -0
- package/fixtures/paper-sources/v22-parked-thebibliography/tex-truth.json +17 -0
- package/fixtures/paper-sources/v23-parked-include/old.bib +1 -0
- package/fixtures/paper-sources/v23-parked-include/paper.tex +7 -0
- package/fixtures/paper-sources/v23-parked-include/parked.tex +1 -0
- package/fixtures/paper-sources/v23-parked-include/refs.bib +1 -0
- package/fixtures/paper-sources/v23-parked-include/tex-truth.json +28 -0
- package/fixtures/paper-sources/v24-quoted-fields/paper.tex +6 -0
- package/fixtures/paper-sources/v24-quoted-fields/refs.bib +5 -0
- package/fixtures/paper-sources/v24-quoted-fields/tex-truth.json +32 -0
- package/fixtures/paper-sources/v25-string-differs/paper.tex +10 -0
- package/fixtures/paper-sources/v25-string-differs/refs.bib +2 -0
- package/fixtures/paper-sources/v25-string-differs/tex-truth.json +32 -0
- package/fixtures/paper-sources/v26-comment-in-names/one.bib +1 -0
- package/fixtures/paper-sources/v26-comment-in-names/paper.tex +7 -0
- package/fixtures/paper-sources/v26-comment-in-names/tex-truth.json +32 -0
- package/fixtures/paper-sources/v26-comment-in-names/two.bib +1 -0
- package/fixtures/paper-sources/v27-shared-string/abbreviations.bib +2 -0
- package/fixtures/paper-sources/v27-shared-string/paper.tex +6 -0
- package/fixtures/paper-sources/v27-shared-string/references.bib +6 -0
- package/fixtures/paper-sources/v27-shared-string/tex-truth.json +31 -0
- package/fixtures/paper-sources/v28-shared-string-block/abbreviations.bib +2 -0
- package/fixtures/paper-sources/v28-shared-string-block/paper.tex +14 -0
- package/fixtures/paper-sources/v28-shared-string-block/tex-truth.json +33 -0
- package/fixtures/paper-sources/v29-generated-input/paper.tex +9 -0
- package/fixtures/paper-sources/v29-generated-input/tex-truth.json +24 -0
- package/fixtures/paper-sources/v3-declared/paper.bib +6 -0
- package/fixtures/paper-sources/v3-declared/paper.tex +6 -0
- package/fixtures/paper-sources/v3-declared/tex-truth.json +28 -0
- package/fixtures/paper-sources/v30-name-next-line/paper.tex +14 -0
- package/fixtures/paper-sources/v30-name-next-line/tex-truth.json +30 -0
- package/fixtures/paper-sources/v4-commented/paper.tex +14 -0
- package/fixtures/paper-sources/v4-commented/refs.bib +6 -0
- package/fixtures/paper-sources/v4-commented/tex-truth.json +28 -0
- package/fixtures/paper-sources/v5-percent-entry/paper.tex +20 -0
- package/fixtures/paper-sources/v5-percent-entry/tex-truth.json +37 -0
- package/fixtures/paper-sources/v6-unclosed/paper.tex +6 -0
- package/fixtures/paper-sources/v6-unclosed/refs.bib +5 -0
- package/fixtures/paper-sources/v6-unclosed/tex-truth.json +37 -0
- package/fixtures/paper-sources/v8-jobname/paper.tex +9 -0
- package/fixtures/paper-sources/v8-jobname/tex-truth.json +30 -0
- package/fixtures/paper-sources/v9-two-bibliographies/one.bib +1 -0
- package/fixtures/paper-sources/v9-two-bibliographies/paper.tex +7 -0
- package/fixtures/paper-sources/v9-two-bibliographies/tex-truth.json +35 -0
- package/fixtures/paper-sources/v9-two-bibliographies/two.bib +1 -0
- package/fixtures/paper-typography/clean-paper/paper.tex +1 -1
- package/fixtures/paper-typography/messy-paper/paper.tex +1 -1
- package/package.json +2 -10
- package/scripts/check.test.ts +23 -1
- package/scripts/check.ts +7 -3
- package/scripts/coverage-config.test.ts +19 -10
- package/scripts/layer-legacy-frozen.test.ts +41 -2
- package/scripts/layer-legacy-frozen.ts +31 -2
- package/skills/grade-paper-writing/SKILL.md +4 -3
- package/skills/grade-paper-writing/SKILL.md.spec.ts +3 -2
- package/skills/grade-paper-writing/prose-lint.mjs +11 -2
- package/skills/grade-paper-writing/prose-lint.test.ts +52 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +83 -33
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +67 -115
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +128 -121
- package/skills/verify-citations/SKILL.md +7 -4
- package/skills/verify-citations/SKILL.md.spec.ts +6 -3
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +17 -19
- package/skills/verify-citations/scripts/bib-authors.cli.test.ts +129 -46
- package/skills/verify-citations/scripts/bib-authors.d.mts +22 -2
- package/skills/verify-citations/scripts/bib-authors.mjs +57 -111
- package/skills/verify-citations/scripts/verify-cites.cli.test.ts +16 -0
- package/skills/verify-citations/scripts/verify-cites.d.mts +19 -3
- package/skills/verify-citations/scripts/verify-cites.mjs +42 -108
- package/skills/verify-citations/scripts/verify-cites.test.mjs +23 -36
- package/src/adapters/bibtex/index.test.ts +163 -0
- package/src/adapters/bibtex/index.ts +155 -0
- package/src/adapters/latex/filecontents.ts +114 -0
- package/src/adapters/latex/includes.ts +18 -10
- package/src/adapters/latex/index.test.ts +73 -0
- package/src/adapters/latex/index.ts +2 -0
- package/src/adapters/latex/nodes.ts +64 -0
- package/src/adapters/latex/parse.test.ts +38 -0
- package/src/adapters/latex/parse.ts +67 -6
- package/src/adapters/memory/index.ts +17 -0
- package/src/adapters/references/index.test.ts +75 -21
- package/src/adapters/references/index.ts +37 -32
- package/src/adapters/sources-record/index.test.ts +87 -0
- package/src/adapters/sources-record/index.ts +82 -0
- package/src/adapters/tex-output/blg.ts +27 -0
- package/src/adapters/tex-output/fls.ts +24 -0
- package/src/adapters/tex-output/index.test.ts +171 -0
- package/src/adapters/tex-output/index.ts +12 -0
- package/src/bib-rules.test.ts +356 -0
- package/src/bib-rules.ts +228 -0
- package/src/bibliography-at.test.ts +286 -0
- package/src/build.test.ts +210 -0
- package/src/build.ts +100 -35
- package/src/claim-provenance.test.ts +33 -7
- package/src/claim-provenance.ts +2 -2
- package/src/cli.harness.mjs +20 -10
- package/src/cli.test.ts +14 -10
- package/src/cli.ts +37 -28
- package/src/domain/paper-source.test.ts +58 -20
- package/src/domain/paper-source.ts +38 -42
- package/src/domain/paper-sources.ts +94 -0
- package/src/domain/sources-record.test.ts +71 -0
- package/src/domain/sources-record.ts +136 -0
- package/src/domain/tex-run.test.ts +228 -0
- package/src/domain/tex-run.ts +139 -0
- package/src/heading-case.test.ts +24 -10
- package/src/latex-log.ts +20 -0
- package/src/latex-loop.harness.ts +1 -0
- package/src/latex-loop.ts +2 -0
- package/src/linted-set.test.ts +9 -1
- package/src/new-paper.harness.mjs +3 -2
- package/src/new-paper.test.ts +9 -5
- package/src/package-dirs.ts +3 -2
- package/src/paper-includes.test.ts +157 -38
- package/src/paper-includes.ts +39 -35
- package/src/paper-prose.test.ts +34 -5
- package/src/paper-prose.ts +2 -2
- package/src/paper-record.test.ts +173 -0
- package/src/paper-record.ts +66 -0
- package/src/paper-settings-commands.test.ts +5 -2
- package/src/paper-sources.ts +151 -0
- package/src/ports/bib-reader.ts +27 -0
- package/src/ports/check-references.ts +7 -3
- package/src/ports/latex.ts +19 -0
- package/src/ports/sources-codec.ts +11 -0
- package/src/ports/tex-output.ts +17 -0
- package/src/recorded-bibliography.ts +297 -0
- package/src/reference-rules.ts +217 -104
- package/src/references.test.ts +520 -177
- package/src/references.ts +185 -37
- package/src/register-bands.test.ts +5 -1
- package/src/register-bands.ts +3 -5
- package/src/register.test.ts +6 -1
- package/src/register.ts +2 -2
- package/src/rule-cases.test.ts +68 -12
- package/src/rule-docs.test.ts +0 -1
- package/src/sources-record.test.ts +308 -0
- package/src/sources-record.ts +231 -0
- package/src/sources-rules.test.ts +152 -0
- package/src/sources-rules.ts +117 -0
- package/src/tex-paper.test.ts +148 -78
- package/src/tex-paper.ts +31 -61
- package/src/tex-venue-rules.test.ts +36 -8
- package/src/tex-venue-rules.ts +3 -5
- package/dist/eslint-rules/bib-reachable-entry.d.ts +0 -22
- package/dist/eslint-rules/bib-reachable-entry.d.ts.map +0 -1
- package/dist/eslint-rules/bib-reachable-entry.js +0 -75
- package/dist/eslint-rules/bib-reachable-entry.js.map +0 -1
- package/eslint-rules/bib-reachable-entry.ts +0 -76
package/scripts/check.ts
CHANGED
|
@@ -30,8 +30,10 @@
|
|
|
30
30
|
*
|
|
31
31
|
* ── NO `--fast` FLAG, ON PURPOSE ───────────────────────────────────────────────────────────
|
|
32
32
|
* A subset flag re-creates the exact failure above: the cheap half gets run and reported as
|
|
33
|
-
* "the gates".
|
|
34
|
-
*
|
|
33
|
+
* "the gates". Every gate runs with CI=true, as in CI, so a local check is the CI check: the e2e
|
|
34
|
+
* steps are strict (`test/e2e/need.ts`) — a missing prerequisite (no TeX) FAILS with what to
|
|
35
|
+
* install, as it does in CI — and a test that still skips is named in the tail, never counted as a
|
|
36
|
+
* pass. "I ran check" then means one thing.
|
|
35
37
|
*/
|
|
36
38
|
import { spawnSync } from "node:child_process";
|
|
37
39
|
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
@@ -293,7 +295,9 @@ function runEach(
|
|
|
293
295
|
cwd: ROOT,
|
|
294
296
|
stdio: "inherit",
|
|
295
297
|
encoding: "utf8",
|
|
296
|
-
|
|
298
|
+
// CI=true, as CI runs every gate: libraries read it (typescript-eslint infers a single run and
|
|
299
|
+
// parses from disk), so without it a local check is not the CI check (2026-10-07).
|
|
300
|
+
env: { ...process.env, CI: "true", PATH: BIN_FIRST_PATH },
|
|
297
301
|
});
|
|
298
302
|
// 🔴 The exit code is read from the command itself, never from a pipe. `cmd | tail && …`
|
|
299
303
|
// reports the FILTER's status, which is almost always zero — that is how a red harness
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import assert from "node:assert/strict";
|
|
22
22
|
import { spawnSync } from "node:child_process";
|
|
23
23
|
import { globSync, readFileSync } from "node:fs";
|
|
24
|
-
import { dirname, join, matchesGlob, resolve } from "node:path";
|
|
24
|
+
import { basename, dirname, join, matchesGlob, resolve } from "node:path";
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
26
|
import yaml from "js-yaml";
|
|
27
27
|
import ts from "typescript";
|
|
@@ -146,16 +146,23 @@ test("every typeOnly exclusion has no runtime code", () => {
|
|
|
146
146
|
});
|
|
147
147
|
|
|
148
148
|
test("every evalOnly exclusion is imported only by an eval or another evalOnly module", () => {
|
|
149
|
-
const
|
|
149
|
+
const only = expand(CONFIG.evalOnly);
|
|
150
|
+
const evalOnly = new Set(only.map((f) => join(ROOT, f)));
|
|
151
|
+
// Any import of one names it — `./x.mjs`, `#lib/x` — so a source whose text holds none of their
|
|
152
|
+
// base names imports none of them, and only the others are parsed.
|
|
153
|
+
const names = only.map((f) => basename(f).replace(/\.[^.]+$/, ""));
|
|
150
154
|
const sources = globSync("**/*.{mjs,ts}", {
|
|
151
155
|
cwd: ROOT,
|
|
152
156
|
exclude: (p) => p === "node_modules" || p === "dist" || p === "coverage",
|
|
153
157
|
});
|
|
154
158
|
const importers = sources.flatMap((f) => {
|
|
155
159
|
const abs = join(ROOT, f);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
160
|
+
const text = readFileSync(abs, "utf8");
|
|
161
|
+
return names.some((n) => text.includes(n))
|
|
162
|
+
? importedPaths(abs, text)
|
|
163
|
+
.filter((target) => evalOnly.has(target))
|
|
164
|
+
.map(() => f)
|
|
165
|
+
: [];
|
|
159
166
|
});
|
|
160
167
|
const outsiders = importers.filter(
|
|
161
168
|
(f) => !f.endsWith(".eval.mjs") && !evalOnly.has(join(ROOT, f)),
|
|
@@ -258,11 +265,13 @@ test("no measured source carries a coverage-ignore comment", () => {
|
|
|
258
265
|
// The scan covers exactly what c8 measures: the same set, derived from the tracked files.
|
|
259
266
|
assert.deepEqual(files, trackedSources().filter(isMeasured));
|
|
260
267
|
assert.ok(files.length > 0);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
),
|
|
265
|
-
|
|
268
|
+
// An ignore comment's text matches IGNORE, so a source whose text does not holds none, and only
|
|
269
|
+
// the others have their comments read off the tree.
|
|
270
|
+
const offenders = files
|
|
271
|
+
.map((f) => [f, readFileSync(join(ROOT, f), "utf8")] as const)
|
|
272
|
+
.filter(([, text]) => IGNORE.test(text))
|
|
273
|
+
.filter(([f, text]) => comments(f, text).some((c) => IGNORE.test(c)))
|
|
274
|
+
.map(([f]) => f);
|
|
266
275
|
assert.deepEqual(offenders, []);
|
|
267
276
|
});
|
|
268
277
|
|
|
@@ -9,6 +9,7 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
import { test } from "vitest";
|
|
10
10
|
import {
|
|
11
11
|
checkFrozen,
|
|
12
|
+
filesToLint,
|
|
12
13
|
FROZEN_FILE,
|
|
13
14
|
judge,
|
|
14
15
|
main,
|
|
@@ -164,8 +165,9 @@ test("main: problems are listed and fail; a clean count passes", async () => {
|
|
|
164
165
|
);
|
|
165
166
|
});
|
|
166
167
|
|
|
167
|
-
//
|
|
168
|
-
//
|
|
168
|
+
// Loads the real config (about 2.5 s) and lints the src/ files that carry a directive (21 of 223),
|
|
169
|
+
// measured 2026-10-07: 5.5 s alone, 10–11 s under c8 — past vitest's 5 s default, so the budget is
|
|
170
|
+
// stated here. Linting all of src/ took 22–23 s under c8 and timed out with the suite in parallel.
|
|
169
171
|
test(
|
|
170
172
|
"checkFrozen on this repository: nothing new, nothing grown — and on a tree with no src/, ESLint refuses loudly",
|
|
171
173
|
{
|
|
@@ -186,6 +188,43 @@ test(
|
|
|
186
188
|
},
|
|
187
189
|
);
|
|
188
190
|
|
|
191
|
+
test("filesToLint: every src/ file whose text carries an eslint-disable directive, in any form — and no other", () => {
|
|
192
|
+
const root = useTempDir("layer-frozen-files-");
|
|
193
|
+
writeTree(root, {
|
|
194
|
+
"src/line.ts": "export const a = 1; // eslint-disable-line no-console\n",
|
|
195
|
+
"src/next.ts":
|
|
196
|
+
"// eslint-disable-next-line no-console\nexport const b = 1;\n",
|
|
197
|
+
"src/block.ts": "/* eslint-disable no-console */\nexport const c = 1;\n",
|
|
198
|
+
"src/deep/inner.ts":
|
|
199
|
+
"// eslint-disable-next-line x -- why\nexport const d = 1;\n",
|
|
200
|
+
"src/plain.ts": "export const e = 1;\n",
|
|
201
|
+
"lib/outside.ts": "// eslint-disable-next-line no-console\n",
|
|
202
|
+
});
|
|
203
|
+
assert.deepEqual(filesToLint(root), [
|
|
204
|
+
"src/block.ts",
|
|
205
|
+
"src/deep/inner.ts",
|
|
206
|
+
"src/line.ts",
|
|
207
|
+
"src/next.ts",
|
|
208
|
+
]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("checkFrozen with the real ESLint on a tree: a new legacy directive in a file the list does not name still fails", async () => {
|
|
212
|
+
const root = useTempDir("layer-frozen-real-");
|
|
213
|
+
writeTree(root, {
|
|
214
|
+
[FROZEN_FILE]: JSON.stringify({ files: {} }),
|
|
215
|
+
"eslint.config.mjs":
|
|
216
|
+
'export default [{ files: ["src/**/*.ts"], rules: { "no-restricted-globals": ["error", "process"] } }];\n',
|
|
217
|
+
"src/plain.ts": "export const quiet = 1;\n",
|
|
218
|
+
"src/new.ts": `// eslint-disable-next-line no-restricted-globals -- ${IO}\nexport const env = process;\n`,
|
|
219
|
+
});
|
|
220
|
+
const r = await checkFrozen(root);
|
|
221
|
+
assert.deepEqual(r.counts, { "src/new.ts": { "no-restricted-globals": 1 } });
|
|
222
|
+
assert.match(
|
|
223
|
+
r.problems.join("\n"),
|
|
224
|
+
/^src\/new\.ts: a new legacy layer exemption/,
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
189
228
|
test("checkFrozen: a lint that returns no result reports that nothing was counted", async () => {
|
|
190
229
|
const root = useTempDir("layer-frozen-empty-");
|
|
191
230
|
writeTree(root, {
|
|
@@ -32,10 +32,15 @@
|
|
|
32
32
|
* counted by construction — and a range directive (`/* eslint-disable … *\/` around a multi-line
|
|
33
33
|
* import) counts exactly what it silences.
|
|
34
34
|
*
|
|
35
|
+
* ESLint lints only the files that can carry one (`filesToLint`): a suppressed finding comes from a
|
|
36
|
+
* directive, and every directive's text holds `eslint-disable` (`-line`, `-next-line`, the block
|
|
37
|
+
* form). The bulk-suppressions file is not applied here. So the narrowing drops only files that
|
|
38
|
+
* cannot add to the count — 21 of 223 today — and the count still comes from ESLint's report.
|
|
39
|
+
*
|
|
35
40
|
* Run: `node scripts/layer-legacy-frozen.ts` (also part of `npm run check`)
|
|
36
41
|
*/
|
|
37
42
|
import { isMain } from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
38
|
-
import { readFileSync } from "node:fs";
|
|
43
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
39
44
|
import { dirname, join, relative, sep } from "node:path";
|
|
40
45
|
import { fileURLToPath } from "node:url";
|
|
41
46
|
import { ESLint } from "eslint";
|
|
@@ -195,11 +200,35 @@ function layerLint(cwd: string): ESLint {
|
|
|
195
200
|
});
|
|
196
201
|
}
|
|
197
202
|
|
|
203
|
+
/**
|
|
204
|
+
* The `src/**\/*.ts` files of `root` whose text holds `eslint-disable` — every file that can carry a
|
|
205
|
+
* suppressed finding — relative to `root`, POSIX separators, sorted.
|
|
206
|
+
*/
|
|
207
|
+
export function filesToLint(root: string): string[] {
|
|
208
|
+
const src = join(root, "src");
|
|
209
|
+
const all = readdirSync(src, { recursive: true, withFileTypes: true })
|
|
210
|
+
.filter((e) => e.isFile() && e.name.endsWith(".ts"))
|
|
211
|
+
.map((e) => join(e.parentPath, e.name));
|
|
212
|
+
return all
|
|
213
|
+
.filter((f) => readFileSync(f, "utf8").includes("eslint-disable"))
|
|
214
|
+
.map((f) => relative(root, f).split(sep).join("/"))
|
|
215
|
+
.sort();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The files ESLint lints: those that can carry a suppression, or — when there are none, or no
|
|
220
|
+
* `src/` — the whole glob, so ESLint itself refuses a tree with no source.
|
|
221
|
+
*/
|
|
222
|
+
const lintTargets = (root: string): string[] => {
|
|
223
|
+
const narrowed = existsSync(join(root, "src")) ? filesToLint(root) : [];
|
|
224
|
+
return narrowed.length > 0 ? narrowed : ["src/**/*.ts"];
|
|
225
|
+
};
|
|
226
|
+
|
|
198
227
|
/** Lint `src/` with the repository's configuration and run the whole check. */
|
|
199
228
|
export async function checkFrozen(
|
|
200
229
|
root: string = ROOT,
|
|
201
230
|
{
|
|
202
|
-
lint = (cwd: string) => layerLint(cwd).lintFiles(
|
|
231
|
+
lint = (cwd: string) => layerLint(cwd).lintFiles(lintTargets(cwd)),
|
|
203
232
|
}: { lint?: (cwd: string) => Promise<readonly Suppressed[]> } = {},
|
|
204
233
|
): Promise<FrozenCheck> {
|
|
205
234
|
const data = FrozenFile.parse(
|
|
@@ -4,7 +4,7 @@ description: Use when asking "does this paper read well?" / "is the writing any
|
|
|
4
4
|
allowed-tools: [Read, Write, Edit, Grep, Glob, Agent, Skill]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<!-- vigiles:sha256:
|
|
7
|
+
<!-- vigiles:sha256:21c8899648c9b223 compiled from skills/grade-paper-writing/SKILL.md.spec.ts -->
|
|
8
8
|
|
|
9
9
|
# grade-paper-writing — grade how the paper READS, then fix it sentence by sentence
|
|
10
10
|
|
|
@@ -222,8 +222,9 @@ is what happened.
|
|
|
222
222
|
node ${CLAUDE_SKILL_DIR}/prose-lint.mjs --headings <paper-dir>/paper.tex # the headings as a set
|
|
223
223
|
```
|
|
224
224
|
|
|
225
|
-
It measures the prose a reader sees —
|
|
226
|
-
`.bib` — so its rates are the paper's, not the source file's.
|
|
225
|
+
It measures the prose a reader sees — the files the last build read spliced in, no preamble,
|
|
226
|
+
comments, floats or inline `.bib` — so its rates are the paper's, not the source file's. Run
|
|
227
|
+
`npx paperlint build` first: with no current build record it reads `paper.tex` alone and says so. The skill is what must ACT on it.
|
|
227
228
|
Its thresholds are sourced; a FLAG means the text sits above a published human or machine
|
|
228
229
|
baseline, which is a defect with a number attached, not an opinion.
|
|
229
230
|
Two entries in `THRESHOLDS` exist because this skill previously ran five times without either
|
|
@@ -228,8 +228,9 @@ is what happened.
|
|
|
228
228
|
node \${CLAUDE_SKILL_DIR}/prose-lint.mjs --headings <paper-dir>/paper.tex # the headings as a set
|
|
229
229
|
\`\`\`
|
|
230
230
|
|
|
231
|
-
It measures the prose a reader sees —
|
|
232
|
-
\`.bib\` — so its rates are the paper's, not the source file's.
|
|
231
|
+
It measures the prose a reader sees — the files the last build read spliced in, no preamble,
|
|
232
|
+
comments, floats or inline \`.bib\` — so its rates are the paper's, not the source file's. Run
|
|
233
|
+
\`npx paperlint build\` first: with no current build record it reads \`paper.tex\` alone and says so. The skill is what must ACT on it.
|
|
233
234
|
Its thresholds are sourced; a FLAG means the text sits above a published human or machine
|
|
234
235
|
baseline, which is a defect with a number attached, not an opinion.
|
|
235
236
|
Two entries in \`THRESHOLDS\` exist because this skill previously ran five times without either
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* `8127 words` against ~4636 real ones — and every metric here is a fraction of that count, so all
|
|
12
12
|
* of them came out understated by about half and printed `ok`. `paperProse` (src/paper-prose.ts)
|
|
13
13
|
* gives the body as the `tex/register` rule reads it: includes spliced, the abstract to the back
|
|
14
|
-
* matter, no headings, captions, floats, code or citation marks.
|
|
14
|
+
* matter, no headings, captions, floats, code or citation marks. The files spliced in are the ones the
|
|
15
|
+
* paper's last build recorded TeX reading (`_build/sources.json`); a paper with no current record is
|
|
16
|
+
* read as `paper.tex` alone, and the run says so on stderr rather than print rates of half a paper.
|
|
15
17
|
*
|
|
16
18
|
* A `.txt` is the text of the rendered PDF — the page a reviewer actually reads.
|
|
17
19
|
*
|
|
@@ -21,8 +23,10 @@
|
|
|
21
23
|
* Advisory: it prints findings; `--flags-only` exits with code 1 if anything was found.
|
|
22
24
|
*/
|
|
23
25
|
import { paperProse } from "#src/paper-prose";
|
|
26
|
+
import { paperRecord } from "#src/paper-record";
|
|
24
27
|
import { latexReader } from "#src/adapters/latex/index";
|
|
25
28
|
import { nodeFiles } from "#src/adapters/node/index";
|
|
29
|
+
import { sourcesCodec } from "#src/adapters/sources-record/index";
|
|
26
30
|
import { texToMdast } from "#eslint-rules/latex-language";
|
|
27
31
|
|
|
28
32
|
const THRESHOLDS = {
|
|
@@ -481,9 +485,14 @@ const flagsOnly = args.includes("--flags-only");
|
|
|
481
485
|
let flagged = 0;
|
|
482
486
|
for (const f of args.filter((a) => !a.startsWith("--"))) {
|
|
483
487
|
const raw = fs.readFileSync(f, "utf8");
|
|
488
|
+
const deps = { files: nodeFiles, latex: latexReader, codec: sourcesCodec };
|
|
484
489
|
const tex = f.endsWith(".tex")
|
|
485
|
-
? paperProse(path.resolve(f), raw,
|
|
490
|
+
? paperProse(path.resolve(f), raw, deps)
|
|
486
491
|
: null;
|
|
492
|
+
if (tex && paperRecord(path.dirname(path.resolve(f)), deps).kind !== "fresh")
|
|
493
|
+
console.error(
|
|
494
|
+
`prose-lint: ${f} has no current build record (_build/sources.json), so only paper.tex was read, not the files it includes — run \`npx paperlint build\` first`,
|
|
495
|
+
);
|
|
487
496
|
// 🔴 NOTHING LEFT IS NOT "CLEAN". A body that strips to no words used to be analysed anyway: every
|
|
488
497
|
// metric is a fraction of the word count, so the report printed NaN, FLAGged three metrics and
|
|
489
498
|
// exited 0. Refused — no number is better than a meaningless one. The test is on sentences: a
|
|
@@ -8,6 +8,8 @@ import { join } from "node:path";
|
|
|
8
8
|
import { dirname } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { expect, test } from "vitest";
|
|
11
|
+
import { cpSync } from "node:fs";
|
|
12
|
+
import { layRecord, recordedTree } from "../../test/recorded-fixture.ts";
|
|
11
13
|
import { runNode, useTempDir, writeTree } from "../../test/support.ts";
|
|
12
14
|
|
|
13
15
|
const SCRIPT = join(dirname(fileURLToPath(import.meta.url)), "prose-lint.mjs");
|
|
@@ -17,16 +19,17 @@ const SCRIPT = join(dirname(fileURLToPath(import.meta.url)), "prose-lint.mjs");
|
|
|
17
19
|
*/
|
|
18
20
|
const SPAWN_LATEX_MS = 60_000;
|
|
19
21
|
/** An accepted paper whose bibliography sits inline in `filecontents`, beside a long preamble. */
|
|
20
|
-
const
|
|
22
|
+
const ACCEPTED_DIR = join(
|
|
21
23
|
dirname(fileURLToPath(import.meta.url)),
|
|
22
24
|
"..",
|
|
23
25
|
"..",
|
|
24
26
|
"fixtures",
|
|
25
27
|
"accepted-papers",
|
|
26
28
|
"agenticdev-acm26",
|
|
27
|
-
"paper.tex",
|
|
28
29
|
);
|
|
29
30
|
const root = useTempDir("prose-lint-");
|
|
31
|
+
/** The accepted paper as a build left it: a copy with TeX's record laid at `_build/sources.json`. */
|
|
32
|
+
const ACCEPTED = join(root, "accepted", "paper.tex");
|
|
30
33
|
|
|
31
34
|
const LONG_CAPTION_SENTENCE = Array.from(
|
|
32
35
|
{ length: 45 },
|
|
@@ -37,7 +40,7 @@ const LONG_CAPTION_SENTENCE = Array.from(
|
|
|
37
40
|
const tex = (body: string): string =>
|
|
38
41
|
`\\documentclass{article}\n\\begin{document}\n${body}\n\\end{document}\n`;
|
|
39
42
|
|
|
40
|
-
|
|
43
|
+
const TREE = {
|
|
41
44
|
"p/paper.tex": tex(
|
|
42
45
|
"\\section{Introduction}\nIn this paper we present a linter. It might possibly help.\n\\input{figures/fig1}\n\\input{figures/fig2}\n\\input{figures/fig4}\n\\input{figures/fig5}",
|
|
43
46
|
),
|
|
@@ -66,7 +69,36 @@ writeTree(root, {
|
|
|
66
69
|
"page.txt":
|
|
67
70
|
"A rendered page. It was hyphen-\nated across a line.\n\n12\n\fNext page text here.\nReferences\n[1] cut here.\n",
|
|
68
71
|
"notes.rtf": "not a paper",
|
|
69
|
-
}
|
|
72
|
+
};
|
|
73
|
+
/** What each paper's last build read: its paper.tex, and the files it includes. */
|
|
74
|
+
const READ: Readonly<Record<string, readonly string[]>> = {
|
|
75
|
+
p: [
|
|
76
|
+
"paper.tex",
|
|
77
|
+
"figures/fig1.tex",
|
|
78
|
+
"figures/fig2.tex",
|
|
79
|
+
"figures/fig4.tex",
|
|
80
|
+
"figures/fig5.tex",
|
|
81
|
+
],
|
|
82
|
+
clean: ["paper.tex"],
|
|
83
|
+
inline: ["paper.tex", "sec/results.tex"],
|
|
84
|
+
commented: ["paper.tex"],
|
|
85
|
+
"ends-on-marks": ["paper.tex"],
|
|
86
|
+
empty: ["paper.tex"],
|
|
87
|
+
};
|
|
88
|
+
writeTree(
|
|
89
|
+
root,
|
|
90
|
+
Object.entries(READ).reduce<Record<string, string>>(
|
|
91
|
+
(tree, [dir, files]) =>
|
|
92
|
+
recordedTree(
|
|
93
|
+
tree,
|
|
94
|
+
dir,
|
|
95
|
+
files.map((path) => ({ path, role: "body" as const })),
|
|
96
|
+
),
|
|
97
|
+
TREE,
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
cpSync(ACCEPTED_DIR, join(root, "accepted"), { recursive: true });
|
|
101
|
+
layRecord(join(root, "accepted"), join(ACCEPTED_DIR, "tex-truth.json"));
|
|
70
102
|
|
|
71
103
|
test("no arguments: the usage line, exit 0", () => {
|
|
72
104
|
assert.deepEqual(runNode(SCRIPT), {
|
|
@@ -157,6 +189,22 @@ test("--flags-only: a paper with no figures directory is clean, exit 0 and silen
|
|
|
157
189
|
);
|
|
158
190
|
});
|
|
159
191
|
|
|
192
|
+
test("🔴 a paper no build has recorded is read as paper.tex alone, and the run says so", () => {
|
|
193
|
+
writeTree(root, {
|
|
194
|
+
"unbuilt/paper.tex": tex("Short and plain. Nothing else."),
|
|
195
|
+
});
|
|
196
|
+
const file = join(root, "unbuilt", "paper.tex");
|
|
197
|
+
const r = runNode(SCRIPT, ["--flags-only", file]);
|
|
198
|
+
assert.deepEqual(
|
|
199
|
+
{ status: r.status, stdout: r.stdout, stderr: r.stderr },
|
|
200
|
+
{
|
|
201
|
+
status: 0,
|
|
202
|
+
stdout: "",
|
|
203
|
+
stderr: `prose-lint: ${file} has no current build record (_build/sources.json), so only paper.tex was read, not the files it includes — run \`npx paperlint build\` first\n`,
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
160
208
|
test("a .tex with no prose in its body is refused with exit 2, not reported as NaN", () => {
|
|
161
209
|
const file = join(root, "empty", "paper.tex");
|
|
162
210
|
assert.deepEqual(runNode(SCRIPT, [file]), {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 🔴 THREE MEASUREMENTS HERE — THESE ARE THREE DEFECTS THAT LIVED THE ENTIRE LIFE OF THE PREDECESSOR. Each
|
|
10
10
|
* is pinned by an assertion on a REAL repository file, not a fixture, because all three
|
|
11
11
|
* held exactly because nobody ever passed the real file:
|
|
12
|
-
* 1. `refs.bib` was
|
|
12
|
+
* 1. `refs.bib` was never opened;
|
|
13
13
|
* 2. the regex `.bib` required `}` on a separate line and gave 0 entries on both our files;
|
|
14
14
|
* 3. in `.bib` an author is written "LastName, FirstName", but comparison took the last token.
|
|
15
15
|
*
|
|
@@ -39,11 +39,18 @@ import {
|
|
|
39
39
|
import { tmpdir } from "node:os";
|
|
40
40
|
import { dirname, join } from "node:path";
|
|
41
41
|
import { fileURLToPath } from "node:url";
|
|
42
|
+
import { bibReader } from "#src/adapters/bibtex/index";
|
|
43
|
+
import { absolutePath } from "#src/domain/paths";
|
|
44
|
+
import { sha256Hex } from "#src/domain/sha256";
|
|
45
|
+
import { serializeSourcesRecord } from "#src/domain/sources-record";
|
|
42
46
|
import { consumerRoot } from "./consumer.mjs";
|
|
43
47
|
|
|
44
48
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
45
49
|
const ROOT = consumerRoot();
|
|
46
50
|
const X = await import(join(HERE, "extract-ref-facts.mjs"));
|
|
51
|
+
/** The facts' entries of a BibTeX text, read by the one bibtex reader. */
|
|
52
|
+
const factsOf = (text) =>
|
|
53
|
+
X.factEntries(bibReader.readFile(absolutePath("/p/refs.bib"), text));
|
|
47
54
|
|
|
48
55
|
// 🔴 PAPERS ROOT — FROM THE DECLARATION, NOT BY THE FIRST CONSUMER'S DIRECTORY NAME (12.09.2026).
|
|
49
56
|
// `package.json` → `paperlint.papersDir`, default `papers`. The same key is read by
|
|
@@ -70,38 +77,71 @@ const REAL_PAPERS = existsSync(PAPERS_ROOT)
|
|
|
70
77
|
: [];
|
|
71
78
|
|
|
72
79
|
const TMP = realpathSync(mkdtempSync(join(tmpdir(), "extract-ref-facts-")));
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* What `paperlint build` leaves in a paper's folder (`_build/sources.json`): bibtex opened `refs.bib`.
|
|
83
|
+
* The paper is read through that record and refused without it, so every paper here is built first.
|
|
84
|
+
*/
|
|
85
|
+
function built(dir) {
|
|
86
|
+
const digest = (name) => sha256Hex(readFileSync(join(dir, name)));
|
|
87
|
+
mkdirSync(join(dir, "_build"), { recursive: true });
|
|
88
|
+
writeFileSync(
|
|
89
|
+
join(dir, "_build", "sources.json"),
|
|
90
|
+
serializeSourcesRecord({
|
|
91
|
+
schema: 1,
|
|
92
|
+
inputs: [
|
|
93
|
+
{ path: "paper.tex", role: "body" },
|
|
94
|
+
{ path: "refs.bib", role: "preamble" },
|
|
95
|
+
],
|
|
96
|
+
written: [],
|
|
97
|
+
bibdata: ["refs"],
|
|
98
|
+
bibtex: {
|
|
99
|
+
ran: true,
|
|
100
|
+
databases: ["refs.bib"],
|
|
101
|
+
keys: [],
|
|
102
|
+
exit: 0,
|
|
103
|
+
errors: [],
|
|
104
|
+
},
|
|
105
|
+
sha256: {
|
|
106
|
+
"paper.tex": digest("paper.tex"),
|
|
107
|
+
"refs.bib": digest("refs.bib"),
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
73
112
|
// Cleanup is attached IMMEDIATELY: assertions throw, and "rmSync at end of file" does not execute precisely in those
|
|
74
113
|
// runs that are red.
|
|
75
114
|
process.on("exit", () => rmSync(TMP, { recursive: true, force: true }));
|
|
76
115
|
|
|
77
|
-
// ── 1. DEFECT #1:
|
|
116
|
+
// ── 1. DEFECT #1: the `.bib` a paper declares is opened ──────────────────────
|
|
117
|
+
//
|
|
118
|
+
// The build recorded that bibtex opened `refs.bib`, and `bibliographyAt` opens it. A fixture paper
|
|
119
|
+
// holds the frozen real `.bib`, so the leg is proven on a real file in every checkout.
|
|
78
120
|
{
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
121
|
+
const dir = join(TMP, "declares-real-bib");
|
|
122
|
+
mkdirSync(dir, { recursive: true });
|
|
123
|
+
writeFileSync(
|
|
124
|
+
join(dir, "paper.tex"),
|
|
125
|
+
"\\documentclass{article}\\begin{document}x\\bibliography{refs}\\end{document}\n",
|
|
82
126
|
);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
);
|
|
97
|
-
}
|
|
127
|
+
writeFileSync(
|
|
128
|
+
join(dir, "refs.bib"),
|
|
129
|
+
readFileSync(join(HERE, "fixtures", "real-bib", "refs.bib")),
|
|
130
|
+
);
|
|
131
|
+
built(dir);
|
|
132
|
+
const found = X.bibliographyFrom(dir);
|
|
133
|
+
assert.deepEqual(
|
|
134
|
+
found.ok && found.texts.map((t) => t.path),
|
|
135
|
+
[join(dir, "refs.bib")],
|
|
136
|
+
`a paper declaring \\bibliography{refs} does not open its refs.bib — the .bib leg is dead again: ${JSON.stringify(found.why ?? "")}`,
|
|
137
|
+
);
|
|
138
|
+
// Real consumer papers: whatever each declares is either read or refused WITH A REASON — never a
|
|
139
|
+
// silent nothing.
|
|
98
140
|
for (const paper of REAL_PAPERS) {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
join(dir, "refs.bib"),
|
|
104
|
-
`resolveSource cannot find real refs.bib for ${paper} — .bib leg is dead again`,
|
|
141
|
+
const r = X.bibliographyFrom(join(PAPERS_ROOT, paper));
|
|
142
|
+
assert.ok(
|
|
143
|
+
r.ok ? r.texts.length > 0 : r.why.length > 0,
|
|
144
|
+
`${paper}: neither a bibliography nor a reason`,
|
|
105
145
|
);
|
|
106
146
|
}
|
|
107
147
|
}
|
|
@@ -128,7 +168,7 @@ process.on("exit", () => rmSync(TMP, { recursive: true, force: true }));
|
|
|
128
168
|
parsed++;
|
|
129
169
|
const text = readFileSync(f, "utf8");
|
|
130
170
|
const want = (text.match(/^@\w+\{/gm) ?? []).length;
|
|
131
|
-
const got =
|
|
171
|
+
const got = factsOf(text);
|
|
132
172
|
assert.ok(
|
|
133
173
|
want > 0,
|
|
134
174
|
`${paper}/refs.bib: no @… entries in file — fixture is broken`,
|
|
@@ -170,7 +210,7 @@ process.on("exit", () => rmSync(TMP, { recursive: true, force: true }));
|
|
|
170
210
|
@misc{inst2,
|
|
171
211
|
author={{sh-guard contributors}},
|
|
172
212
|
title={Another}, year={2026}}`;
|
|
173
|
-
const es =
|
|
213
|
+
const es = factsOf(bib);
|
|
174
214
|
assert.deepEqual(
|
|
175
215
|
es.map((e) => e.authors),
|
|
176
216
|
[["Adversa AI"], ["sh-guard contributors"]],
|
|
@@ -185,7 +225,7 @@ process.on("exit", () => rmSync(TMP, { recursive: true, force: true }));
|
|
|
185
225
|
// single brace — ordinary author, and order "LastName, FirstName" must be unwound.
|
|
186
226
|
{
|
|
187
227
|
const bib = `@misc{human, author={Adversa, Alice}, title={T}, year={2026}}`;
|
|
188
|
-
const [e] =
|
|
228
|
+
const [e] = factsOf(bib);
|
|
189
229
|
assert.deepEqual(
|
|
190
230
|
e.authors,
|
|
191
231
|
["Alice Adversa"],
|
|
@@ -201,7 +241,7 @@ process.on("exit", () => rmSync(TMP, { recursive: true, force: true }));
|
|
|
201
241
|
const bib = `@inproceedings{k1,
|
|
202
242
|
author={Jimenez, Carlos E. and Di Penta, Massimiliano and others},
|
|
203
243
|
title={A Title}, booktitle={ICLR}, year={2024}, note={arXiv:2310.06770}}`;
|
|
204
|
-
const [e] =
|
|
244
|
+
const [e] = factsOf(bib);
|
|
205
245
|
assert.deepEqual(
|
|
206
246
|
e.authors,
|
|
207
247
|
["Carlos E. Jimenez", "Massimiliano Di Penta"],
|
|
@@ -313,7 +353,12 @@ process.on("exit", () => rmSync(TMP, { recursive: true, force: true }));
|
|
|
313
353
|
const { spawnSync } = await import("node:child_process");
|
|
314
354
|
const dir = join(TMP, "empty");
|
|
315
355
|
mkdirSync(dir, { recursive: true });
|
|
356
|
+
writeFileSync(
|
|
357
|
+
join(dir, "paper.tex"),
|
|
358
|
+
"\\documentclass{article}\\begin{document}x\\bibliography{refs}\\end{document}\n",
|
|
359
|
+
);
|
|
316
360
|
writeFileSync(join(dir, "refs.bib"), "% no entries at all\n");
|
|
361
|
+
built(dir);
|
|
317
362
|
const r = spawnSync(
|
|
318
363
|
"node",
|
|
319
364
|
[join(HERE, "extract-ref-facts.mjs"), dir, "--offline"],
|
|
@@ -346,8 +391,8 @@ process.on("exit", () => rmSync(TMP, { recursive: true, force: true }));
|
|
|
346
391
|
);
|
|
347
392
|
assert.equal(r.status, 1);
|
|
348
393
|
assert.ok(
|
|
349
|
-
/
|
|
350
|
-
|
|
394
|
+
/no paper\.tex/u.test(r.stderr),
|
|
395
|
+
`message must say what is missing: ${r.stderr}`,
|
|
351
396
|
);
|
|
352
397
|
}
|
|
353
398
|
|
|
@@ -360,6 +405,11 @@ process.on("exit", () => rmSync(TMP, { recursive: true, force: true }));
|
|
|
360
405
|
"@misc{lovelace2025,\n author={Lovelace, Ada and Hopper, Grace},\n title={A Study},\n" +
|
|
361
406
|
" note={arXiv:2503.09002},\n year={2025}}\n";
|
|
362
407
|
writeFileSync(join(dir, "refs.bib"), paper);
|
|
408
|
+
writeFileSync(
|
|
409
|
+
join(dir, "paper.tex"),
|
|
410
|
+
"\\documentclass{article}\\begin{document}x\\bibliography{refs}\\end{document}\n",
|
|
411
|
+
);
|
|
412
|
+
built(dir);
|
|
363
413
|
writeFileSync(
|
|
364
414
|
join(dir, "repro", "refs-cache.json"),
|
|
365
415
|
JSON.stringify({
|
|
@@ -390,6 +440,6 @@ process.on("exit", () => rmSync(TMP, { recursive: true, force: true }));
|
|
|
390
440
|
}
|
|
391
441
|
|
|
392
442
|
console.log(
|
|
393
|
-
"✓ extract-ref-facts: refs.bib
|
|
443
|
+
"✓ extract-ref-facts: the declared refs.bib is opened, both real .bib parse completely, name order unwound, " +
|
|
394
444
|
"arXiv-DOI does not go to CrossRef, registry readers run on raw bodies, zero entries = refusal",
|
|
395
445
|
);
|