paperlint 6.2.2 → 6.3.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 (134) hide show
  1. package/README.md +8 -1
  2. package/dist/adapters/hotcrp/index.d.ts +7 -0
  3. package/dist/adapters/hotcrp/index.d.ts.map +1 -0
  4. package/dist/adapters/hotcrp/index.js +7 -0
  5. package/dist/adapters/hotcrp/index.js.map +1 -0
  6. package/dist/adapters/hotcrp/portal.io.d.ts +20 -0
  7. package/dist/adapters/hotcrp/portal.io.d.ts.map +1 -0
  8. package/dist/adapters/hotcrp/portal.io.js +90 -0
  9. package/dist/adapters/hotcrp/portal.io.js.map +1 -0
  10. package/dist/adapters/hotcrp/response.d.ts +13 -0
  11. package/dist/adapters/hotcrp/response.d.ts.map +1 -0
  12. package/dist/adapters/hotcrp/response.js +138 -0
  13. package/dist/adapters/hotcrp/response.js.map +1 -0
  14. package/dist/cli.d.ts +3 -1
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +109 -34
  17. package/dist/cli.js.map +1 -1
  18. package/dist/domain/result.d.ts +4 -0
  19. package/dist/domain/result.d.ts.map +1 -1
  20. package/dist/domain/result.js +2 -0
  21. package/dist/domain/result.js.map +1 -1
  22. package/dist/domain/submission.d.ts +106 -0
  23. package/dist/domain/submission.d.ts.map +1 -0
  24. package/dist/domain/submission.js +39 -0
  25. package/dist/domain/submission.js.map +1 -0
  26. package/dist/domain/venue-name.d.ts +38 -0
  27. package/dist/domain/venue-name.d.ts.map +1 -0
  28. package/dist/domain/venue-name.js +71 -0
  29. package/dist/domain/venue-name.js.map +1 -0
  30. package/dist/folder-venue-rule.d.ts +51 -0
  31. package/dist/folder-venue-rule.d.ts.map +1 -0
  32. package/dist/folder-venue-rule.js +96 -0
  33. package/dist/folder-venue-rule.js.map +1 -0
  34. package/dist/init.d.ts +6 -0
  35. package/dist/init.d.ts.map +1 -1
  36. package/dist/init.js +3 -3
  37. package/dist/init.js.map +1 -1
  38. package/dist/lib/paper-config.d.ts.map +1 -1
  39. package/dist/lib/paper-config.js +5 -1
  40. package/dist/lib/paper-config.js.map +1 -1
  41. package/dist/new-paper.d.ts +19 -5
  42. package/dist/new-paper.d.ts.map +1 -1
  43. package/dist/new-paper.js +23 -4
  44. package/dist/new-paper.js.map +1 -1
  45. package/dist/paper-settings.d.ts +3 -0
  46. package/dist/paper-settings.d.ts.map +1 -1
  47. package/dist/paper-settings.js +20 -4
  48. package/dist/paper-settings.js.map +1 -1
  49. package/dist/ports/submission-portal.d.ts +19 -0
  50. package/dist/ports/submission-portal.d.ts.map +1 -0
  51. package/dist/ports/submission-portal.js +2 -0
  52. package/dist/ports/submission-portal.js.map +1 -0
  53. package/dist/presets.d.ts +24 -0
  54. package/dist/presets.d.ts.map +1 -1
  55. package/dist/presets.js +35 -3
  56. package/dist/presets.js.map +1 -1
  57. package/dist/rules-config.js +1 -1
  58. package/dist/submission.d.ts +61 -0
  59. package/dist/submission.d.ts.map +1 -0
  60. package/dist/submission.js +211 -0
  61. package/dist/submission.js.map +1 -0
  62. package/dist/tex-requirements.d.ts +3 -0
  63. package/dist/tex-requirements.d.ts.map +1 -1
  64. package/dist/tex-requirements.js +3 -0
  65. package/dist/tex-requirements.js.map +1 -1
  66. package/dist/tex-venue-rules.d.ts +6 -7
  67. package/dist/tex-venue-rules.d.ts.map +1 -1
  68. package/dist/tex-venue-rules.js +10 -14
  69. package/dist/tex-venue-rules.js.map +1 -1
  70. package/dist/types.d.ts +13 -0
  71. package/dist/types.d.ts.map +1 -1
  72. package/docs/configuration.md +17 -10
  73. package/docs/rules/paper/folder-venue-leftover.md +104 -0
  74. package/docs/rules/talk/captions-cover.md +1 -1
  75. package/docs/rules/talk/duration-floor.md +1 -1
  76. package/docs/rules.md +44 -42
  77. package/docs/skills.md +5 -4
  78. package/docs/submission.md +64 -0
  79. package/docs/talk.md +1 -1
  80. package/lib/paper-config.ts +6 -1
  81. package/lib/skill-checks.d.mts +7 -0
  82. package/lib/skill-trigger-cases.mjs +11 -0
  83. package/package.json +2 -2
  84. package/presets/aidc.jsonc +6 -0
  85. package/presets/venue-profile.schema.json +17 -1
  86. package/skills/paper-pipeline/paper-pipeline.harness.mjs +4 -0
  87. package/skills/submission-portal/SKILL.md +69 -0
  88. package/skills/submission-portal/SKILL.md.spec.ts +71 -0
  89. package/skills/submission-portal/submission-portal.eval.mjs +15 -0
  90. package/skills/submission-portal/submission-portal.test.ts +81 -0
  91. package/skills/submit-paper/SKILL.md +2 -1
  92. package/skills/submit-paper/SKILL.md.spec.ts +1 -0
  93. package/skills/verify-citations/SKILL.md +8 -4
  94. package/skills/verify-citations/SKILL.md.spec.ts +7 -3
  95. package/skills/verify-citations/scripts/verify-cites.mjs +23 -5
  96. package/skills/verify-citations/scripts/verify-cites.test.mjs +76 -5
  97. package/src/adapters/hotcrp/cassette/README.md +24 -0
  98. package/src/adapters/hotcrp/cassette/get-noauth.json +10 -0
  99. package/src/adapters/hotcrp/cassette/get-paper.json +39 -0
  100. package/src/adapters/hotcrp/cassette/post-dot.json +21 -0
  101. package/src/adapters/hotcrp/cassette/post-ok.json +12 -0
  102. package/src/adapters/hotcrp/index.ts +11 -0
  103. package/src/adapters/hotcrp/portal.io.ts +130 -0
  104. package/src/adapters/hotcrp/portal.test.ts +503 -0
  105. package/src/adapters/hotcrp/response.ts +181 -0
  106. package/src/cli.test.ts +109 -0
  107. package/src/cli.ts +140 -21
  108. package/src/domain/result.test.ts +9 -1
  109. package/src/domain/result.ts +5 -0
  110. package/src/domain/submission.test.ts +45 -0
  111. package/src/domain/submission.ts +141 -0
  112. package/src/domain/venue-name.test.ts +115 -0
  113. package/src/domain/venue-name.ts +129 -0
  114. package/src/folder-venue-rule.test.ts +182 -0
  115. package/src/folder-venue-rule.ts +141 -0
  116. package/src/init.test.ts +22 -0
  117. package/src/init.ts +9 -2
  118. package/src/new-paper.harness.mjs +11 -9
  119. package/src/new-paper.test.ts +129 -8
  120. package/src/new-paper.ts +38 -7
  121. package/src/paper-settings.test.ts +12 -1
  122. package/src/paper-settings.ts +25 -5
  123. package/src/parse-args.test.ts +19 -0
  124. package/src/ports/submission-portal.ts +26 -0
  125. package/src/presets.test.ts +22 -0
  126. package/src/presets.ts +46 -3
  127. package/src/rule-cases.test.ts +19 -0
  128. package/src/rules-config.ts +1 -1
  129. package/src/submission.test.ts +496 -0
  130. package/src/submission.ts +311 -0
  131. package/src/tex-requirements.ts +8 -0
  132. package/src/tex-venue-rules.test.ts +1 -1
  133. package/src/tex-venue-rules.ts +12 -25
  134. package/src/types.ts +13 -0
