paperlint 4.1.0 → 5.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 +26 -2
- package/CONTRIBUTING.md +7 -0
- package/README.md +15 -4
- package/action.harness.mjs +54 -36
- package/action.yml +12 -27
- package/dist/adapters/latex/document-class.d.ts +6 -5
- package/dist/adapters/latex/document-class.d.ts.map +1 -1
- package/dist/adapters/latex/document-class.js +18 -10
- package/dist/adapters/latex/document-class.js.map +1 -1
- package/dist/adapters/latex/includes.d.ts +16 -0
- package/dist/adapters/latex/includes.d.ts.map +1 -0
- package/dist/adapters/latex/includes.js +48 -0
- package/dist/adapters/latex/includes.js.map +1 -0
- package/dist/adapters/latex/index.d.ts +9 -5
- package/dist/adapters/latex/index.d.ts.map +1 -1
- package/dist/adapters/latex/index.js +8 -1
- package/dist/adapters/latex/index.js.map +1 -1
- package/dist/adapters/latex/nodes.d.ts +5 -1
- package/dist/adapters/latex/nodes.d.ts.map +1 -1
- package/dist/adapters/latex/nodes.js +6 -2
- package/dist/adapters/latex/nodes.js.map +1 -1
- package/dist/adapters/latex/parse.d.ts.map +1 -1
- package/dist/adapters/latex/parse.js +1 -0
- package/dist/adapters/latex/parse.js.map +1 -1
- package/dist/adapters/latex/prose.d.ts +8 -0
- package/dist/adapters/latex/prose.d.ts.map +1 -0
- package/dist/adapters/latex/prose.js +353 -0
- package/dist/adapters/latex/prose.js.map +1 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +7 -3
- package/dist/build.js.map +1 -1
- package/dist/claim-provenance.d.ts +34 -0
- package/dist/claim-provenance.d.ts.map +1 -0
- package/dist/claim-provenance.js +146 -0
- package/dist/claim-provenance.js.map +1 -0
- package/dist/cli.d.ts +2 -8
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +81 -31
- package/dist/cli.js.map +1 -1
- package/dist/domain/paper-source.d.ts +58 -0
- package/dist/domain/paper-source.d.ts.map +1 -0
- package/dist/domain/paper-source.js +109 -0
- package/dist/domain/paper-source.js.map +1 -0
- package/dist/domain/sentences.d.ts +26 -0
- package/dist/domain/sentences.d.ts.map +1 -0
- package/dist/domain/sentences.js +48 -0
- package/dist/domain/sentences.js.map +1 -0
- package/dist/domain/tex-document.d.ts +39 -4
- package/dist/domain/tex-document.d.ts.map +1 -1
- package/dist/domain/tex-document.js.map +1 -1
- package/dist/eslint-rules/latex-language.d.ts +5 -1
- package/dist/eslint-rules/latex-language.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.js +6 -3
- package/dist/eslint-rules/latex-language.js.map +1 -1
- package/dist/eslint-rules/paper-context.d.ts +20 -0
- package/dist/eslint-rules/paper-context.d.ts.map +1 -0
- package/dist/eslint-rules/paper-context.js +44 -0
- package/dist/eslint-rules/paper-context.js.map +1 -0
- package/dist/eslint-rules/paper-typography.d.ts +8 -1
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
- package/dist/eslint-rules/paper-typography.js +85 -19
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/eslint-rules/tex-build.d.ts.map +1 -1
- package/dist/eslint-rules/tex-build.js +7 -1
- package/dist/eslint-rules/tex-build.js.map +1 -1
- package/dist/init.d.ts +4 -19
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +24 -29
- package/dist/init.js.map +1 -1
- package/dist/package-dirs.d.ts +2 -0
- package/dist/package-dirs.d.ts.map +1 -1
- package/dist/package-dirs.js +8 -0
- package/dist/package-dirs.js.map +1 -1
- package/dist/paper-files.d.ts +2 -2
- package/dist/paper-includes.d.ts +17 -0
- package/dist/paper-includes.d.ts.map +1 -0
- package/dist/paper-includes.js +51 -0
- package/dist/paper-includes.js.map +1 -0
- package/dist/ports/latex.d.ts +12 -1
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/register.d.ts +40 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +134 -0
- package/dist/register.js.map +1 -0
- package/dist/rules-config.d.ts +1 -0
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.js +1 -1
- package/dist/structure.js.map +1 -1
- package/dist/tex-paper.d.ts +33 -0
- package/dist/tex-paper.d.ts.map +1 -0
- package/dist/tex-paper.js +103 -0
- package/dist/tex-paper.js.map +1 -0
- package/dist/tex-venue-rules.d.ts +7 -2
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +31 -17
- package/dist/tex-venue-rules.js.map +1 -1
- package/docs/configuration.md +5 -4
- package/docs/install.md +8 -8
- package/docs/prior-art/paper-folder-scaffolding.md +4 -7
- package/docs/rules/tex/claim-provenance.md +83 -0
- package/docs/rules/tex/register.md +77 -0
- package/docs/rules/tex/required-section.md +3 -1
- package/docs/rules/tex/template.md +12 -0
- package/docs/rules/tex/venue-leftover.md +4 -2
- package/docs/rules.md +17 -10
- package/eslint-rules/latex-language.ts +10 -4
- package/eslint-rules/paper-context.test.ts +47 -0
- package/eslint-rules/paper-context.ts +57 -0
- package/eslint-rules/paper-typography.test.mjs +48 -1
- package/eslint-rules/paper-typography.ts +139 -24
- package/eslint-rules/tex-build.ts +8 -1
- package/eslint.config.mjs +1 -0
- package/fixtures/real-markdown-paper/baseline.json +1 -2
- package/fixtures/real-markdown-paper/baseline.ts +8 -7
- package/package.json +2 -1
- package/scripts/check.ts +2 -2
- package/scripts/eslint-report-guard.mjs +5 -2
- package/scripts/harness-api.frozen.json +0 -1
- package/skills/cold-read-diff/SKILL.md +31 -10
- package/skills/cold-read-diff/SKILL.md.spec.ts +30 -9
- package/skills/draft-paper/SKILL.md +2 -2
- package/skills/draft-paper/SKILL.md.spec.ts +1 -1
- package/skills/grade-paper-writing/SKILL.md +109 -44
- package/skills/grade-paper-writing/SKILL.md.spec.ts +108 -43
- package/skills/harden-paper/SKILL.md +35 -36
- package/skills/harden-paper/SKILL.md.spec.ts +34 -35
- 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 +3 -3
- package/skills/paper-pipeline/SKILL.md.spec.ts +2 -2
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +6 -6
- package/skills/paper-pipeline/references/pipeline-status-template.md +2 -16
- package/skills/paper-pipeline/references/writing-craft.md +53 -18
- package/skills/pc-panel-review/SKILL.md +36 -11
- package/skills/pc-panel-review/SKILL.md.spec.ts +35 -10
- package/src/adapters/latex/document-class.ts +20 -12
- package/src/adapters/latex/includes.ts +74 -0
- package/src/adapters/latex/index.test.ts +183 -1
- package/src/adapters/latex/index.ts +14 -4
- package/src/adapters/latex/nodes.ts +11 -2
- package/src/adapters/latex/parse.ts +1 -0
- package/src/adapters/latex/prose.ts +484 -0
- package/src/build.harness.mjs +4 -2
- package/src/build.test.ts +18 -1
- package/src/build.ts +7 -3
- package/src/claim-provenance.test.ts +352 -0
- package/src/claim-provenance.ts +217 -0
- package/src/cli.harness.mjs +22 -54
- package/src/cli.test.ts +2 -18
- package/src/cli.ts +92 -36
- package/src/domain/paper-source.test.ts +156 -0
- package/src/domain/paper-source.ts +233 -0
- package/src/domain/sentences.ts +67 -0
- package/src/domain/tex-document.ts +36 -3
- package/src/init.test.ts +33 -13
- package/src/init.ts +25 -48
- package/src/linted-set.test.ts +11 -1
- package/src/new-paper.harness.mjs +1 -5
- package/src/package-dirs.ts +9 -0
- package/src/paper-files.ts +2 -2
- package/src/paper-includes.test.ts +161 -0
- package/src/paper-includes.ts +77 -0
- package/src/ports/latex.ts +13 -0
- package/src/register.test.ts +165 -0
- package/src/register.ts +177 -0
- package/src/rule-cases.test.ts +43 -23
- package/src/rule-docs.test.ts +0 -1
- package/src/rules-config.ts +1 -0
- package/src/structure.harness.ts +1 -3
- package/src/structure.ts +1 -1
- package/src/tex-paper.test.ts +148 -0
- package/src/tex-paper.ts +161 -0
- package/src/tex-venue-rules.test.ts +87 -5
- package/src/tex-venue-rules.ts +62 -30
- package/templates/paper/PIPELINE-STATUS.md +0 -4
- package/dist/action-ref.d.ts +0 -12
- package/dist/action-ref.d.ts.map +0 -1
- package/dist/action-ref.js +0 -16
- package/dist/action-ref.js.map +0 -1
- package/dist/eslint-rules/paper-research-question.d.ts +0 -31
- package/dist/eslint-rules/paper-research-question.d.ts.map +0 -1
- package/dist/eslint-rules/paper-research-question.js +0 -188
- package/dist/eslint-rules/paper-research-question.js.map +0 -1
- package/eslint-rules/paper-research-question.harness.mjs +0 -212
- package/eslint-rules/paper-research-question.ts +0 -199
- package/fixtures/paper-research-question/comment-only/PIPELINE-STATUS.md +0 -9
- package/fixtures/paper-research-question/comment-only/paper.tex +0 -7
- package/fixtures/paper-research-question/declared-not-in-paper/PIPELINE-STATUS.md +0 -10
- package/fixtures/paper-research-question/declared-not-in-paper/paper.tex +0 -6
- package/fixtures/paper-research-question/draft/PIPELINE-STATUS.md +0 -6
- package/fixtures/paper-research-question/draft/paper.tex +0 -2
- package/fixtures/paper-research-question/markdown-no-rq/PIPELINE-STATUS.md +0 -9
- package/fixtures/paper-research-question/markdown-no-rq/paper.md +0 -4
- package/fixtures/paper-research-question/shipped-no-rq/PIPELINE-STATUS.md +0 -12
- package/fixtures/paper-research-question/shipped-no-rq/paper.tex +0 -3
- package/fixtures/paper-research-question/shipped-with-rq/PIPELINE-STATUS.md +0 -10
- package/fixtures/paper-research-question/shipped-with-rq/paper.tex +0 -2
- package/skills/harden-paper/fixtures/uncited-assertions-sample.md +0 -43
- package/skills/harden-paper/fixtures/uncited-assertions-sample.tex +0 -77
- package/src/action-ref.test.ts +0 -26
- package/src/action-ref.ts +0 -15
package/src/cli.harness.mjs
CHANGED
|
@@ -132,8 +132,8 @@ check(
|
|
|
132
132
|
Array.isArray(cfg) && ruleBlocks(cfg).length === 4,
|
|
133
133
|
);
|
|
134
134
|
check(
|
|
135
|
-
"with a LaTeX language
|
|
136
|
-
ruleBlocks(buildConfig({}, {})).length ===
|
|
135
|
+
"with a LaTeX language two blocks are added: paper.tex, and the files it includes",
|
|
136
|
+
ruleBlocks(buildConfig({}, {})).length === 6,
|
|
137
137
|
);
|
|
138
138
|
check(
|
|
139
139
|
"🔴 the project's paper TEMPLATE directory is ignored — flat config does not skip dot-directories",
|
|
@@ -181,7 +181,7 @@ check(
|
|
|
181
181
|
writeFileSync(join(paper, "paper.md"), "# Intro\n\nRQ1: does it hold?\n");
|
|
182
182
|
writeFileSync(
|
|
183
183
|
join(paper, "PIPELINE-STATUS.md"),
|
|
184
|
-
`---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: 100\n source: versions/s.tex\n sourceBytes: 4\
|
|
184
|
+
`---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: 100\n source: versions/s.tex\n sourceBytes: 4\n---\n# S\n\n| id | note |\n|---|---|\n| cites | bib-authors run |\n`,
|
|
185
185
|
);
|
|
186
186
|
writeFileSync(
|
|
187
187
|
join(root, "package.json"),
|
|
@@ -441,32 +441,14 @@ check(
|
|
|
441
441
|
!existsSync(join(dir, WORKFLOW_PATH)),
|
|
442
442
|
);
|
|
443
443
|
check(
|
|
444
|
-
"and the CI
|
|
445
|
-
|
|
446
|
-
/paths: writing/.test(out.text()),
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
|
-
{
|
|
450
|
-
// Guards: the printed step carries the tag of the running release, not a placeholder.
|
|
451
|
-
const dir = project("ci-printed-pin", { papers: ["writing"] });
|
|
452
|
-
const out = say();
|
|
453
|
-
await init(dir, {
|
|
454
|
-
log: out.log,
|
|
455
|
-
err: out.log,
|
|
456
|
-
interactive: false,
|
|
457
|
-
run: haveAll,
|
|
458
|
-
version: "1.2.3",
|
|
459
|
-
});
|
|
460
|
-
check(
|
|
461
|
-
"🔴 a released version — the printed step is pinned to its tag, no <commit-sha>",
|
|
462
|
-
/uses: zernie\/paperlint@v1\.2\.3\b/.test(out.text()) &&
|
|
463
|
-
!/<commit-sha>/.test(out.text()),
|
|
444
|
+
"and the CI steps are still printed — they just get pasted in by hand",
|
|
445
|
+
out.text().includes("- uses: ./node_modules/paperlint"),
|
|
464
446
|
);
|
|
465
447
|
}
|
|
466
448
|
{
|
|
467
449
|
const dir = project("ci-yes", { papers: ["writing"] });
|
|
468
450
|
const asked = [];
|
|
469
|
-
const wf = await offerWorkflow(dir,
|
|
451
|
+
const wf = await offerWorkflow(dir, {
|
|
470
452
|
interactive: true,
|
|
471
453
|
ask: async (q) => {
|
|
472
454
|
asked.push(q);
|
|
@@ -477,16 +459,22 @@ check(
|
|
|
477
459
|
"saying yes WRITES the workflow",
|
|
478
460
|
wf === "written" && existsSync(join(dir, WORKFLOW_PATH)),
|
|
479
461
|
);
|
|
462
|
+
const steps =
|
|
463
|
+
yamlLoad(readFileSync(join(dir, WORKFLOW_PATH), "utf8"))?.jobs?.papers
|
|
464
|
+
?.steps ?? [];
|
|
480
465
|
check(
|
|
481
|
-
"
|
|
482
|
-
|
|
466
|
+
"🔴 the workflow runs the action from the installed package — one revision, set by the lockfile",
|
|
467
|
+
steps.some((st) => st.uses === "./node_modules/paperlint") &&
|
|
468
|
+
!steps.some((st) =>
|
|
469
|
+
String(st.uses ?? "").startsWith("zernie/paperlint"),
|
|
470
|
+
),
|
|
483
471
|
);
|
|
484
472
|
check(
|
|
485
|
-
"
|
|
486
|
-
|
|
473
|
+
"and it repeats no papers directory — the action reads papersDir from paperlint.json",
|
|
474
|
+
steps.every((st) => st.with?.paths === undefined),
|
|
487
475
|
);
|
|
488
476
|
check("the question is asked exactly once", asked.length === 1);
|
|
489
|
-
const again = await offerWorkflow(dir,
|
|
477
|
+
const again = await offerWorkflow(dir, {
|
|
490
478
|
interactive: true,
|
|
491
479
|
ask: async () => "y",
|
|
492
480
|
});
|
|
@@ -495,29 +483,9 @@ check(
|
|
|
495
483
|
again === "kept",
|
|
496
484
|
);
|
|
497
485
|
}
|
|
498
|
-
{
|
|
499
|
-
const dir = project("ci-pinned", { papers: ["writing"] });
|
|
500
|
-
const wf = await offerWorkflow(dir, "writing", {
|
|
501
|
-
interactive: true,
|
|
502
|
-
ask: async () => "y",
|
|
503
|
-
version: "1.2.3",
|
|
504
|
-
});
|
|
505
|
-
const yaml = existsSync(join(dir, WORKFLOW_PATH))
|
|
506
|
-
? readFileSync(join(dir, WORKFLOW_PATH), "utf8")
|
|
507
|
-
: "";
|
|
508
|
-
const uses = (yamlLoad(yaml)?.jobs?.papers?.steps ?? []).map(
|
|
509
|
-
(s) => s.uses,
|
|
510
|
-
);
|
|
511
|
-
check(
|
|
512
|
-
"🔴 a released version — the written workflow is pinned to its tag, no <commit-sha> left",
|
|
513
|
-
wf === "written" &&
|
|
514
|
-
uses.includes("zernie/paperlint@v1.2.3") &&
|
|
515
|
-
!yaml.includes("<commit-sha>"),
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
486
|
{
|
|
519
487
|
const dir = project("ci-no", { papers: ["writing"] });
|
|
520
|
-
const no = await offerWorkflow(dir,
|
|
488
|
+
const no = await offerWorkflow(dir, {
|
|
521
489
|
interactive: true,
|
|
522
490
|
ask: async () => "",
|
|
523
491
|
});
|
|
@@ -532,7 +500,7 @@ check(
|
|
|
532
500
|
// 🔴 THE `.catch` HERE IS LOAD-BEARING, NOT CAUTION. A leaked exception is a PROPERTY that
|
|
533
501
|
// gets asserted; without it, the harness crashes instead of failing a named assertion,
|
|
534
502
|
// i.e. the real defect would look like a hole in the test rather than a caught one.
|
|
535
|
-
const aborted = await offerWorkflow(dir,
|
|
503
|
+
const aborted = await offerWorkflow(dir, {
|
|
536
504
|
interactive: true,
|
|
537
505
|
ask: async () => {
|
|
538
506
|
throw new Error("Aborted with Ctrl+D");
|
|
@@ -733,7 +701,7 @@ check(
|
|
|
733
701
|
"x".repeat(100),
|
|
734
702
|
);
|
|
735
703
|
const status = (bytes) =>
|
|
736
|
-
`---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: ${bytes}\n source: versions/s.tex\n sourceBytes: 4\
|
|
704
|
+
`---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: ${bytes}\n source: versions/s.tex\n sourceBytes: 4\n---\n# S\n\n| id | note |\n|---|---|\n| cites | bib-authors run |\n`;
|
|
737
705
|
writeFileSync(join(paper, "versions", "s.tex"), "abcd");
|
|
738
706
|
writeFileSync(join(paper, "paper.md"), "# Intro\n\nRQ1: does it hold?\n");
|
|
739
707
|
|
|
@@ -822,7 +790,7 @@ check(
|
|
|
822
790
|
);
|
|
823
791
|
writeFileSync(join(paper, "paper.md"), "# Intro\n\nRQ1: does it hold?\n");
|
|
824
792
|
const status = (bytes) =>
|
|
825
|
-
`---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: ${bytes}\n source: versions/s.tex\n sourceBytes: 4\
|
|
793
|
+
`---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: ${bytes}\n source: versions/s.tex\n sourceBytes: 4\n---\n# S\n\n| id | note |\n|---|---|\n| cites | bib-authors run |\n`;
|
|
826
794
|
writeFileSync(join(paper, "PIPELINE-STATUS.md"), status(100));
|
|
827
795
|
|
|
828
796
|
// findProjectRoot — kept separate from the run so the failure is distinguishable
|
|
@@ -934,7 +902,7 @@ check(
|
|
|
934
902
|
);
|
|
935
903
|
writeFileSync(
|
|
936
904
|
join(paper, "PIPELINE-STATUS.md"),
|
|
937
|
-
`---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: 100\n source: versions/s.tex\n sourceBytes: 4\
|
|
905
|
+
`---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: 100\n source: versions/s.tex\n sourceBytes: 4\n---\n# S\n\n| id | note |\n|---|---|\n| cites | bib-authors run |\n`,
|
|
938
906
|
);
|
|
939
907
|
writeFileSync(
|
|
940
908
|
join(root, "paperlint.json"),
|
package/src/cli.test.ts
CHANGED
|
@@ -14,7 +14,6 @@ import type { runToolchain } from "./toolchain.ts";
|
|
|
14
14
|
import {
|
|
15
15
|
chooseVenue,
|
|
16
16
|
initTexLive,
|
|
17
|
-
ownVersion,
|
|
18
17
|
paperRuleBlocks,
|
|
19
18
|
parseArgs,
|
|
20
19
|
parseSettings,
|
|
@@ -259,7 +258,7 @@ test("build: a markdown-only paper is refused", async () => {
|
|
|
259
258
|
code: 1,
|
|
260
259
|
out: [
|
|
261
260
|
"papers/md",
|
|
262
|
-
` inputs: TEXINPUTS += ${venues}`,
|
|
261
|
+
` inputs: TEXINPUTS += the paper's directory, then ${venues}`,
|
|
263
262
|
" compile: refused — no paper.tex; paperlint compiles LaTeX, and this paper has none",
|
|
264
263
|
" measure: skipped — nothing is compiled",
|
|
265
264
|
" references: skipped — nothing is compiled",
|
|
@@ -288,7 +287,7 @@ test("build: with no TeX Live a dry run says where a real run stops, and a real
|
|
|
288
287
|
out: [
|
|
289
288
|
`engine: none — a real run would stop here: paperlint build: no TeX Live with every package these papers need — run \`npx paperlint toolchain\` (${missing})`,
|
|
290
289
|
"papers/tex",
|
|
291
|
-
` inputs: TEXINPUTS += ${venues}`,
|
|
290
|
+
` inputs: TEXINPUTS += the paper's directory, then ${venues}`,
|
|
292
291
|
" compile: paper.tex (\\documentclass{article})",
|
|
293
292
|
" measure: pdf.js → _build/paper.facts.json (facts for the lint rules; nothing is judged here)",
|
|
294
293
|
" references: online: citations exist, titles and authors match → _build/references.json (never fails the build)",
|
|
@@ -529,21 +528,6 @@ test("chooseVenue: a question that fails (the stream ended) takes the default, n
|
|
|
529
528
|
assert.deepEqual(r, { ok: true, value: null });
|
|
530
529
|
});
|
|
531
530
|
|
|
532
|
-
test("ownVersion: the manifest's version; an unreadable manifest or a non-string version is undefined", () => {
|
|
533
|
-
assert.deepEqual(
|
|
534
|
-
[
|
|
535
|
-
ownVersion(() => '{"version": "1.2.3"}'),
|
|
536
|
-
ownVersion(() => "{ nope"),
|
|
537
|
-
ownVersion(() => '{"version": 3}'),
|
|
538
|
-
ownVersion(() => {
|
|
539
|
-
throw new Error("ENOENT");
|
|
540
|
-
}),
|
|
541
|
-
typeof ownVersion(),
|
|
542
|
-
],
|
|
543
|
-
["1.2.3", undefined, undefined, undefined, "string"],
|
|
544
|
-
);
|
|
545
|
-
});
|
|
546
|
-
|
|
547
531
|
test("init's TeX Live install runs the toolchain over the project's papers, never a check", () => {
|
|
548
532
|
const dir = writeTree(join(root, "init-tex"), { "package.json": "{}" });
|
|
549
533
|
const calls: Parameters<typeof runToolchain>[0][] = [];
|
package/src/cli.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* but that is INTERNAL machinery, and you no longer need to know it in order to run them.
|
|
10
10
|
*
|
|
11
11
|
* Two entry points into the tool, and both are whole now:
|
|
12
|
-
* npx paperlint lint
|
|
13
|
-
* uses:
|
|
12
|
+
* npx paperlint lint ← here
|
|
13
|
+
* uses: ./node_modules/paperlint ← action.yml, after `npm ci`
|
|
14
14
|
*
|
|
15
15
|
* ⚠️ THE BOUNDARY THIS UTILITY HAS NO RIGHT TO ERASE: the consumer's data stays with the consumer.
|
|
16
16
|
* Where the papers are and the project's own rule blocks — that is about ONE corpus, so it lives in
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
asEslintResults,
|
|
43
43
|
} from "./structure.ts";
|
|
44
44
|
import { buildPapers, papersIn, anyFailed, remedyFor, MAIN } from "./build.ts";
|
|
45
|
+
import { includeBlocks, paperBodies, unreadLines } from "./paper-includes.ts";
|
|
45
46
|
import { prepareEngine } from "./build-engine.ts";
|
|
46
47
|
import { cacheRoot, cachedTree, runToolchain } from "./toolchain.ts";
|
|
47
48
|
import { banalInstaller, parseBanalSettings } from "./adapters/banal/index.ts";
|
|
@@ -50,6 +51,8 @@ import { latexReader } from "./adapters/latex/index.ts";
|
|
|
50
51
|
import { hostDirs, nodeAdapters, nodeFiles } from "./adapters/node/index.ts";
|
|
51
52
|
import { VENUE_RULE_LEVELS, venueRules } from "./venue-rules.ts";
|
|
52
53
|
import { TEX_VENUE_RULE_LEVELS, texVenueRules } from "./tex-venue-rules.ts";
|
|
54
|
+
import { claimProvenanceRule } from "./claim-provenance.ts";
|
|
55
|
+
import { registerRule } from "./register.ts";
|
|
53
56
|
import {
|
|
54
57
|
paperRules,
|
|
55
58
|
stringFields,
|
|
@@ -122,7 +125,6 @@ export { init };
|
|
|
122
125
|
export { nextSteps } from "./init.ts";
|
|
123
126
|
|
|
124
127
|
import paperStages from "#eslint-rules/paper-stages";
|
|
125
|
-
import researchQuestion from "#eslint-rules/paper-research-question";
|
|
126
128
|
import typography from "#eslint-rules/paper-typography";
|
|
127
129
|
import texBuild from "#eslint-rules/tex-build";
|
|
128
130
|
import bibReachable from "#eslint-rules/bib-reachable-entry";
|
|
@@ -225,7 +227,7 @@ export function buildConfig(
|
|
|
225
227
|
opts: PaperlintConfig = {},
|
|
226
228
|
texLanguage: unknown,
|
|
227
229
|
): ConfigBlock[] {
|
|
228
|
-
const paperRules = { ...
|
|
230
|
+
const paperRules = { ...typography.rules };
|
|
229
231
|
// The reference rules judge `_build/references.json`, and only on `paper.tex`.
|
|
230
232
|
const texPaperRules = {
|
|
231
233
|
...paperRules,
|
|
@@ -233,7 +235,6 @@ export function buildConfig(
|
|
|
233
235
|
};
|
|
234
236
|
// Each typography rule reports every occurrence where it is, and fixes it (`--fix`).
|
|
235
237
|
const prose = {
|
|
236
|
-
"paper/research-question": "warn",
|
|
237
238
|
"paper/section-word": "warn",
|
|
238
239
|
"paper/leading-zero": "warn",
|
|
239
240
|
};
|
|
@@ -312,6 +313,14 @@ export function buildConfig(
|
|
|
312
313
|
venuesDir: presetsDir(),
|
|
313
314
|
latex: latexReader,
|
|
314
315
|
}),
|
|
316
|
+
"claim-provenance": claimProvenanceRule({
|
|
317
|
+
files: nodeFiles,
|
|
318
|
+
latex: latexReader,
|
|
319
|
+
}),
|
|
320
|
+
register: registerRule({
|
|
321
|
+
files: nodeFiles,
|
|
322
|
+
latex: latexReader,
|
|
323
|
+
}),
|
|
315
324
|
},
|
|
316
325
|
},
|
|
317
326
|
paper: { rules: texPaperRules },
|
|
@@ -324,23 +333,64 @@ export function buildConfig(
|
|
|
324
333
|
"paper/figure-ref-style": "warn",
|
|
325
334
|
"bib/reachable-entry": "warn",
|
|
326
335
|
"tex/future-promise": "warn",
|
|
336
|
+
// A number with no owner: a prose rule, on beside the others above.
|
|
337
|
+
"tex/claim-provenance": "warn",
|
|
338
|
+
// Sentence-initial conjunctions over the whole body: register, not vocabulary.
|
|
339
|
+
"tex/register": "warn",
|
|
327
340
|
"tex/acm-frontmatter-override": "error",
|
|
328
341
|
// Silent for a paper whose paperlint.json names no venue (src/venue-rules.ts).
|
|
329
342
|
...VENUE_RULE_LEVELS,
|
|
330
343
|
...TEX_VENUE_RULE_LEVELS,
|
|
331
344
|
},
|
|
332
345
|
});
|
|
346
|
+
// THE FILES A paper.tex INCLUDES FROM ITS BODY, each read on its own and reported at its own path
|
|
347
|
+
// (#144). The block claims every `.tex` but `paper.tex` — yet it is left out of the scope below, so
|
|
348
|
+
// it reaches only the files `paperlint lint` un-ignores by name for each paper (src/paper-includes.ts):
|
|
349
|
+
// a frozen version or a macro file is never enumerated. The rules are the ones that read a file as
|
|
350
|
+
// a fragment (no `document` environment: all of it is body); the whole-paper rules stay on
|
|
351
|
+
// `paper.tex`, which reads its includes itself (src/tex-paper.ts).
|
|
352
|
+
const fragments: readonly ConfigBlock[] = texLanguage
|
|
353
|
+
? [
|
|
354
|
+
{
|
|
355
|
+
files: ["**/*.tex"],
|
|
356
|
+
ignores: ["**/paper.tex"],
|
|
357
|
+
plugins: {
|
|
358
|
+
tex: {
|
|
359
|
+
languages: { latex: texLanguage },
|
|
360
|
+
rules: { "future-promise": texBuild["future-promise"] },
|
|
361
|
+
},
|
|
362
|
+
paper: {
|
|
363
|
+
rules: {
|
|
364
|
+
"section-word": typography.rules["section-word"],
|
|
365
|
+
"leading-zero": typography.rules["leading-zero"],
|
|
366
|
+
"figure-ref-style": typography.rules["figure-ref-style"],
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
language: "tex/latex",
|
|
371
|
+
rules: {
|
|
372
|
+
"paper/section-word": "warn",
|
|
373
|
+
"paper/leading-zero": "warn",
|
|
374
|
+
"paper/figure-ref-style": "warn",
|
|
375
|
+
"tex/future-promise": "warn",
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
]
|
|
379
|
+
: [];
|
|
333
380
|
// 🔴 ONLY THE FILES THESE BLOCKS CLAIM ARE LINTED (src/paper-files.ts). Without this block ESLint's
|
|
334
381
|
// built-in defaults lint every .js/.mjs/.cjs under the papers directory — a paper's vendored
|
|
335
382
|
// `repro/` code failed the run with 74 parse errors and not one finding on a paper file. It goes
|
|
336
383
|
// FIRST: the `.template/` ignore below must come after its directory un-ignore to win.
|
|
337
|
-
const owners = ruleOwners(cfg);
|
|
384
|
+
const owners = ruleOwners([...cfg, ...fragments]);
|
|
338
385
|
cfg.unshift(scopeToOwned(ownedPatterns(cfg)));
|
|
339
386
|
// The consumer's own blocks, LAST, so a later block wins — ESLint's rule. Parsed by
|
|
340
387
|
// `readConfig`; each carries the settings file's directory as its `basePath`. Each is split so a
|
|
341
388
|
// rule reaches only the files its plugin is registered for: `paper` is a different plugin beside
|
|
342
389
|
// PIPELINE-STATUS.md than beside paper.tex, and ESLint throws on a rule its plugin lacks.
|
|
343
|
-
cfg.push(
|
|
390
|
+
cfg.push(
|
|
391
|
+
...fragments,
|
|
392
|
+
...(opts.rules ?? []).flatMap((b) => narrowToOwners(b, owners)),
|
|
393
|
+
);
|
|
344
394
|
return cfg;
|
|
345
395
|
}
|
|
346
396
|
|
|
@@ -481,6 +531,17 @@ export async function silentOptionalRules(
|
|
|
481
531
|
return [...turnedOn].filter((id) => !reached.has(id));
|
|
482
532
|
}
|
|
483
533
|
|
|
534
|
+
/**
|
|
535
|
+
* The paper directories `paths` reach: each directory and the papers under it. A FILE named on the
|
|
536
|
+
* command line belongs to the paper it sits in.
|
|
537
|
+
*/
|
|
538
|
+
function paperDirs(paths: readonly string[]): readonly string[] {
|
|
539
|
+
const dirs = paths.map((p) =>
|
|
540
|
+
existsSync(p) && statSync(p).isFile() ? dirname(p) : p,
|
|
541
|
+
);
|
|
542
|
+
return [...new Set(dirs.flatMap((p) => [p, ...papersIn(p)]))];
|
|
543
|
+
}
|
|
544
|
+
|
|
484
545
|
/**
|
|
485
546
|
* Every linted paper's rules, as ESLint blocks scoped to that paper, in two groups: what its venue
|
|
486
547
|
* preset turns on, and what its own `paperlint.json` says. The caller puts the root's blocks between
|
|
@@ -490,11 +551,7 @@ export async function silentOptionalRules(
|
|
|
490
551
|
export function paperRuleBlocks(
|
|
491
552
|
paths: readonly string[],
|
|
492
553
|
): Parsed<{ preset: RuleBlock[]; own: RuleBlock[] }> {
|
|
493
|
-
|
|
494
|
-
const dirs = paths.map((p) =>
|
|
495
|
-
existsSync(p) && statSync(p).isFile() ? dirname(p) : p,
|
|
496
|
-
);
|
|
497
|
-
const papers = [...new Set(dirs.flatMap((p) => [p, ...papersIn(p)]))];
|
|
554
|
+
const papers = paperDirs(paths);
|
|
498
555
|
const out: { preset: RuleBlock[]; own: RuleBlock[] } = {
|
|
499
556
|
preset: [],
|
|
500
557
|
own: [],
|
|
@@ -745,28 +802,6 @@ const VALUE_FLAGS: ReadonlySet<string> = new Set([
|
|
|
745
802
|
"--max-warnings",
|
|
746
803
|
]);
|
|
747
804
|
|
|
748
|
-
/**
|
|
749
|
-
* This package's own version, from the `package.json` beside `src/` and `dist/` alike. `init` pins
|
|
750
|
-
* the CI action to its release tag; an unreadable manifest yields `undefined`, and init then keeps
|
|
751
|
-
* the placeholder instead of guessing.
|
|
752
|
-
*/
|
|
753
|
-
export function ownVersion(
|
|
754
|
-
readManifest: () => string = () =>
|
|
755
|
-
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
756
|
-
): string | undefined {
|
|
757
|
-
try {
|
|
758
|
-
const manifest: unknown = JSON.parse(readManifest());
|
|
759
|
-
return typeof manifest === "object" &&
|
|
760
|
-
manifest !== null &&
|
|
761
|
-
"version" in manifest &&
|
|
762
|
-
typeof manifest.version === "string"
|
|
763
|
-
? manifest.version
|
|
764
|
-
: undefined;
|
|
765
|
-
} catch {
|
|
766
|
-
return undefined;
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
|
|
770
805
|
/**
|
|
771
806
|
* Reading the config, ONE reader for all commands. Pulled out of `run()` the moment a second
|
|
772
807
|
* command needed the same config (`build`): two copies of this block would have drifted apart on
|
|
@@ -1401,7 +1436,6 @@ async function runInit(
|
|
|
1401
1436
|
log,
|
|
1402
1437
|
err,
|
|
1403
1438
|
cwd,
|
|
1404
|
-
version: ownVersion(),
|
|
1405
1439
|
yes: a.yes,
|
|
1406
1440
|
hooks: !a.noHooks,
|
|
1407
1441
|
paper: a.paper,
|
|
@@ -1537,10 +1571,32 @@ export async function run(
|
|
|
1537
1571
|
texLanguage = null;
|
|
1538
1572
|
}
|
|
1539
1573
|
|
|
1574
|
+
// Each paper's body files, resolved the way the build resolves them — only when the LaTeX language
|
|
1575
|
+
// loaded, since only its rules read them. The project's papers too: `paperlint lint .` reaches
|
|
1576
|
+
// papers two levels down, which a directory's immediate papers do not.
|
|
1577
|
+
const bodies = texLanguage
|
|
1578
|
+
? paperBodies(paperDirs([...paths, ...papersRoots(cfg)]), {
|
|
1579
|
+
files: nodeFiles,
|
|
1580
|
+
latex: latexReader,
|
|
1581
|
+
})
|
|
1582
|
+
: [];
|
|
1583
|
+
// Named only for the papers this run lints: a paper elsewhere in the project is not this run's.
|
|
1584
|
+
const inScope = (dir: string) =>
|
|
1585
|
+
paths.some((p) => dir === p || dir.startsWith(`${p}${sep}`));
|
|
1586
|
+
unreadLines(
|
|
1587
|
+
bodies.filter((b) => inScope(b.dir)),
|
|
1588
|
+
(p) => shown(cwd, p),
|
|
1589
|
+
).forEach((l) => {
|
|
1590
|
+
err(l);
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1540
1593
|
const eslint = new ESLint({
|
|
1541
1594
|
cwd: lintRoot(root, paths),
|
|
1542
1595
|
overrideConfigFile: true,
|
|
1543
|
-
overrideConfig: eslintConfig(
|
|
1596
|
+
overrideConfig: eslintConfig([
|
|
1597
|
+
...buildConfig(withPapers, texLanguage),
|
|
1598
|
+
...includeBlocks(bodies),
|
|
1599
|
+
]),
|
|
1544
1600
|
fix: a.fix,
|
|
1545
1601
|
});
|
|
1546
1602
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The paper as one text: includes spliced where they stand, every character mapped back to its
|
|
3
|
+
* file, a missing file reported, a subfile's body only, and a file that includes itself entered once.
|
|
4
|
+
* The reader is a stand-in that finds `\input{…}` by position, so these tests are about assembling,
|
|
5
|
+
* not about parsing LaTeX (the LaTeX adapter's own tests cover `includes`).
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import {
|
|
9
|
+
assemblePaper,
|
|
10
|
+
originOf,
|
|
11
|
+
type AssembleDeps,
|
|
12
|
+
type Include,
|
|
13
|
+
} from "./paper-source.ts";
|
|
14
|
+
|
|
15
|
+
/** `\input{x}` / `\include{x}` / `\subfile{x}` in `src`, by a scan good enough for these inputs. */
|
|
16
|
+
const includes = (src: string): readonly Include[] =>
|
|
17
|
+
[...src.matchAll(/\\(input|include|subfile)\{([^}]*)\}/gu)].map((m) => ({
|
|
18
|
+
macro:
|
|
19
|
+
m[1] === "subfile" ? "subfile" : m[1] === "include" ? "include" : "input",
|
|
20
|
+
target: m[2] ?? "",
|
|
21
|
+
span: { start: m.index, end: m.index + m[0].length },
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const deps = (files: Record<string, string>): AssembleDeps => ({
|
|
25
|
+
includes,
|
|
26
|
+
documentBody: (src) => {
|
|
27
|
+
const a = src.indexOf("\\begin{document}");
|
|
28
|
+
const b = src.indexOf("\\end{document}");
|
|
29
|
+
return a < 0 || b < 0
|
|
30
|
+
? null
|
|
31
|
+
: { start: a + "\\begin{document}".length, end: b };
|
|
32
|
+
},
|
|
33
|
+
read: (p) => files[p] ?? null,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("assemblePaper — the text TeX reads", () => {
|
|
37
|
+
it("splices each file where its include stands; `.tex` is tried first", () => {
|
|
38
|
+
const main = "A\\input{s/one}B\\include{two.tex}C";
|
|
39
|
+
const p = assemblePaper(
|
|
40
|
+
"paper.tex",
|
|
41
|
+
main,
|
|
42
|
+
deps({ "s/one.tex": "one", "two.tex": "two" }),
|
|
43
|
+
);
|
|
44
|
+
expect(p.text).toBe("AoneBtwoC");
|
|
45
|
+
expect(p.missing).toEqual([]);
|
|
46
|
+
expect([...new Set(p.segments.map((g) => g.file))]).toEqual([
|
|
47
|
+
"paper.tex",
|
|
48
|
+
"s/one.tex",
|
|
49
|
+
"two.tex",
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("nested includes resolve from the paper directory, and map back through the outermost include", () => {
|
|
54
|
+
const main = "x\\input{a}y";
|
|
55
|
+
const p = assemblePaper(
|
|
56
|
+
"paper.tex",
|
|
57
|
+
main,
|
|
58
|
+
deps({ "a.tex": "[\\input{s/b}]", "s/b.tex": "BB" }),
|
|
59
|
+
);
|
|
60
|
+
expect(p.text).toBe("x[BB]y");
|
|
61
|
+
expect(originOf(p, { start: 2, end: 4 })).toEqual({
|
|
62
|
+
file: "s/b.tex",
|
|
63
|
+
source: "BB",
|
|
64
|
+
span: { start: 0, end: 2 },
|
|
65
|
+
via: { start: 1, end: 10 },
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("🔴 a missing file is reported where it stands, and left out", () => {
|
|
70
|
+
const main = "a\\input{gone}b";
|
|
71
|
+
const p = assemblePaper("paper.tex", main, deps({}));
|
|
72
|
+
expect(p.text).toBe("ab");
|
|
73
|
+
expect(p.missing).toEqual([
|
|
74
|
+
{
|
|
75
|
+
file: "paper.tex",
|
|
76
|
+
target: "gone",
|
|
77
|
+
span: { start: 1, end: 13 },
|
|
78
|
+
via: { start: 1, end: 13 },
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("assemblePaper — subfiles and a file that includes itself", () => {
|
|
85
|
+
it("a subfile contributes its document body, not its preamble", () => {
|
|
86
|
+
const sub =
|
|
87
|
+
"\\documentclass[paper]{subfiles}\\begin{document}BODY\\end{document}";
|
|
88
|
+
const p = assemblePaper(
|
|
89
|
+
"paper.tex",
|
|
90
|
+
"<\\subfile{s}>",
|
|
91
|
+
deps({ "s.tex": sub }),
|
|
92
|
+
);
|
|
93
|
+
expect(p.text).toBe("<BODY>");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("a subfile with no document environment is read whole", () => {
|
|
97
|
+
const p = assemblePaper(
|
|
98
|
+
"paper.tex",
|
|
99
|
+
"<\\subfile{s}>",
|
|
100
|
+
deps({ "s.tex": "ALL" }),
|
|
101
|
+
);
|
|
102
|
+
expect(p.text).toBe("<ALL>");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("a file that includes itself is entered once", () => {
|
|
106
|
+
const p = assemblePaper(
|
|
107
|
+
"paper.tex",
|
|
108
|
+
"m\\input{a}",
|
|
109
|
+
deps({ "a.tex": "a\\input{a}\\input{paper.tex}" }),
|
|
110
|
+
);
|
|
111
|
+
expect(p.text).toBe("ma");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("originOf — where a span of the assembled text comes from", () => {
|
|
116
|
+
const p = assemblePaper(
|
|
117
|
+
"paper.tex",
|
|
118
|
+
"ab\\input{x}cd",
|
|
119
|
+
deps({ "x.tex": "XYZ" }),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
it("in the main file: its own offsets, via nothing", () => {
|
|
123
|
+
expect(originOf(p, { start: 0, end: 2 })).toEqual({
|
|
124
|
+
file: "paper.tex",
|
|
125
|
+
source: "ab\\input{x}cd",
|
|
126
|
+
span: { start: 0, end: 2 },
|
|
127
|
+
via: null,
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("after the include: offsets in the main file past the macro", () => {
|
|
132
|
+
expect(originOf(p, { start: 5, end: 7 })).toEqual({
|
|
133
|
+
file: "paper.tex",
|
|
134
|
+
source: "ab\\input{x}cd",
|
|
135
|
+
span: { start: 11, end: 13 },
|
|
136
|
+
via: null,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("a span running out of an included file is cut at its end", () => {
|
|
141
|
+
expect(originOf(p, { start: 3, end: 7 })).toEqual({
|
|
142
|
+
file: "x.tex",
|
|
143
|
+
source: "XYZ",
|
|
144
|
+
span: { start: 1, end: 3 },
|
|
145
|
+
via: { start: 2, end: 11 },
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("an empty span at the very end belongs to the last stretch; outside the text there is none", () => {
|
|
150
|
+
expect(originOf(p, { start: 7, end: 7 })?.span).toEqual({
|
|
151
|
+
start: 13,
|
|
152
|
+
end: 13,
|
|
153
|
+
});
|
|
154
|
+
expect(originOf(p, { start: 9, end: 9 })).toBe(null);
|
|
155
|
+
});
|
|
156
|
+
});
|