paperlint 6.2.3 → 7.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/CONTRIBUTING.md +4 -2
- package/README.md +10 -3
- package/dist/adapters/hotcrp/index.d.ts +7 -0
- package/dist/adapters/hotcrp/index.d.ts.map +1 -0
- package/dist/adapters/hotcrp/index.js +7 -0
- package/dist/adapters/hotcrp/index.js.map +1 -0
- package/dist/adapters/hotcrp/portal.io.d.ts +20 -0
- package/dist/adapters/hotcrp/portal.io.d.ts.map +1 -0
- package/dist/adapters/hotcrp/portal.io.js +90 -0
- package/dist/adapters/hotcrp/portal.io.js.map +1 -0
- package/dist/adapters/hotcrp/response.d.ts +13 -0
- package/dist/adapters/hotcrp/response.d.ts.map +1 -0
- package/dist/adapters/hotcrp/response.js +138 -0
- package/dist/adapters/hotcrp/response.js.map +1 -0
- package/dist/adapters/latex/conditionals.d.ts +21 -0
- package/dist/adapters/latex/conditionals.d.ts.map +1 -0
- package/dist/adapters/latex/conditionals.js +75 -0
- package/dist/adapters/latex/conditionals.js.map +1 -0
- package/dist/adapters/latex/headings.d.ts +10 -0
- package/dist/adapters/latex/headings.d.ts.map +1 -0
- package/dist/adapters/latex/headings.js +129 -0
- package/dist/adapters/latex/headings.js.map +1 -0
- package/dist/adapters/latex/index.d.ts +1 -0
- package/dist/adapters/latex/index.d.ts.map +1 -1
- package/dist/adapters/latex/index.js +3 -0
- package/dist/adapters/latex/index.js.map +1 -1
- package/dist/adapters/latex/parse.d.ts +1 -1
- package/dist/adapters/latex/parse.d.ts.map +1 -1
- package/dist/adapters/latex/parse.js +3 -2
- package/dist/adapters/latex/parse.js.map +1 -1
- package/dist/build.d.ts +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +1 -1
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +4 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +126 -37
- package/dist/cli.js.map +1 -1
- package/dist/domain/heading-case.d.ts +100 -0
- package/dist/domain/heading-case.d.ts.map +1 -0
- package/dist/domain/heading-case.js +365 -0
- package/dist/domain/heading-case.js.map +1 -0
- package/dist/domain/result.d.ts +4 -0
- package/dist/domain/result.d.ts.map +1 -1
- package/dist/domain/result.js +2 -0
- package/dist/domain/result.js.map +1 -1
- package/dist/domain/submission.d.ts +106 -0
- package/dist/domain/submission.d.ts.map +1 -0
- package/dist/domain/submission.js +39 -0
- package/dist/domain/submission.js.map +1 -0
- package/dist/domain/tex-document.d.ts +32 -0
- package/dist/domain/tex-document.d.ts.map +1 -1
- package/dist/domain/tex-document.js +11 -0
- package/dist/domain/tex-document.js.map +1 -1
- package/dist/domain/venue-name.d.ts +38 -0
- package/dist/domain/venue-name.d.ts.map +1 -0
- package/dist/domain/venue-name.js +71 -0
- package/dist/domain/venue-name.js.map +1 -0
- package/dist/eslint-rules/latex-language.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.js +16 -3
- package/dist/eslint-rules/latex-language.js.map +1 -1
- package/dist/facts-file.d.ts +3 -3
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +3 -3
- package/dist/facts-file.js.map +1 -1
- package/dist/folder-venue-rule.d.ts +51 -0
- package/dist/folder-venue-rule.d.ts.map +1 -0
- package/dist/folder-venue-rule.js +96 -0
- package/dist/folder-venue-rule.js.map +1 -0
- package/dist/heading-case.d.ts +48 -0
- package/dist/heading-case.d.ts.map +1 -0
- package/dist/heading-case.js +191 -0
- package/dist/heading-case.js.map +1 -0
- package/dist/init.d.ts +6 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +3 -3
- package/dist/init.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +5 -1
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/new-paper.d.ts +19 -5
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +23 -4
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-settings.d.ts +3 -0
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +20 -4
- package/dist/paper-settings.js.map +1 -1
- package/dist/ports/latex.d.ts +7 -1
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/ports/submission-portal.d.ts +19 -0
- package/dist/ports/submission-portal.d.ts.map +1 -0
- package/dist/ports/submission-portal.js +2 -0
- package/dist/ports/submission-portal.js.map +1 -0
- package/dist/preset-card-rule.d.ts +100 -0
- package/dist/preset-card-rule.d.ts.map +1 -0
- package/dist/preset-card-rule.js +221 -0
- package/dist/preset-card-rule.js.map +1 -0
- package/dist/presets.d.ts +43 -2
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +130 -63
- package/dist/presets.js.map +1 -1
- package/dist/rules-config.js +1 -1
- package/dist/submission.d.ts +61 -0
- package/dist/submission.d.ts.map +1 -0
- package/dist/submission.js +211 -0
- package/dist/submission.js.map +1 -0
- package/dist/tex-requirements.d.ts +36 -3
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +30 -2
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +7 -8
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +10 -14
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/configuration.md +30 -11
- package/docs/optional-rules.md +4 -3
- package/docs/rules/format/page-limit.md +1 -1
- package/docs/rules/paper/folder-venue-leftover.md +104 -0
- package/docs/rules/talk/captions-cover.md +1 -1
- package/docs/rules/talk/duration-floor.md +1 -1
- package/docs/rules/talk/profile.md +1 -1
- package/docs/rules/tex/heading-case.md +424 -0
- package/docs/rules.md +64 -53
- package/docs/skills.md +5 -4
- package/docs/submission.md +64 -0
- package/docs/talk.md +1 -1
- package/eslint-rules/latex-language.test.mjs +12 -0
- package/eslint-rules/latex-language.ts +17 -4
- package/eslint-rules/paper-typography.test.mjs +12 -0
- package/eslint.config.mjs +59 -1
- package/fixtures/build-e2e/acmart/paper.tex +1 -1
- package/fixtures/build-e2e/fallback/expect.json +3 -3
- package/lib/paper-config.ts +6 -1
- package/lib/skill-checks.d.mts +7 -0
- package/lib/skill-checks.mjs +31 -0
- package/lib/skill-trigger-cases.mjs +11 -0
- package/package.json +8 -2
- package/presets/acm-sigconf.jsonc +5 -0
- package/presets/acm-sigconf.md +43 -0
- package/presets/agenticdev.jsonc +27 -6
- package/presets/agenticdev.md +151 -0
- package/presets/aidc.jsonc +12 -0
- package/presets/aidc.md +75 -0
- package/presets/aisec.jsonc +5 -1
- package/presets/aisec.md +117 -0
- package/presets/ieee-conference.jsonc +5 -0
- package/presets/ieee-conference.md +38 -0
- package/presets/realm.jsonc +7 -2
- package/presets/realm.md +166 -0
- package/presets/tex/agenticdev.tex +1 -1
- package/presets/tex-base.jsonc +6 -0
- package/presets/tex-base.md +30 -0
- package/presets/venue-profile.schema.json +57 -2
- package/skills/find-venue/SKILL.md +6 -5
- package/skills/find-venue/SKILL.md.spec.ts +5 -4
- package/skills/paper-pipeline/SKILL.md +5 -6
- package/skills/paper-pipeline/SKILL.md.spec.ts +4 -5
- package/skills/paper-pipeline/paper-pipeline.harness.mjs +4 -0
- package/skills/paper-pipeline/references/acl-venue-rules.md +1 -1
- package/skills/study-accepted-papers/SKILL.md +3 -3
- package/skills/study-accepted-papers/SKILL.md.spec.ts +2 -2
- package/skills/submission-portal/SKILL.md +69 -0
- package/skills/submission-portal/SKILL.md.spec.ts +71 -0
- package/skills/submission-portal/submission-portal.eval.mjs +15 -0
- package/skills/submission-portal/submission-portal.test.ts +81 -0
- package/skills/submit-paper/SKILL.md +27 -22
- package/skills/submit-paper/SKILL.md.spec.ts +26 -21
- package/skills/submit-paper/references/publishers/acm.md +1 -1
- package/src/adapters/hotcrp/cassette/README.md +24 -0
- package/src/adapters/hotcrp/cassette/get-noauth.json +10 -0
- package/src/adapters/hotcrp/cassette/get-paper.json +39 -0
- package/src/adapters/hotcrp/cassette/post-dot.json +21 -0
- package/src/adapters/hotcrp/cassette/post-ok.json +12 -0
- package/src/adapters/hotcrp/index.ts +11 -0
- package/src/adapters/hotcrp/portal.io.ts +130 -0
- package/src/adapters/hotcrp/portal.test.ts +503 -0
- package/src/adapters/hotcrp/response.ts +181 -0
- package/src/adapters/latex/conditionals.ts +130 -0
- package/src/adapters/latex/headings.ts +163 -0
- package/src/adapters/latex/index.test.ts +65 -0
- package/src/adapters/latex/index.ts +3 -0
- package/src/adapters/latex/parse.ts +3 -2
- package/src/build.ts +2 -2
- package/src/cli.test.ts +155 -3
- package/src/cli.ts +161 -25
- package/src/domain/heading-case.test.ts +660 -0
- package/src/domain/heading-case.ts +538 -0
- package/src/domain/result.test.ts +9 -1
- package/src/domain/result.ts +5 -0
- package/src/domain/submission.test.ts +45 -0
- package/src/domain/submission.ts +141 -0
- package/src/domain/tex-document.ts +36 -0
- package/src/domain/venue-name.test.ts +121 -0
- package/src/domain/venue-name.ts +129 -0
- package/src/facts-file.test.ts +4 -4
- package/src/facts-file.ts +6 -6
- package/src/folder-venue-rule.test.ts +182 -0
- package/src/folder-venue-rule.ts +141 -0
- package/src/heading-case.test.ts +500 -0
- package/src/heading-case.ts +275 -0
- package/src/init.test.ts +22 -0
- package/src/init.ts +9 -2
- package/src/new-paper.harness.mjs +11 -9
- package/src/new-paper.test.ts +135 -10
- package/src/new-paper.ts +38 -7
- package/src/paper-settings-commands.test.ts +25 -10
- package/src/paper-settings.test.ts +12 -1
- package/src/paper-settings.ts +25 -5
- package/src/parse-args.test.ts +19 -0
- package/src/ports/latex.ts +7 -0
- package/src/ports/submission-portal.ts +26 -0
- package/src/preset-card-rule.test.ts +266 -0
- package/src/preset-card-rule.ts +384 -0
- package/src/presets.test.ts +212 -37
- package/src/presets.ts +183 -71
- package/src/rule-cases.test.ts +147 -42
- package/src/rules-config.ts +1 -1
- package/src/submission.test.ts +497 -0
- package/src/submission.ts +311 -0
- package/src/talk-rules.test.ts +6 -3
- package/src/tex-requirements.harness.ts +25 -11
- package/src/tex-requirements.test.ts +127 -1
- package/src/tex-requirements.ts +78 -8
- package/src/tex-venue-rules.test.ts +36 -22
- package/src/tex-venue-rules.ts +14 -26
- package/src/types.ts +13 -0
- package/src/venue-rules.test.ts +7 -6
- package/templates/paper/paper.tex +1 -1
- package/skills/submit-paper/references/venues/agenticdev.md +0 -171
- package/skills/submit-paper/references/venues/aisec.md +0 -105
- package/skills/submit-paper/references/venues/realm.md +0 -155
package/src/cli.test.ts
CHANGED
|
@@ -7,9 +7,15 @@
|
|
|
7
7
|
import assert from "node:assert/strict";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
|
+
import { createServer } from "node:http";
|
|
10
11
|
import { dirname, join } from "node:path";
|
|
11
12
|
import { test } from "vitest";
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
runNode,
|
|
15
|
+
useTempDir,
|
|
16
|
+
venuePreset,
|
|
17
|
+
writeTree,
|
|
18
|
+
} from "../test/support.ts";
|
|
13
19
|
import type { runToolchain } from "./toolchain.ts";
|
|
14
20
|
import {
|
|
15
21
|
chooseVenue,
|
|
@@ -82,6 +88,14 @@ test("parseSettings refuses `talk` in the root file: it is one paper's setting",
|
|
|
82
88
|
);
|
|
83
89
|
});
|
|
84
90
|
|
|
91
|
+
test("parseSettings refuses `submission` in the root file: a submission is one paper's", () => {
|
|
92
|
+
const r = parseSettings({ submission: { id: 7 } }, "paperlint.json", "/r");
|
|
93
|
+
assert.equal(
|
|
94
|
+
r.ok ? null : r.error,
|
|
95
|
+
"paperlint.json: \"submission\" is a paper's setting — set it in that paper's paperlint.json",
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
85
99
|
test("parseSettings refuses a non-object, names one unknown key or several, and a mistyped default", () => {
|
|
86
100
|
const error = (json: unknown) => {
|
|
87
101
|
const r = parseSettings(json, "paperlint.json", "/r");
|
|
@@ -186,8 +200,12 @@ test("runHook: a runtime with no cli.js beside it, and a hook killed by a signal
|
|
|
186
200
|
test("chooseVenue: a preset by path (inside and outside the paper), by prefixed name, and a broken one", async () => {
|
|
187
201
|
const dir = join(root, "venues");
|
|
188
202
|
writeTree(dir, {
|
|
189
|
-
"papers/p/mine.jsonc": JSON.stringify(
|
|
190
|
-
|
|
203
|
+
"papers/p/mine.jsonc": JSON.stringify(
|
|
204
|
+
venuePreset("mine", { extends: "paperlint:agenticdev" }),
|
|
205
|
+
),
|
|
206
|
+
"shared/ours.jsonc": JSON.stringify(
|
|
207
|
+
venuePreset("ours", { extends: "paperlint:agenticdev" }),
|
|
208
|
+
),
|
|
191
209
|
"shared/broken.jsonc": "{ nope",
|
|
192
210
|
});
|
|
193
211
|
const paperDir = join(dir, "papers", "p");
|
|
@@ -385,6 +403,38 @@ test("silentOptionalRules: a config with no rules turns nothing on", async () =>
|
|
|
385
403
|
assert.deepEqual(await silentOptionalRules(eslint, [], {}), []);
|
|
386
404
|
});
|
|
387
405
|
|
|
406
|
+
const lastPageOn = {
|
|
407
|
+
rules: [
|
|
408
|
+
{ basePath: ".", rules: { "pdf/last-page-balance": "error" as const } },
|
|
409
|
+
],
|
|
410
|
+
};
|
|
411
|
+
const eslintWith = (rules: Record<string, unknown>) => ({
|
|
412
|
+
calculateConfigForFile: () => Promise.resolve({ rules }),
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("silentOptionalRules: a rule on for a paper is reached", async () => {
|
|
416
|
+
const eslint = eslintWith({ "pdf/last-page-balance": [2, {}] });
|
|
417
|
+
assert.deepEqual(
|
|
418
|
+
await silentOptionalRules(eslint, ["p/paper.tex"], lastPageOn),
|
|
419
|
+
[],
|
|
420
|
+
);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("silentOptionalRules: a rule a paper's own block turns off is reached — the way out of a preset's", async () => {
|
|
424
|
+
const eslint = eslintWith({ "pdf/last-page-balance": [0] });
|
|
425
|
+
assert.deepEqual(
|
|
426
|
+
await silentOptionalRules(eslint, ["p/paper.tex"], lastPageOn),
|
|
427
|
+
[],
|
|
428
|
+
);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("silentOptionalRules: a rule no block names for any paper is silent — a glob that matched nothing", async () => {
|
|
432
|
+
assert.deepEqual(
|
|
433
|
+
await silentOptionalRules(eslintWith({}), ["p/paper.tex"], lastPageOn),
|
|
434
|
+
["pdf/last-page-balance"],
|
|
435
|
+
);
|
|
436
|
+
});
|
|
437
|
+
|
|
388
438
|
test("init with no path and --format md sets up the current directory", async () => {
|
|
389
439
|
const dir = writeTree(join(root, "init-here"), { "package.json": "{}" });
|
|
390
440
|
const r = await cli(
|
|
@@ -603,3 +653,105 @@ test("parseSettings refuses a root `identity` that is not a list of words, and t
|
|
|
603
653
|
true,
|
|
604
654
|
);
|
|
605
655
|
});
|
|
656
|
+
|
|
657
|
+
test("parseArgs: submission's flags — two values and two switches", () => {
|
|
658
|
+
assert.deepEqual(
|
|
659
|
+
parseArgs([
|
|
660
|
+
"submission",
|
|
661
|
+
"update",
|
|
662
|
+
"papers/p",
|
|
663
|
+
"--pdf=build/p.pdf",
|
|
664
|
+
"--abstract",
|
|
665
|
+
"abs.txt",
|
|
666
|
+
"--submit",
|
|
667
|
+
"--save",
|
|
668
|
+
]),
|
|
669
|
+
{
|
|
670
|
+
...parseArgs([]),
|
|
671
|
+
cmd: "submission",
|
|
672
|
+
paths: ["update", "papers/p"],
|
|
673
|
+
pdf: "build/p.pdf",
|
|
674
|
+
abstract: "abs.txt",
|
|
675
|
+
submit: true,
|
|
676
|
+
save: true,
|
|
677
|
+
},
|
|
678
|
+
);
|
|
679
|
+
assert.equal(
|
|
680
|
+
parseArgs(["submission", "update", "--pdf"]).missingValue,
|
|
681
|
+
"--pdf",
|
|
682
|
+
);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
/** A paper whose venue's HotCRP portal is `site`, submission 7, with a paper.pdf. */
|
|
686
|
+
const submissionPaper = (dir: string, site: string): void => {
|
|
687
|
+
writeTree(dir, {
|
|
688
|
+
"package.json": "{}",
|
|
689
|
+
"venue.jsonc": JSON.stringify(
|
|
690
|
+
venuePreset("venue", {
|
|
691
|
+
extends: "paperlint:aidc",
|
|
692
|
+
portal: { kind: "hotcrp", url: site },
|
|
693
|
+
}),
|
|
694
|
+
),
|
|
695
|
+
"papers/p/paperlint.json": JSON.stringify({
|
|
696
|
+
extends: "../../venue.jsonc",
|
|
697
|
+
submission: { id: 7 },
|
|
698
|
+
}),
|
|
699
|
+
"papers/p/paper.pdf": "%PDF fake",
|
|
700
|
+
});
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
test("🔴 submission: without HOTCRP_TOKEN it names the variable and asks the portal nothing", async () => {
|
|
704
|
+
const dir = join(root, "submission-no-token");
|
|
705
|
+
submissionPaper(dir, "http://127.0.0.1:9");
|
|
706
|
+
const saved = process.env["HOTCRP_TOKEN"];
|
|
707
|
+
delete process.env["HOTCRP_TOKEN"];
|
|
708
|
+
try {
|
|
709
|
+
const r = await cli(["submission", "show", "papers/p"], dir);
|
|
710
|
+
assert.equal(r.code, 2);
|
|
711
|
+
assert.match(r.err, /^HOTCRP_TOKEN is not set — /);
|
|
712
|
+
} finally {
|
|
713
|
+
if (saved !== undefined) process.env["HOTCRP_TOKEN"] = saved;
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test("submission show: through the HotCRP adapter to a local portal, the token in the header only", async () => {
|
|
718
|
+
const seen: { url: string; auth: string }[] = [];
|
|
719
|
+
const server = createServer((req, res) => {
|
|
720
|
+
seen.push({ url: req.url ?? "", auth: req.headers.authorization ?? "" });
|
|
721
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
722
|
+
res.end(
|
|
723
|
+
JSON.stringify({
|
|
724
|
+
ok: true,
|
|
725
|
+
paper: { object: "paper", pid: 7, status: "submitted", title: "T" },
|
|
726
|
+
}),
|
|
727
|
+
);
|
|
728
|
+
});
|
|
729
|
+
await new Promise<void>((r) => server.listen(0, "127.0.0.1", r));
|
|
730
|
+
const address = server.address();
|
|
731
|
+
assert.ok(address !== null && typeof address === "object");
|
|
732
|
+
const dir = join(root, "submission-show");
|
|
733
|
+
submissionPaper(dir, `http://127.0.0.1:${String(address.port)}`);
|
|
734
|
+
const saved = process.env["HOTCRP_TOKEN"];
|
|
735
|
+
process.env["HOTCRP_TOKEN"] = "hct_cli_test";
|
|
736
|
+
try {
|
|
737
|
+
const r = await cli(["submission", "show"], join(dir, "papers/p"));
|
|
738
|
+
assert.equal(r.code, 0, r.err);
|
|
739
|
+
assert.deepEqual(seen, [
|
|
740
|
+
{ url: "/api/paper?p=7&word_limit=hard", auth: "bearer hct_cli_test" },
|
|
741
|
+
]);
|
|
742
|
+
assert.match(
|
|
743
|
+
r.out,
|
|
744
|
+
/^submission 7 on http:\/\/127\.0\.0\.1:\d+ \(hotcrp\)$/m,
|
|
745
|
+
);
|
|
746
|
+
assert.match(r.out, /^match {9}NO — the portal holds no PDF$/m);
|
|
747
|
+
assert.doesNotMatch(r.out + r.err, /hct_cli_test/);
|
|
748
|
+
} finally {
|
|
749
|
+
if (saved === undefined) delete process.env["HOTCRP_TOKEN"];
|
|
750
|
+
else process.env["HOTCRP_TOKEN"] = saved;
|
|
751
|
+
await new Promise<void>((r) => {
|
|
752
|
+
server.close(() => {
|
|
753
|
+
r();
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
});
|
package/src/cli.ts
CHANGED
|
@@ -78,6 +78,7 @@ import {
|
|
|
78
78
|
REGISTER_BAND_RULE_LEVELS,
|
|
79
79
|
registerBandRules,
|
|
80
80
|
} from "./register-bands.ts";
|
|
81
|
+
import { headingCaseRule } from "./heading-case.ts";
|
|
81
82
|
import {
|
|
82
83
|
paperRules,
|
|
83
84
|
identityField,
|
|
@@ -86,6 +87,10 @@ import {
|
|
|
86
87
|
} from "./paper-settings.ts";
|
|
87
88
|
import { referenceRules, REFERENCE_RULE_LEVELS } from "./reference-rules.ts";
|
|
88
89
|
import { onlineReferences } from "./adapters/references/index.ts";
|
|
90
|
+
import { hotcrpPortal } from "./adapters/hotcrp/index.ts";
|
|
91
|
+
import { runSubmission } from "./submission.ts";
|
|
92
|
+
import type { PortalKind, SupportedPortal } from "./domain/submission.ts";
|
|
93
|
+
import type { SubmissionPortal } from "./ports/submission-portal.ts";
|
|
89
94
|
import type { CheckReferences } from "./ports/check-references.ts";
|
|
90
95
|
import {
|
|
91
96
|
narrowToOwners,
|
|
@@ -99,6 +104,8 @@ import {
|
|
|
99
104
|
presetProblemText,
|
|
100
105
|
resolvePreset,
|
|
101
106
|
shippedPresets,
|
|
107
|
+
shippedSpec,
|
|
108
|
+
shippedVenueNames,
|
|
102
109
|
SHIPPED_PREFIX,
|
|
103
110
|
type PaperPreset,
|
|
104
111
|
type PresetTemplate,
|
|
@@ -152,6 +159,10 @@ export { init };
|
|
|
152
159
|
export { nextSteps } from "./init.ts";
|
|
153
160
|
|
|
154
161
|
import paperStages from "#eslint-rules/paper-stages";
|
|
162
|
+
import {
|
|
163
|
+
FOLDER_VENUE_RULE_LEVELS,
|
|
164
|
+
folderVenueRules,
|
|
165
|
+
} from "./folder-venue-rule.ts";
|
|
155
166
|
import typography from "#eslint-rules/paper-typography";
|
|
156
167
|
import texBuild from "#eslint-rules/tex-build";
|
|
157
168
|
import bibReachable from "#eslint-rules/bib-reachable-entry";
|
|
@@ -171,7 +182,9 @@ const USAGE = `paperlint — machine-checkable gates for a paper kept in git
|
|
|
171
182
|
.claude/settings.json, offer the CI step, report what is missing
|
|
172
183
|
npx paperlint new <name> [--venue <preset>] [--kind <kind>] [--format tex|md]
|
|
173
184
|
create <papers>/<name>/ from the template; never overwrites,
|
|
174
|
-
on an existing folder adds only the missing files, then lints it
|
|
185
|
+
on an existing folder adds only the missing files, then lints it.
|
|
186
|
+
A <name> that names a venue (aisec-2026) is refused: name the
|
|
187
|
+
paper after the work
|
|
175
188
|
npx paperlint lint [paths…] run every rule over your papers
|
|
176
189
|
npx paperlint build <paper> | --all
|
|
177
190
|
compile paper.tex to paper.pdf: pdflatex and bibtex, rerun until
|
|
@@ -187,6 +200,14 @@ const USAGE = `paperlint — machine-checkable gates for a paper kept in git
|
|
|
187
200
|
into ~/.cache/paperlint/texlive (PAPERLINT_TEXLIVE_DIR overrides); a second
|
|
188
201
|
run does nothing. --check: report what is missing, change nothing
|
|
189
202
|
npx paperlint doctor say what is actually wired — and what only LOOKS wired
|
|
203
|
+
npx paperlint submission show [paper]
|
|
204
|
+
the paper's submission on the venue's portal: status, title,
|
|
205
|
+
type, topics, abstract length, the PDF the portal holds, and
|
|
206
|
+
whether it is the local paper.pdf (sha256)
|
|
207
|
+
npx paperlint submission update [paper] [--pdf <file>] [--abstract <file>] [--submit] [--save]
|
|
208
|
+
send the PDF (default: the paper's own), an abstract, or
|
|
209
|
+
"submitted". A DRY RUN unless --save: the portal checks the
|
|
210
|
+
change and keeps nothing
|
|
190
211
|
npx paperlint hook <name> run an editor hook (.claude/settings.json calls this)
|
|
191
212
|
npx paperlint --help
|
|
192
213
|
|
|
@@ -205,6 +226,19 @@ new:
|
|
|
205
226
|
--kind <kind> the paper's kind at that venue (its page limit), e.g. short — one of the
|
|
206
227
|
preset's kinds; needs --venue
|
|
207
228
|
--format tex|md the paper's source format; default tex
|
|
229
|
+
--allow-venue-name create the folder even though its name names a venue (a whole word such as
|
|
230
|
+
"realm" can be the work's own). Without it such a name is refused: a rejected
|
|
231
|
+
paper moves to another venue and keeps its folder
|
|
232
|
+
|
|
233
|
+
submission:
|
|
234
|
+
The venue preset declares the portal — "portal": { "kind": "hotcrp", "url": "<site>" } — and the
|
|
235
|
+
paper's paperlint.json its submission there — "submission": { "id": <number> }. The token comes
|
|
236
|
+
only from the environment: HOTCRP_TOKEN (HotCRP: Account settings → Developer).
|
|
237
|
+
[paper] the paper's folder; default the current directory
|
|
238
|
+
--pdf <file> the PDF to compare (show) or send (update) instead of the paper's own
|
|
239
|
+
--abstract <file> update: a plain-text file whose text replaces the abstract
|
|
240
|
+
--submit update: mark the submission submitted
|
|
241
|
+
--save update: really change the submission. Without it, nothing is saved
|
|
208
242
|
|
|
209
243
|
lint:
|
|
210
244
|
npx paperlint lint [paths…] [--fix] [--config <file.json>] [--json]
|
|
@@ -258,6 +292,18 @@ const TEX_RULE_DEPS = {
|
|
|
258
292
|
latex: latexReader,
|
|
259
293
|
};
|
|
260
294
|
|
|
295
|
+
/** The register band rules and `tex/heading-case`: the rules of the `tex` plugin that read a paper's words. */
|
|
296
|
+
const REGISTER_AND_CASE_RULES = {
|
|
297
|
+
...registerBandRules(TEX_RULE_DEPS),
|
|
298
|
+
"heading-case": headingCaseRule(TEX_RULE_DEPS),
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
/** The rules of the files `paper.tex` includes, each read on its own as a fragment (#144). */
|
|
302
|
+
const FRAGMENT_RULES = {
|
|
303
|
+
"future-promise": texBuild["future-promise"],
|
|
304
|
+
"heading-case": REGISTER_AND_CASE_RULES["heading-case"],
|
|
305
|
+
};
|
|
306
|
+
|
|
261
307
|
/** What the talk rules read with: the disk, the shipped presets, and the three media readers. */
|
|
262
308
|
const TALK_RULE_DEPS = {
|
|
263
309
|
files: nodeFiles,
|
|
@@ -313,11 +359,22 @@ export function buildConfig(
|
|
|
313
359
|
},
|
|
314
360
|
{
|
|
315
361
|
files: ["**/PIPELINE-STATUS.md"],
|
|
316
|
-
|
|
362
|
+
// Every paper folder has one, whatever its source format: the rules about the folder itself
|
|
363
|
+
// (its stages, its frozen sources, its name) are judged here, once per paper.
|
|
364
|
+
plugins: {
|
|
365
|
+
markdown,
|
|
366
|
+
paper: {
|
|
367
|
+
rules: {
|
|
368
|
+
...paperStages.rules,
|
|
369
|
+
...folderVenueRules({ files: nodeFiles, venuesDir: presetsDir() }),
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
},
|
|
317
373
|
...md,
|
|
318
374
|
rules: {
|
|
319
375
|
"paper/stages": "error",
|
|
320
376
|
"paper/source": "error",
|
|
377
|
+
...FOLDER_VENUE_RULE_LEVELS,
|
|
321
378
|
},
|
|
322
379
|
},
|
|
323
380
|
{
|
|
@@ -358,7 +415,7 @@ export function buildConfig(
|
|
|
358
415
|
...texVenueRules(TEX_RULE_DEPS),
|
|
359
416
|
"claim-provenance": claimProvenanceRule(TEX_RULE_DEPS),
|
|
360
417
|
register: registerRule(TEX_RULE_DEPS),
|
|
361
|
-
...
|
|
418
|
+
...REGISTER_AND_CASE_RULES,
|
|
362
419
|
},
|
|
363
420
|
},
|
|
364
421
|
paper: { rules: texPaperRules },
|
|
@@ -410,7 +467,7 @@ export function buildConfig(
|
|
|
410
467
|
plugins: {
|
|
411
468
|
tex: {
|
|
412
469
|
languages: { latex: texLanguage },
|
|
413
|
-
rules:
|
|
470
|
+
rules: FRAGMENT_RULES,
|
|
414
471
|
},
|
|
415
472
|
paper: {
|
|
416
473
|
rules: {
|
|
@@ -579,7 +636,11 @@ export async function silentOptionalRules(
|
|
|
579
636
|
for (const f of new Set(files.filter((p) => basename(p) === MAIN))) {
|
|
580
637
|
// Undefined for a file outside ESLint's cwd or scope: it reaches nothing.
|
|
581
638
|
const cfg = ComputedConfig.parse(await eslint.calculateConfigForFile(f));
|
|
582
|
-
for
|
|
639
|
+
// Reached is "some block names the rule for this paper", on OR off: a paper that turns off what
|
|
640
|
+
// its venue preset turned on is the documented way out, and reads the same as a glob that missed
|
|
641
|
+
// only if "off" is counted as "not there" — which refused every such paper.
|
|
642
|
+
for (const id of turnedOn)
|
|
643
|
+
if (cfg?.rules?.[id] !== undefined) reached.add(id);
|
|
583
644
|
}
|
|
584
645
|
return [...turnedOn].filter((id) => !reached.has(id));
|
|
585
646
|
}
|
|
@@ -781,10 +842,15 @@ export function parseArgs(argv: readonly string[]): Args {
|
|
|
781
842
|
check: false,
|
|
782
843
|
yes: false,
|
|
783
844
|
noHooks: false,
|
|
845
|
+
allowVenueName: false,
|
|
784
846
|
paper: null,
|
|
785
847
|
format: null,
|
|
786
848
|
venue: null,
|
|
787
849
|
kind: null,
|
|
850
|
+
pdf: null,
|
|
851
|
+
abstract: null,
|
|
852
|
+
submit: false,
|
|
853
|
+
save: false,
|
|
788
854
|
hooksMode: null,
|
|
789
855
|
// -1 = warnings NEVER fail the run. In this set most findings are advisory by design, and a
|
|
790
856
|
// gate that fails on advice gets muted entirely.
|
|
@@ -824,19 +890,14 @@ export function parseArgs(argv: readonly string[]): Args {
|
|
|
824
890
|
if (inline === "") out.missingValue = a;
|
|
825
891
|
return inline === "" ? undefined : inline;
|
|
826
892
|
};
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
else if (a === "--all") out.all = true;
|
|
830
|
-
else if (a === "--dry-run") out.dryRun = true;
|
|
831
|
-
else if (a === "--check") out.check = true;
|
|
832
|
-
else if (a === "--yes" || a === "-y") out.yes = true;
|
|
833
|
-
else if (a === "--no-hooks") out.noHooks = true;
|
|
893
|
+
const on = SWITCHES.get(a);
|
|
894
|
+
if (on !== undefined) out[on] = true;
|
|
834
895
|
else if (a.startsWith("--hooks="))
|
|
835
896
|
out.hooksMode = a.slice("--hooks=".length);
|
|
836
|
-
else if (
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
897
|
+
else if (STRING_FLAGS.has(a)) {
|
|
898
|
+
const field = STRING_FLAGS.get(a);
|
|
899
|
+
if (field !== undefined) out[field] = take() ?? null;
|
|
900
|
+
}
|
|
840
901
|
// `--options` was the first spelling and is kept working. It named the wrong thing — every
|
|
841
902
|
// other tool in the stack calls this file its config — but a flag in someone's CI is not
|
|
842
903
|
// ours to break.
|
|
@@ -852,12 +913,49 @@ export function parseArgs(argv: readonly string[]): Args {
|
|
|
852
913
|
return out;
|
|
853
914
|
}
|
|
854
915
|
|
|
916
|
+
/** The flags that take no value: each turns one field of `Args` on. */
|
|
917
|
+
const SWITCHES: ReadonlyMap<
|
|
918
|
+
string,
|
|
919
|
+
| "json"
|
|
920
|
+
| "fix"
|
|
921
|
+
| "all"
|
|
922
|
+
| "dryRun"
|
|
923
|
+
| "check"
|
|
924
|
+
| "yes"
|
|
925
|
+
| "noHooks"
|
|
926
|
+
| "allowVenueName"
|
|
927
|
+
| "submit"
|
|
928
|
+
| "save"
|
|
929
|
+
> = new Map([
|
|
930
|
+
["--json", "json"],
|
|
931
|
+
["--fix", "fix"],
|
|
932
|
+
["--all", "all"],
|
|
933
|
+
["--dry-run", "dryRun"],
|
|
934
|
+
["--check", "check"],
|
|
935
|
+
["--yes", "yes"],
|
|
936
|
+
["-y", "yes"],
|
|
937
|
+
["--no-hooks", "noHooks"],
|
|
938
|
+
["--allow-venue-name", "allowVenueName"],
|
|
939
|
+
["--submit", "submit"],
|
|
940
|
+
["--save", "save"],
|
|
941
|
+
]);
|
|
942
|
+
|
|
943
|
+
/** The flags whose value is one string field of `Args`. */
|
|
944
|
+
const STRING_FLAGS: ReadonlyMap<
|
|
945
|
+
string,
|
|
946
|
+
"paper" | "format" | "venue" | "kind" | "pdf" | "abstract"
|
|
947
|
+
> = new Map([
|
|
948
|
+
["--paper", "paper"],
|
|
949
|
+
["--format", "format"],
|
|
950
|
+
["--venue", "venue"],
|
|
951
|
+
["--kind", "kind"],
|
|
952
|
+
["--pdf", "pdf"],
|
|
953
|
+
["--abstract", "abstract"],
|
|
954
|
+
]);
|
|
955
|
+
|
|
855
956
|
/** The flags that take a value, in either spelling: `--flag value` or `--flag=value`. */
|
|
856
957
|
const VALUE_FLAGS: ReadonlySet<string> = new Set([
|
|
857
|
-
|
|
858
|
-
"--format",
|
|
859
|
-
"--venue",
|
|
860
|
-
"--kind",
|
|
958
|
+
...STRING_FLAGS.keys(),
|
|
861
959
|
"--config",
|
|
862
960
|
"--options",
|
|
863
961
|
"--max-warnings",
|
|
@@ -1048,15 +1146,21 @@ export async function createPaperAt(
|
|
|
1048
1146
|
err,
|
|
1049
1147
|
cwd,
|
|
1050
1148
|
venue = null,
|
|
1149
|
+
allowVenueName = false,
|
|
1051
1150
|
}: {
|
|
1052
1151
|
log: typeof console.log;
|
|
1053
1152
|
err: typeof console.error;
|
|
1054
1153
|
cwd: string;
|
|
1055
1154
|
/** What `--venue` chose; null writes the template's `paperlint.json` as it is. */
|
|
1056
1155
|
venue?: VenueChoice | null;
|
|
1156
|
+
/** `--allow-venue-name`: a name that names a venue is created, not refused. */
|
|
1157
|
+
allowVenueName?: boolean;
|
|
1057
1158
|
},
|
|
1058
1159
|
): Promise<number> {
|
|
1059
|
-
const result = newPaper(papersRoot, name, format, {
|
|
1160
|
+
const result = newPaper(papersRoot, name, format, {
|
|
1161
|
+
venue,
|
|
1162
|
+
venues: allowVenueName ? [] : shippedVenueNames(PRESET_DEPS),
|
|
1163
|
+
});
|
|
1060
1164
|
const here = (p: string): string => shown(cwd, p);
|
|
1061
1165
|
for (const line of reportNewPaper(result, here)) log(line);
|
|
1062
1166
|
if (!result.ok) return 2;
|
|
@@ -1075,7 +1179,7 @@ export async function createPaperAt(
|
|
|
1075
1179
|
|
|
1076
1180
|
/** A venue `paperlint new` will write, with what the messages need to say about it. */
|
|
1077
1181
|
export interface VenueChoice extends VenueSetting {
|
|
1078
|
-
/** The preset's word in messages (`
|
|
1182
|
+
/** The preset's word in messages: the venue's name (`AgenticDev`), else the file name (`my-workshop`). */
|
|
1079
1183
|
readonly label: string;
|
|
1080
1184
|
/** The preset's kinds; empty when it sets no page limit. */
|
|
1081
1185
|
readonly kinds: readonly string[];
|
|
@@ -1194,7 +1298,7 @@ function venueSpec(
|
|
|
1194
1298
|
return bad(
|
|
1195
1299
|
`--venue ${venue}: no such venue preset. Shipped: ${shipped.join(", ")} — or a path to your own preset, starting with ./ or ../`,
|
|
1196
1300
|
);
|
|
1197
|
-
return { ok: true, value:
|
|
1301
|
+
return { ok: true, value: shippedSpec(name) };
|
|
1198
1302
|
}
|
|
1199
1303
|
|
|
1200
1304
|
/**
|
|
@@ -1274,6 +1378,7 @@ async function runNew(
|
|
|
1274
1378
|
err,
|
|
1275
1379
|
cwd,
|
|
1276
1380
|
venue: venue.value,
|
|
1381
|
+
allowVenueName: a.allowVenueName,
|
|
1277
1382
|
});
|
|
1278
1383
|
}
|
|
1279
1384
|
|
|
@@ -1420,7 +1525,37 @@ const SIMPLE: Readonly<
|
|
|
1420
1525
|
banal: hostBanalInstaller(),
|
|
1421
1526
|
tex: toolchainTex(cwd),
|
|
1422
1527
|
}),
|
|
1528
|
+
submission: (a, { log, err, cwd }) =>
|
|
1529
|
+
runSubmission(
|
|
1530
|
+
{
|
|
1531
|
+
sub: a.paths[0],
|
|
1532
|
+
paperDir: a.paths[1] ?? ".",
|
|
1533
|
+
extra: a.paths.slice(2),
|
|
1534
|
+
pdf: a.pdf,
|
|
1535
|
+
abstract: a.abstract,
|
|
1536
|
+
submit: a.submit,
|
|
1537
|
+
save: a.save,
|
|
1538
|
+
},
|
|
1539
|
+
{
|
|
1540
|
+
files: nodeFiles,
|
|
1541
|
+
presets: PRESET_DEPS,
|
|
1542
|
+
cwd,
|
|
1543
|
+
env: (name) => process.env[name],
|
|
1544
|
+
portalFor,
|
|
1545
|
+
log,
|
|
1546
|
+
err,
|
|
1547
|
+
},
|
|
1548
|
+
),
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1551
|
+
/** The adapter of each portal kind — one per kind, chosen here at the root. */
|
|
1552
|
+
const PORTALS: Readonly<
|
|
1553
|
+
Record<PortalKind, (url: string, token: string) => SubmissionPortal>
|
|
1554
|
+
> = {
|
|
1555
|
+
hotcrp: (url, token) => hotcrpPortal({ url, token }),
|
|
1423
1556
|
};
|
|
1557
|
+
const portalFor = (p: SupportedPortal, token: string): SubmissionPortal =>
|
|
1558
|
+
PORTALS[p.kind](p.url, token);
|
|
1424
1559
|
|
|
1425
1560
|
/** banal's installer, wired from this process's environment: the composition root's work. */
|
|
1426
1561
|
function hostBanalInstaller(): ToolInstaller {
|
|
@@ -1503,6 +1638,7 @@ async function runInit(
|
|
|
1503
1638
|
format: isFormat(a.format) ? a.format : null,
|
|
1504
1639
|
createPaper: (papersRoot, name, format) =>
|
|
1505
1640
|
createPaperAt(papersRoot, name, format, { log, err, cwd }),
|
|
1641
|
+
venues: shippedVenueNames(PRESET_DEPS),
|
|
1506
1642
|
tex: initTexLive(a, { log, err, cwd }),
|
|
1507
1643
|
resolveCliPapers: (root: string): string | null =>
|
|
1508
1644
|
cliPapers({ ...a, config: null }, root),
|
|
@@ -1584,8 +1720,8 @@ export async function run(
|
|
|
1584
1720
|
// editing a file.
|
|
1585
1721
|
//
|
|
1586
1722
|
// 🔴 A path FROM THE CONFIG is resolved relative to the PROJECT ROOT, not the current directory.
|
|
1587
|
-
// Otherwise walking up is pointless: from `papers/
|
|
1588
|
-
// `"papersDir": "papers"` would point at `papers/
|
|
1723
|
+
// Otherwise walking up is pointless: from `papers/rule-drift` the root would be found, but
|
|
1724
|
+
// `"papersDir": "papers"` would point at `papers/rule-drift/papers`. A command-line argument stays
|
|
1589
1725
|
// relative to the current directory: it was typed here and now.
|
|
1590
1726
|
//
|
|
1591
1727
|
// Both kinds end up ABSOLUTE: ESLint below runs from `lintRoot`, not from here, and would resolve a
|