@@ -4,7 +4,7 @@
4
4
  * One file name at two levels, one schema (lib/paper-config.mjs → SETTINGS_KEYS):
5
5
  *
6
6
  * paperlint.json the project (optional): papersDir, rules, defaults
7
- * <paper>/paperlint.json this paper: { extends, kind, pdf, rules }
7
+ * <paper>/paperlint.json this paper: { extends, kind, pdf, rules, identity, talk, submission }
8
8
  * a venue preset `paperlint:<name>` (shipped) or `./x.jsonc` (src/presets.ts)
9
9
  *
10
10
  * The paper's `extends`, `kind` and `pdf` win over the root's; with no own value, the root's is
@@ -32,6 +32,10 @@ import {
32
32
  } from "./rules-config.ts";
33
33
  import { messageOf } from "./domain/text.ts";
34
34
  import { parsePaperTalk, type PaperTalk } from "./domain/talk.ts";
35
+ import {
36
+ parsePaperSubmission,
37
+ type PaperSubmission,
38
+ } from "./domain/submission.ts";
35
39
 
36
40
  /** What one paper declares. Every absent field is null. */
37
41
  export interface PaperSettings {
@@ -54,6 +58,8 @@ export interface PaperSettings {
54
58
  readonly identity: readonly string[] | null;
55
59
  /** How this paper is presented (`talk/*`); null when it declares no talk. Never the root's. */
56
60
  readonly talk: PaperTalk | null;
61
+ /** Which submission on the venue's portal is this paper's; null when it says none. Never the root's. */
62
+ readonly submission: PaperSubmission | null;
57
63
  }
58
64
 
59
65
  /** Why a paper's settings cannot be read. */
@@ -112,17 +118,25 @@ export function identityField(
112
118
  );
113
119
  }
