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.
Files changed (234) hide show
  1. package/CONTRIBUTING.md +4 -2
  2. package/README.md +10 -3
  3. package/dist/adapters/hotcrp/index.d.ts +7 -0
  4. package/dist/adapters/hotcrp/index.d.ts.map +1 -0
  5. package/dist/adapters/hotcrp/index.js +7 -0
  6. package/dist/adapters/hotcrp/index.js.map +1 -0
  7. package/dist/adapters/hotcrp/portal.io.d.ts +20 -0
  8. package/dist/adapters/hotcrp/portal.io.d.ts.map +1 -0
  9. package/dist/adapters/hotcrp/portal.io.js +90 -0
  10. package/dist/adapters/hotcrp/portal.io.js.map +1 -0
  11. package/dist/adapters/hotcrp/response.d.ts +13 -0
  12. package/dist/adapters/hotcrp/response.d.ts.map +1 -0
  13. package/dist/adapters/hotcrp/response.js +138 -0
  14. package/dist/adapters/hotcrp/response.js.map +1 -0
  15. package/dist/adapters/latex/conditionals.d.ts +21 -0
  16. package/dist/adapters/latex/conditionals.d.ts.map +1 -0
  17. package/dist/adapters/latex/conditionals.js +75 -0
  18. package/dist/adapters/latex/conditionals.js.map +1 -0
  19. package/dist/adapters/latex/headings.d.ts +10 -0
  20. package/dist/adapters/latex/headings.d.ts.map +1 -0
  21. package/dist/adapters/latex/headings.js +129 -0
  22. package/dist/adapters/latex/headings.js.map +1 -0
  23. package/dist/adapters/latex/index.d.ts +1 -0
  24. package/dist/adapters/latex/index.d.ts.map +1 -1
  25. package/dist/adapters/latex/index.js +3 -0
  26. package/dist/adapters/latex/index.js.map +1 -1
  27. package/dist/adapters/latex/parse.d.ts +1 -1
  28. package/dist/adapters/latex/parse.d.ts.map +1 -1
  29. package/dist/adapters/latex/parse.js +3 -2
  30. package/dist/adapters/latex/parse.js.map +1 -1
  31. package/dist/build.d.ts +1 -1
  32. package/dist/build.d.ts.map +1 -1
  33. package/dist/build.js +1 -1
  34. package/dist/build.js.map +1 -1
  35. package/dist/cli.d.ts +4 -2
  36. package/dist/cli.d.ts.map +1 -1
  37. package/dist/cli.js +126 -37
  38. package/dist/cli.js.map +1 -1
  39. package/dist/domain/heading-case.d.ts +100 -0
  40. package/dist/domain/heading-case.d.ts.map +1 -0
  41. package/dist/domain/heading-case.js +365 -0
  42. package/dist/domain/heading-case.js.map +1 -0
  43. package/dist/domain/result.d.ts +4 -0
  44. package/dist/domain/result.d.ts.map +1 -1
  45. package/dist/domain/result.js +2 -0
  46. package/dist/domain/result.js.map +1 -1
  47. package/dist/domain/submission.d.ts +106 -0
  48. package/dist/domain/submission.d.ts.map +1 -0
  49. package/dist/domain/submission.js +39 -0
  50. package/dist/domain/submission.js.map +1 -0
  51. package/dist/domain/tex-document.d.ts +32 -0
  52. package/dist/domain/tex-document.d.ts.map +1 -1
  53. package/dist/domain/tex-document.js +11 -0
  54. package/dist/domain/tex-document.js.map +1 -1
  55. package/dist/domain/venue-name.d.ts +38 -0
  56. package/dist/domain/venue-name.d.ts.map +1 -0
  57. package/dist/domain/venue-name.js +71 -0
  58. package/dist/domain/venue-name.js.map +1 -0
  59. package/dist/eslint-rules/latex-language.d.ts.map +1 -1
  60. package/dist/eslint-rules/latex-language.js +16 -3
  61. package/dist/eslint-rules/latex-language.js.map +1 -1
  62. package/dist/facts-file.d.ts +3 -3
  63. package/dist/facts-file.d.ts.map +1 -1
  64. package/dist/facts-file.js +3 -3
  65. package/dist/facts-file.js.map +1 -1
  66. package/dist/folder-venue-rule.d.ts +51 -0
  67. package/dist/folder-venue-rule.d.ts.map +1 -0
  68. package/dist/folder-venue-rule.js +96 -0
  69. package/dist/folder-venue-rule.js.map +1 -0
  70. package/dist/heading-case.d.ts +48 -0
  71. package/dist/heading-case.d.ts.map +1 -0
  72. package/dist/heading-case.js +191 -0
  73. package/dist/heading-case.js.map +1 -0
  74. package/dist/init.d.ts +6 -0
  75. package/dist/init.d.ts.map +1 -1
  76. package/dist/init.js +3 -3
  77. package/dist/init.js.map +1 -1
  78. package/dist/lib/paper-config.d.ts.map +1 -1
  79. package/dist/lib/paper-config.js +5 -1
  80. package/dist/lib/paper-config.js.map +1 -1
  81. package/dist/new-paper.d.ts +19 -5
  82. package/dist/new-paper.d.ts.map +1 -1
  83. package/dist/new-paper.js +23 -4
  84. package/dist/new-paper.js.map +1 -1
  85. package/dist/paper-settings.d.ts +3 -0
  86. package/dist/paper-settings.d.ts.map +1 -1
  87. package/dist/paper-settings.js +20 -4
  88. package/dist/paper-settings.js.map +1 -1
  89. package/dist/ports/latex.d.ts +7 -1
  90. package/dist/ports/latex.d.ts.map +1 -1
  91. package/dist/ports/submission-portal.d.ts +19 -0
  92. package/dist/ports/submission-portal.d.ts.map +1 -0
  93. package/dist/ports/submission-portal.js +2 -0
  94. package/dist/ports/submission-portal.js.map +1 -0
  95. package/dist/preset-card-rule.d.ts +100 -0
  96. package/dist/preset-card-rule.d.ts.map +1 -0
  97. package/dist/preset-card-rule.js +221 -0
  98. package/dist/preset-card-rule.js.map +1 -0
  99. package/dist/presets.d.ts +43 -2
  100. package/dist/presets.d.ts.map +1 -1
  101. package/dist/presets.js +130 -63
  102. package/dist/presets.js.map +1 -1
  103. package/dist/rules-config.js +1 -1
  104. package/dist/submission.d.ts +61 -0
  105. package/dist/submission.d.ts.map +1 -0
  106. package/dist/submission.js +211 -0
  107. package/dist/submission.js.map +1 -0
  108. package/dist/tex-requirements.d.ts +36 -3
  109. package/dist/tex-requirements.d.ts.map +1 -1
  110. package/dist/tex-requirements.js +30 -2
  111. package/dist/tex-requirements.js.map +1 -1
  112. package/dist/tex-venue-rules.d.ts +7 -8
  113. package/dist/tex-venue-rules.d.ts.map +1 -1
  114. package/dist/tex-venue-rules.js +10 -14
  115. package/dist/tex-venue-rules.js.map +1 -1
  116. package/dist/types.d.ts +13 -0
  117. package/dist/types.d.ts.map +1 -1
  118. package/docs/configuration.md +30 -11
  119. package/docs/optional-rules.md +4 -3
  120. package/docs/rules/format/page-limit.md +1 -1
  121. package/docs/rules/paper/folder-venue-leftover.md +104 -0
  122. package/docs/rules/talk/captions-cover.md +1 -1
  123. package/docs/rules/talk/duration-floor.md +1 -1
  124. package/docs/rules/talk/profile.md +1 -1
  125. package/docs/rules/tex/heading-case.md +424 -0
  126. package/docs/rules.md +64 -53
  127. package/docs/skills.md +5 -4
  128. package/docs/submission.md +64 -0
  129. package/docs/talk.md +1 -1
  130. package/eslint-rules/latex-language.test.mjs +12 -0
  131. package/eslint-rules/latex-language.ts +17 -4
  132. package/eslint-rules/paper-typography.test.mjs +12 -0
  133. package/eslint.config.mjs +59 -1
  134. package/fixtures/build-e2e/acmart/paper.tex +1 -1
  135. package/fixtures/build-e2e/fallback/expect.json +3 -3
  136. package/lib/paper-config.ts +6 -1
  137. package/lib/skill-checks.d.mts +7 -0
  138. package/lib/skill-checks.mjs +31 -0
  139. package/lib/skill-trigger-cases.mjs +11 -0
  140. package/package.json +8 -2
  141. package/presets/acm-sigconf.jsonc +5 -0
  142. package/presets/acm-sigconf.md +43 -0
  143. package/presets/agenticdev.jsonc +27 -6
  144. package/presets/agenticdev.md +151 -0
  145. package/presets/aidc.jsonc +12 -0
  146. package/presets/aidc.md +75 -0
  147. package/presets/aisec.jsonc +5 -1
  148. package/presets/aisec.md +117 -0
  149. package/presets/ieee-conference.jsonc +5 -0
  150. package/presets/ieee-conference.md +38 -0
  151. package/presets/realm.jsonc +7 -2
  152. package/presets/realm.md +166 -0
  153. package/presets/tex/agenticdev.tex +1 -1
  154. package/presets/tex-base.jsonc +6 -0
  155. package/presets/tex-base.md +30 -0
  156. package/presets/venue-profile.schema.json +57 -2
  157. package/skills/find-venue/SKILL.md +6 -5
  158. package/skills/find-venue/SKILL.md.spec.ts +5 -4
  159. package/skills/paper-pipeline/SKILL.md +5 -6
  160. package/skills/paper-pipeline/SKILL.md.spec.ts +4 -5
  161. package/skills/paper-pipeline/paper-pipeline.harness.mjs +4 -0
  162. package/skills/paper-pipeline/references/acl-venue-rules.md +1 -1
  163. package/skills/study-accepted-papers/SKILL.md +3 -3
  164. package/skills/study-accepted-papers/SKILL.md.spec.ts +2 -2
  165. package/skills/submission-portal/SKILL.md +69 -0
  166. package/skills/submission-portal/SKILL.md.spec.ts +71 -0
  167. package/skills/submission-portal/submission-portal.eval.mjs +15 -0
  168. package/skills/submission-portal/submission-portal.test.ts +81 -0
  169. package/skills/submit-paper/SKILL.md +27 -22
  170. package/skills/submit-paper/SKILL.md.spec.ts +26 -21
  171. package/skills/submit-paper/references/publishers/acm.md +1 -1
  172. package/src/adapters/hotcrp/cassette/README.md +24 -0
  173. package/src/adapters/hotcrp/cassette/get-noauth.json +10 -0
  174. package/src/adapters/hotcrp/cassette/get-paper.json +39 -0
  175. package/src/adapters/hotcrp/cassette/post-dot.json +21 -0
  176. package/src/adapters/hotcrp/cassette/post-ok.json +12 -0
  177. package/src/adapters/hotcrp/index.ts +11 -0
  178. package/src/adapters/hotcrp/portal.io.ts +130 -0
  179. package/src/adapters/hotcrp/portal.test.ts +503 -0
  180. package/src/adapters/hotcrp/response.ts +181 -0
  181. package/src/adapters/latex/conditionals.ts +130 -0
  182. package/src/adapters/latex/headings.ts +163 -0
  183. package/src/adapters/latex/index.test.ts +65 -0
  184. package/src/adapters/latex/index.ts +3 -0
  185. package/src/adapters/latex/parse.ts +3 -2
  186. package/src/build.ts +2 -2
  187. package/src/cli.test.ts +155 -3
  188. package/src/cli.ts +161 -25
  189. package/src/domain/heading-case.test.ts +660 -0
  190. package/src/domain/heading-case.ts +538 -0
  191. package/src/domain/result.test.ts +9 -1
  192. package/src/domain/result.ts +5 -0
  193. package/src/domain/submission.test.ts +45 -0
  194. package/src/domain/submission.ts +141 -0
  195. package/src/domain/tex-document.ts +36 -0
  196. package/src/domain/venue-name.test.ts +121 -0
  197. package/src/domain/venue-name.ts +129 -0
  198. package/src/facts-file.test.ts +4 -4
  199. package/src/facts-file.ts +6 -6
  200. package/src/folder-venue-rule.test.ts +182 -0
  201. package/src/folder-venue-rule.ts +141 -0
  202. package/src/heading-case.test.ts +500 -0
  203. package/src/heading-case.ts +275 -0
  204. package/src/init.test.ts +22 -0
  205. package/src/init.ts +9 -2
  206. package/src/new-paper.harness.mjs +11 -9
  207. package/src/new-paper.test.ts +135 -10
  208. package/src/new-paper.ts +38 -7
  209. package/src/paper-settings-commands.test.ts +25 -10
  210. package/src/paper-settings.test.ts +12 -1
  211. package/src/paper-settings.ts +25 -5
  212. package/src/parse-args.test.ts +19 -0
  213. package/src/ports/latex.ts +7 -0
  214. package/src/ports/submission-portal.ts +26 -0
  215. package/src/preset-card-rule.test.ts +266 -0
  216. package/src/preset-card-rule.ts +384 -0
  217. package/src/presets.test.ts +212 -37
  218. package/src/presets.ts +183 -71
  219. package/src/rule-cases.test.ts +147 -42
  220. package/src/rules-config.ts +1 -1
  221. package/src/submission.test.ts +497 -0
  222. package/src/submission.ts +311 -0
  223. package/src/talk-rules.test.ts +6 -3
  224. package/src/tex-requirements.harness.ts +25 -11
  225. package/src/tex-requirements.test.ts +127 -1
  226. package/src/tex-requirements.ts +78 -8
  227. package/src/tex-venue-rules.test.ts +36 -22
  228. package/src/tex-venue-rules.ts +14 -26
  229. package/src/types.ts +13 -0
  230. package/src/venue-rules.test.ts +7 -6
  231. package/templates/paper/paper.tex +1 -1
  232. package/skills/submit-paper/references/venues/agenticdev.md +0 -171
  233. package/skills/submit-paper/references/venues/aisec.md +0 -105
  234. 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 { runNode, useTempDir, writeTree } from "../test/support.ts";
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({ extends: "paperlint:agenticdev" }),
190
- "shared/ours.jsonc": JSON.stringify({ extends: "paperlint:agenticdev" }),
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
- plugins: { markdown, paper: paperStages },
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
- ...registerBandRules(TEX_RULE_DEPS),
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: { "future-promise": texBuild["future-promise"] },
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 (const id of turnedOn) if (isOn(cfg?.rules?.[id])) reached.add(id);
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
- if (a === "--json") out.json = true;
828
- else if (a === "--fix") out.fix = true;
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 (a === "--paper") out.paper = take() ?? null;
837
- else if (a === "--format") out.format = take() ?? null;
838
- else if (a === "--venue") out.venue = take() ?? null;
839
- else if (a === "--kind") out.kind = take() ?? null;
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
- "--paper",
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, { venue });
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 (`agenticdev`, `my-workshop`). */
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: `${SHIPPED_PREFIX}${name}` };
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/aisec-2026` the root would be found, but
1588
- // `"papersDir": "papers"` would point at `papers/aisec-2026/papers`. A command-line argument stays
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