114
120
 
115
- /** `rules` (checked for shape only) and `talk`, each null when absent. */
121
+ /** `rules` (checked for shape only), `talk` and `submission`, each null when absent. */
116
122
  function rulesAndTalk(
117
123
  json: Readonly<Record<string, unknown>>,
118
- ): Result<Pick<PaperSettings, "rules" | "talk">, string> {
124
+ ): Result<Pick<PaperSettings, "rules" | "talk" | "submission">, string> {
119
125
  const rules = json["rules"];
120
126
  if (rules !== undefined && !isObject(rules) && !Array.isArray(rules))
121
127
  return err(
122
128
  `"rules" must be an object of rule id → severity, or a list of ESLint blocks`,
123
129
  );
124
130
  const talk = parsePaperTalk(json["talk"]);
125
- return talk.ok ? ok({ rules: rules ?? null, talk: talk.value }) : talk;
131
+ if (!talk.ok) return talk;
132
+ const submission = parsePaperSubmission(json["submission"]);
133
+ return submission.ok
134
+ ? ok({
135
+ rules: rules ?? null,
136
+ talk: talk.value,
137
+ submission: submission.value,
138
+ })
139
+ : submission;
126
140
  }
127
141
 
128
142
  /** The parsed JSON of `paperlint.json` → the settings, or one line saying what is wrong. Pure. */
@@ -232,7 +246,12 @@ function merge(
232
246
  paper: PaperSettings | null,
233
247
  ): PaperSettings | null {
234
248
  const r = root ?? NO_DEFAULTS;
235
- const p = paper ?? { ...NO_DEFAULTS, rules: null, talk: null };
249
+ const p = paper ?? {
250
+ ...NO_DEFAULTS,
251
+ rules: null,
252
+ talk: null,
253
+ submission: null,
254
+ };
236
255
  const merged: PaperSettings = {
237
256
  extends: p.extends ?? r.extends,
238
257
  kind: p.kind ?? r.kind,
@@ -240,6 +259,7 @@ function merge(
240
259
  rules: p.rules,
241
260
  identity: joined(r.identity, p.identity),
242
261
  talk: p.talk,
262
+ submission: p.submission,
243
263
  };
244
264
  return paper === null && Object.values(merged).every((v) => v === null)
245
265
  ? null
@@ -35,6 +35,25 @@ describe("parseArgs — `--flag=value` is the same as `--flag value`", () => {
35
35
  });
36
36
  });
37
37
 
38
+ describe("parseArgs — the switches, each turning one field on", () => {
39
+ it.each([
40
+ ["--json", "json"],
41
+ ["--fix", "fix"],
42
+ ["--all", "all"],
43
+ ["--dry-run", "dryRun"],
44
+ ["--check", "check"],
45
+ ["--yes", "yes"],
46
+ ["-y", "yes"],
47
+ ["--no-hooks", "noHooks"],
48
+ ["--allow-venue-name", "allowVenueName"],
49
+ ] as const)("%s → %s", (flag, key) => {
50
+ const off = parseArgs(["new", "x"]);
51
+ const on = parseArgs(["new", "x", flag]);
52
+ expect(off[key]).toBe(false);
53
+ expect(on).toEqual({ ...off, [key]: true });
54
+ });
55
+ });
56
+
38
57
  describe("an unknown flag is refused, not read as a path", () => {
39
58
  it("exits 2 and names the flag", async () => {
40
59
  const err: string[] = [];
@@ -0,0 +1,26 @@
1
+ /**
2
+ * A conference portal holding a paper's submission — the port `paperlint submission` talks through.
3
+ * Shaped by what the command needs (read one submission, send one change), not by any portal's API;
4
+ * one adapter per portal kind (`src/adapters/hotcrp/`).
5
+ */
6
+ import type { Result } from "../domain/result.ts";
7
+ import type {
8
+ PortalFailure,
9
+ SubmissionChange,
10
+ SubmissionView,
11
+ UpdateOutcome,
12
+ } from "../domain/submission.ts";
13
+
14
+ export interface SubmissionPortal {
15
+ /** The submission as the portal holds it now. */
16
+ show(id: number): Promise<Result<SubmissionView, PortalFailure>>;
17
+ /**
18
+ * Send a change. Without `save` the portal only checks it (a dry run) and keeps nothing; an
19
+ * answer the portal gave — valid or not — is an outcome, not a failure.
20
+ */
21
+ update(
22
+ id: number,
23
+ change: SubmissionChange,
24
+ o: { readonly save: boolean },
25
+ ): Promise<Result<UpdateOutcome, PortalFailure>>;
26
+ }
@@ -16,6 +16,7 @@ import {
16
16
  MAX_PRESET_DEPTH,
17
17
  SHIPPED_PREFIX,
18
18
  shippedPresets,
19
+ shippedVenueNames,
19
20
  labelOf,
20
21
  presetProblemText,
21
22
  resolvePreset,
@@ -297,3 +298,24 @@ describe("labelOf — display only, never used to resolve", () => {
297
298
  expect(labelOf(spec)).toBe(label);
298
299
  });
299
300
  });
301
+
302
+ describe("shippedVenueNames — what each shipped venue is called", () => {
303
+ it("every shipped preset, by its label and the names along its chain", () => {
304
+ const names = shippedVenueNames(deps());
305
+ expect(names.map((n) => n.label)).toEqual(shippedPresets(VENUES));
306
+ expect(names.find((n) => n.label === "aisec")?.aliases).toEqual(
307
+ expect.arrayContaining(["AISec", "ACM CCS"]),
308
+ );
309
+ });
310
+
311
+ it("a preset that does not resolve is left out", () => {
312
+ const files = memoryFiles(
313
+ Object.fromEntries(
314
+ Object.entries(shipped).filter(([f]) => !f.endsWith("realm.jsonc")),
315
+ ),
316
+ );
317
+ expect(
318
+ shippedVenueNames({ files, venuesDir: VENUES }).map((n) => n.label),
319
+ ).toEqual(shippedPresets(VENUES).filter((n) => n !== "realm"));
320
+ });
321
+ });
package/src/presets.ts CHANGED
@@ -19,7 +19,8 @@
19
19
  * tex union — a child never removes a package its parent needs
20
20
  * format per key, the child wins; `kinds` by kind name, a child's kind replaces that kind
21
21
  * rules per rule id, the child wins
22
- * template the child wins; so does `name`, and `required_sections`, `register` and `talk` (each whole)
22
+ * template the child wins; so does `name`, and `required_sections`, `register`, `talk` and
23
+ * `portal` (each whole)
23
24
  * aliases union, with every `name` — what the venue is called along the chain
24
25
  *
25
26
  * ── THE LABEL ─────────────────────────────────────────────────────────────────────
@@ -42,7 +43,7 @@ import {
42
43
  type VenueFormat,
43
44
  } from "./tex-requirements.ts";
44
45
  import { callerPath } from "./caller-path.ts";
45
- import { err, ok, type Result } from "./domain/result.ts";
46
+ import { err, isOk, ok, type Result } from "./domain/result.ts";
46
47
  import type { Files } from "./ports/files.ts";
47
48
  import {
48
49
  readPaperSettings,
@@ -53,6 +54,7 @@ import { CONFIG_FILE } from "#lib/paper-config";
53
54
  import { messageOf } from "./domain/text.ts";
54
55
  import type { RegisterAnchor } from "./domain/register.ts";
55
56
  import type { VenueTalk } from "./domain/talk.ts";
57
+ import type { VenuePortal } from "./domain/submission.ts";
56
58
 
57
59
  /** The prefix of a shipped preset's spec. */
58
60
  export const SHIPPED_PREFIX = "paperlint:";
@@ -95,6 +97,8 @@ export interface Preset {
95
97
  readonly registerAnchors: readonly RegisterAnchor[];
96
98
  /** What the venue asks a presenter to send (`talk/*`); null when no file of the chain says. */
97
99
  readonly talk: VenueTalk | null;
100
+ /** Where the venue takes submissions (`paperlint submission`); null when no file of the chain says. */
101
+ readonly portal: VenuePortal | null;
98
102
  }
99
103
 
100
104
  /** Why a spec does not resolve. */
@@ -227,6 +231,7 @@ function merged(
227
231
  rules: {},
228
232
  registerAnchors: [],
229
233
  talk: null,
234
+ portal: null,
230
235
  };
231
236
  const m = rootFirst.reduce(
232
237
  (acc, { preset: p, file }) => ({
@@ -246,6 +251,7 @@ function merged(
246
251
  rules: { ...acc.rules, ...p.rules },
247
252
  registerAnchors: p.registerAnchors ?? acc.registerAnchors,
248
253
  talk: p.talk ?? acc.talk,
254
+ portal: p.portal ?? acc.portal,
249
255
  }),
250
256
  base,
251
257
  );
@@ -280,11 +286,48 @@ export function shippedPresets(venuesDir: string): string[] {
280
286
  return venueNames(venuesDir);
281
287
  }
282
288
 
289
+ /** The spec a paper writes in `extends` for the shipped preset `name`: `paperlint:<name>`. */
290
+ export const shippedSpec = (name: string): string => `${SHIPPED_PREFIX}${name}`;
291
+
292
+ /**
293
+ * Every shipped preset that resolves, each read the way a paper extending it would read it. The one
294
+ * piece that reads the presets directory. One that does not resolve is left out: `pdf/profile`
295
+ * owns that.
296
+ */
297
+ export function resolveShipped(deps: PresetDeps): readonly Preset[] {
298
+ const from = join(deps.venuesDir, CONFIG_FILE);
299
+ return shippedPresets(deps.venuesDir)
300
+ .map((name) => resolvePreset(shippedSpec(name), from, deps))
301
+ .filter(isOk)
302
+ .map((r) => r.value);
303
+ }
304
+
305
+ /** A shipped venue as a name knows it, and the files of its chain: what tells it from a paper's own. */
306
+ export type ShippedVenue = Pick<Preset, "label" | "aliases" | "chain">;
307
+
308
+ /**
309
+ * A resolved preset as a name knows it: its label, its aliases, its chain. Pure. (Not Remeda's
310
+ * `pick`: the app layer may not import it — `boundaries/dependencies`, `APP_EXTERNALS`.)
311
+ */
312
+ export const venueOf = ({ label, aliases, chain }: Preset): ShippedVenue => ({
313
+ label,
314
+ aliases,
315
+ chain,
316
+ });
317
+
318
+ /**
319
+ * THE ONE SOURCE OF VENUE NAMES: every shipped venue's label and `aliases` (with each `name` along
320
+ * its chain). Read by `paperlint new`, which refuses a folder name naming one, and through
321
+ * `otherVenues` by `tex/venue-leftover` and `paper/folder-venue-leftover`.
322
+ */
323
+ export const shippedVenueNames = (deps: PresetDeps): readonly ShippedVenue[] =>
324
+ resolveShipped(deps).map(venueOf);
325
+
283
326
  /** One line for a problem, naming what to change. Pure. */
284
327
  export function presetProblemText(p: PresetProblem): string {
285
328
  switch (p.kind) {
286
329
  case "unsupported":
287
- return `"extends": "${p.spec}" — a preset is \`paperlint:<name>\` (shipped) or a path starting with ./ or ../ (your own); npm presets: not yet supported. Did you mean "${SHIPPED_PREFIX}${p.spec}"?`;
330
+ return `"extends": "${p.spec}" — a preset is \`paperlint:<name>\` (shipped) or a path starting with ./ or ../ (your own); npm presets: not yet supported. Did you mean "${shippedSpec(p.spec)}"?`;
288
331
  case "not-found":
289
332
  return `"extends": "${p.spec}" names no preset (${p.file}); shipped presets (${SHIPPED_PREFIX}<name>): ${p.shipped.join(", ")}`;
290
333
  case "cycle":
@@ -389,6 +389,25 @@ const CASES: Readonly<Record<string, RuleCases>> = {
389
389
  },
390
390
  silent: aidc("Text."),
391
391
  },
392
+ "paper/folder-venue-leftover": {
393
+ reports: {
394
+ // An AIDC paper still in the folder it had when it targeted AISec.
395
+ tree: {
396
+ "papers/aisec-2026/PIPELINE-STATUS.md": STATUS,
397
+ "papers/aisec-2026/paper.tex": tex("x"),
398
+ "papers/aisec-2026/paperlint.json": AIDC[`${P}/paperlint.json`],
399
+ },
400
+ file: "papers/aisec-2026/PIPELINE-STATUS.md",
401
+ severity: 1,
402
+ line: 1,
403
+ },
404
+ // The folder names the paper's own venue.
405
+ silent: {
406
+ "papers/aidc-2026/PIPELINE-STATUS.md": STATUS,
407
+ "papers/aidc-2026/paper.tex": tex("x"),
408
+ "papers/aidc-2026/paperlint.json": AIDC[`${P}/paperlint.json`],
409
+ },
410
+ },
392
411
  "tex/venue-leftover": {
393
412
  reports: {
394
413
  tree: aidc("First written for AISec."),
@@ -6,7 +6,7 @@
6
6
  * A consumer turns an optional rule on, or changes a built-in rule's severity, by writing what they
7
7
  * would write in an ESLint flat config — a list of `{ files, ignores, rules }` blocks:
8
8
  *
9
- * "rules": [ { "files": ["papers/agenticdev-2026/**"],
9
+ * "rules": [ { "files": ["papers/rule-drift/**"],
10
10
  * "rules": { "pdf/last-page-balance": ["error", { "tolerancePt": 120 }] } } ]
11
11
  *
12
12
  * The blocks are appended AFTER paperlint's built-in config, so a later block wins, exactly as in ESLint.