paperlint 7.0.0 → 7.2.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 (152) hide show
  1. package/.github/workflows/ci.yml +3 -0
  2. package/.github/workflows/deadlines.yml +100 -0
  3. package/README.md +24 -11
  4. package/dist/adapters/hotcrp/deadlines.d.ts +8 -0
  5. package/dist/adapters/hotcrp/deadlines.d.ts.map +1 -0
  6. package/dist/adapters/hotcrp/deadlines.io.d.ts +6 -0
  7. package/dist/adapters/hotcrp/deadlines.io.d.ts.map +1 -0
  8. package/dist/adapters/hotcrp/deadlines.io.js +26 -0
  9. package/dist/adapters/hotcrp/deadlines.io.js.map +1 -0
  10. package/dist/adapters/hotcrp/deadlines.js +134 -0
  11. package/dist/adapters/hotcrp/deadlines.js.map +1 -0
  12. package/dist/adapters/hotcrp/index.d.ts +2 -0
  13. package/dist/adapters/hotcrp/index.d.ts.map +1 -1
  14. package/dist/adapters/hotcrp/index.js +2 -0
  15. package/dist/adapters/hotcrp/index.js.map +1 -1
  16. package/dist/cli.d.ts.map +1 -1
  17. package/dist/cli.js +42 -7
  18. package/dist/cli.js.map +1 -1
  19. package/dist/cycle-rules.d.ts +24 -0
  20. package/dist/cycle-rules.d.ts.map +1 -0
  21. package/dist/cycle-rules.js +212 -0
  22. package/dist/cycle-rules.js.map +1 -0
  23. package/dist/domain/body-pages.d.ts +29 -0
  24. package/dist/domain/body-pages.d.ts.map +1 -1
  25. package/dist/domain/body-pages.js +25 -0
  26. package/dist/domain/body-pages.js.map +1 -1
  27. package/dist/domain/cycle.d.ts +148 -0
  28. package/dist/domain/cycle.d.ts.map +1 -0
  29. package/dist/domain/cycle.js +281 -0
  30. package/dist/domain/cycle.js.map +1 -0
  31. package/dist/domain/deadline.d.ts +128 -0
  32. package/dist/domain/deadline.d.ts.map +1 -0
  33. package/dist/domain/deadline.js +281 -0
  34. package/dist/domain/deadline.js.map +1 -0
  35. package/dist/eslint-rules/cycle-stage.d.ts +39 -0
  36. package/dist/eslint-rules/cycle-stage.d.ts.map +1 -0
  37. package/dist/eslint-rules/cycle-stage.js +119 -0
  38. package/dist/eslint-rules/cycle-stage.js.map +1 -0
  39. package/dist/lib/paper-config.d.ts.map +1 -1
  40. package/dist/lib/paper-config.js +2 -0
  41. package/dist/lib/paper-config.js.map +1 -1
  42. package/dist/paper-includes.d.ts +3 -0
  43. package/dist/paper-includes.d.ts.map +1 -1
  44. package/dist/paper-includes.js +17 -2
  45. package/dist/paper-includes.js.map +1 -1
  46. package/dist/paper-settings.d.ts +7 -0
  47. package/dist/paper-settings.d.ts.map +1 -1
  48. package/dist/paper-settings.js +61 -8
  49. package/dist/paper-settings.js.map +1 -1
  50. package/dist/ports/venue-deadlines.d.ts +14 -0
  51. package/dist/ports/venue-deadlines.d.ts.map +1 -0
  52. package/dist/ports/venue-deadlines.js +2 -0
  53. package/dist/ports/venue-deadlines.js.map +1 -0
  54. package/dist/presets.d.ts +22 -3
  55. package/dist/presets.d.ts.map +1 -1
  56. package/dist/presets.js +26 -17
  57. package/dist/presets.js.map +1 -1
  58. package/dist/submission.d.ts +16 -0
  59. package/dist/submission.d.ts.map +1 -1
  60. package/dist/submission.js +59 -16
  61. package/dist/submission.js.map +1 -1
  62. package/dist/tex-requirements.d.ts +12 -0
  63. package/dist/tex-requirements.d.ts.map +1 -1
  64. package/dist/tex-requirements.js +26 -3
  65. package/dist/tex-requirements.js.map +1 -1
  66. package/dist/tex-venue-rules.d.ts +24 -2
  67. package/dist/tex-venue-rules.d.ts.map +1 -1
  68. package/dist/tex-venue-rules.js +98 -10
  69. package/dist/tex-venue-rules.js.map +1 -1
  70. package/dist/venue-rules.d.ts +12 -1
  71. package/dist/venue-rules.d.ts.map +1 -1
  72. package/dist/venue-rules.js +81 -17
  73. package/dist/venue-rules.js.map +1 -1
  74. package/docs/configuration.md +140 -12
  75. package/docs/e2e.md +14 -12
  76. package/docs/rules/cycle/evidence.md +87 -0
  77. package/docs/rules/cycle/port-done.md +75 -0
  78. package/docs/rules/cycle/record.md +152 -0
  79. package/docs/rules/cycle/stage.md +85 -0
  80. package/docs/rules/format/page-limit.md +18 -1
  81. package/docs/rules/tex/template.md +25 -5
  82. package/docs/rules/tex/venue-leftover.md +13 -0
  83. package/docs/rules.md +68 -55
  84. package/docs/submission.md +10 -1
  85. package/eslint-rules/cycle-stage.test.mjs +131 -0
  86. package/eslint-rules/cycle-stage.ts +141 -0
  87. package/eslint.config.mjs +41 -1
  88. package/fixtures/build-e2e/msr/PIPELINE-STATUS.md +3 -0
  89. package/fixtures/build-e2e/msr/expect.json +18 -0
  90. package/fixtures/build-e2e/msr/paper.tex +49 -0
  91. package/fixtures/build-e2e/msr/paperlint.json +1 -0
  92. package/fixtures/build-e2e/msr-appendix-after/PIPELINE-STATUS.md +3 -0
  93. package/fixtures/build-e2e/msr-appendix-after/expect.json +20 -0
  94. package/fixtures/build-e2e/msr-appendix-after/paper.tex +48 -0
  95. package/fixtures/build-e2e/msr-appendix-after/paperlint.json +1 -0
  96. package/lib/paper-config.ts +3 -0
  97. package/package.json +1 -1
  98. package/presets/aidc.jsonc +24 -3
  99. package/presets/aidc.md +6 -3
  100. package/presets/ieee-conference.md +1 -1
  101. package/presets/msr.jsonc +95 -0
  102. package/presets/msr.md +98 -0
  103. package/presets/venue-profile.schema.json +64 -0
  104. package/scripts/check.ts +4 -0
  105. package/scripts/refresh-deadlines.test.ts +431 -0
  106. package/scripts/refresh-deadlines.ts +387 -0
  107. package/skills/camera-ready/SKILL.md +8 -1
  108. package/skills/camera-ready/SKILL.md.spec.ts +7 -0
  109. package/skills/submission-portal/SKILL.md +2 -2
  110. package/skills/submission-portal/SKILL.md.spec.ts +1 -1
  111. package/skills/submit-paper/SKILL.md +9 -1
  112. package/skills/submit-paper/SKILL.md.spec.ts +8 -0
  113. package/src/adapters/hotcrp/cassette/README.md +22 -0
  114. package/src/adapters/hotcrp/cassette/deadlines-aidc26.html +17 -0
  115. package/src/adapters/hotcrp/cassette/deadlines-fse2027.html +24 -0
  116. package/src/adapters/hotcrp/cassette/deadlines-icse2027.html +25 -0
  117. package/src/adapters/hotcrp/cassette/deadlines-msr2027.html +25 -0
  118. package/src/adapters/hotcrp/cassette/deadlines-nosuch.html +18 -0
  119. package/src/adapters/hotcrp/cassette/deadlines-sosp26.html +26 -0
  120. package/src/adapters/hotcrp/deadlines.io.test.ts +86 -0
  121. package/src/adapters/hotcrp/deadlines.io.ts +27 -0
  122. package/src/adapters/hotcrp/deadlines.test.ts +170 -0
  123. package/src/adapters/hotcrp/deadlines.ts +161 -0
  124. package/src/adapters/hotcrp/index.ts +2 -0
  125. package/src/cli.test.ts +9 -0
  126. package/src/cli.ts +50 -6
  127. package/src/cycle-rules.test.ts +278 -0
  128. package/src/cycle-rules.ts +273 -0
  129. package/src/domain/body-pages.test.ts +144 -0
  130. package/src/domain/body-pages.ts +61 -1
  131. package/src/domain/cycle.test.ts +456 -0
  132. package/src/domain/cycle.ts +432 -0
  133. package/src/domain/deadline.test.ts +386 -0
  134. package/src/domain/deadline.ts +472 -0
  135. package/src/new-paper.test.ts +28 -0
  136. package/src/paper-includes.test.ts +26 -0
  137. package/src/paper-includes.ts +20 -2
  138. package/src/paper-settings-commands.test.ts +1 -1
  139. package/src/paper-settings.test.ts +167 -0
  140. package/src/paper-settings.ts +85 -11
  141. package/src/ports/venue-deadlines.ts +14 -0
  142. package/src/preset-card-rule.test.ts +1 -0
  143. package/src/presets.test.ts +171 -1
  144. package/src/presets.ts +65 -15
  145. package/src/rule-cases.test.ts +144 -0
  146. package/src/submission.test.ts +147 -4
  147. package/src/submission.ts +100 -18
  148. package/src/tex-requirements.ts +52 -4
  149. package/src/tex-venue-rules.test.ts +237 -0
  150. package/src/tex-venue-rules.ts +130 -15
  151. package/src/venue-rules.test.ts +255 -2
  152. package/src/venue-rules.ts +115 -22
@@ -37,6 +37,8 @@
37
37
  * no geometry (no banal) pdf/measured (warn); geometry and body-size silent, page-limit judges
38
38
  * only a body counted before the references; fonts still judge
39
39
  * kind does not resolve pdf/profile (error); page-limit silent; everything else still judges
40
+ * cycle phase `porting` every rule silent — the source is in the previous venue's template by
41
+ * declaration (src/domain/cycle.ts); cycle/port-done watches the declaration
40
42
  */
41
43
  import { dirname, isAbsolute, join, basename, relative } from "node:path";
42
44
  import {
@@ -58,8 +60,14 @@ import { fieldOf } from "./domain/record.ts";
58
60
  import type { Files } from "./ports/files.ts";
59
61
  import { CONFIG_FILE } from "#lib/paper-config";
60
62
  import { findLeaks, type Leak, type Searchable } from "./domain/anonymity.ts";
61
- import { bodyEnd, type BodyEnd } from "./domain/body-pages.ts";
63
+ import {
64
+ bodyEnd,
65
+ pageSplit,
66
+ type BodyEnd,
67
+ type PageSplit,
68
+ } from "./domain/body-pages.ts";
62
69
  import type { TextFacts } from "./facts-file.ts";
70
+ import { effectiveBlind, isPorting } from "./domain/cycle.ts";
63
71
 
64
72
  // ── the verdict's vocabulary ─────────────────────────────────────────────────────────
65
73
 
@@ -91,8 +99,19 @@ export interface Resolved {
91
99
  export type Assessment =
92
100
  | { readonly kind: "no-venue" }
93
101
  /** A `paperlint.json` that extends no preset yet — `pdf/measured` names the file to set. */
94
- | { readonly kind: "no-preset"; readonly file: string }
102
+ | {
103
+ readonly kind: "no-preset";
104
+ readonly file: string;
105
+ /** The paper keeps `cycles`: its venue is the current cycle's, and `extends` beside them is refused. */
106
+ readonly cycles: boolean;
107
+ }
95
108
  | { readonly kind: "unresolved"; readonly finding: Finding }
109
+ /**
110
+ * The current cycle declares its port to the venue's template as open work (`"phase": "porting"`):
111
+ * a PDF built with the previous venue's template says nothing about this venue's limits, so every
112
+ * venue rule is silent, and `cycle/port-done` speaks when the port is done and the phase still says so.
113
+ */
114
+ | { readonly kind: "porting" }
96
115
  | { readonly kind: "unbuilt"; readonly venue: Resolved }
97
116
  | { readonly kind: "stale"; readonly finding: Finding }
98
117
  | {
@@ -142,9 +161,13 @@ function kindOf(
142
161
  /** How the venue reviews, and who the paper says wrote it: what `anonymity/*` judges by. */
143
162
  const reviewOf = (
144
163
  preset: Pick<Preset, "blind">,
145
- settings: Pick<PaperSettings, "identity">,
164
+ settings: Pick<PaperSettings, "identity" | "cycles">,
146
165
  ): Pick<Resolved, "blind" | "identity"> => ({
147
- blind: preset.blind,
166
+ // An accepted attempt's camera-ready carries the authors: no longer blind.
167
+ blind: effectiveBlind(
168
+ preset.blind,
169
+ settings.cycles?.current ?? { kind: "none" },
170
+ ),
148
171
  identity: settings.identity,
149
172
  });
150
173
 
@@ -169,13 +192,25 @@ function freshFacts(
169
192
 
170
193
  const isFinding = (v: object): v is Finding => "messageId" in v;
171
194
 
195
+ /** The paper's declared venue and kind, resolved: what the judges compare the facts against. */
196
+ const resolvedOf = (preset: Preset, settings: PaperSettings): Resolved => ({
197
+ venue: preset.label,
198
+ format: preset.format,
199
+ ...kindOf(preset.label, preset.format, settings.kind),
200
+ ...reviewOf(preset, settings),
201
+ });
202
+
172
203
  /** One paper, assessed. Reads through `deps.files` only; never throws on a paper's files. */
173
204
  export function assessPaper(paperDir: string, deps: VenueRuleDeps): Assessment {
174
205
  const p = paperPreset(paperDir, deps);
175
206
  if (p.kind === "none")
176
207
  return p.settings === null
177
208
  ? { kind: "no-venue" }
178
- : { kind: "no-preset", file: join(paperDir, CONFIG_FILE) };
209
+ : {
210
+ kind: "no-preset",
211
+ file: join(paperDir, CONFIG_FILE),
212
+ cycles: p.settings.cycles !== null,
213
+ };
179
214
  if (p.kind === "settings-problem")
180
215
  return {
181
216
  kind: "unresolved",
@@ -186,13 +221,9 @@ export function assessPaper(paperDir: string, deps: VenueRuleDeps): Assessment {
186
221
  kind: "unresolved",
187
222
  finding: finding("preset", { why: presetProblemText(p.problem) }),
188
223
  };
189
- const label = p.preset.label;
190
- const venue: Resolved = {
191
- venue: label,
192
- format: p.preset.format,
193
- ...kindOf(label, p.preset.format, p.settings.kind),
194
- ...reviewOf(p.preset, p.settings),
195
- };
224
+ if (p.settings.cycles !== null && isPorting(p.settings.cycles.current))
225
+ return { kind: "porting" };
226
+ const venue = resolvedOf(p.preset, p.settings);
196
227
  const factsText = text(deps.files, factsPath(paperDir));
197
228
  if (factsText === null) return { kind: "unbuilt", venue };
198
229
  const facts = freshFacts(deps.files, paperDir, factsText);
@@ -261,9 +292,19 @@ export function judgeGeometry(
261
292
  return out;
262
293
  }
263
294
 
295
+ /**
296
+ * How many of banal's two counts — body, then references — a venue's counting replaces with its own
297
+ * from the text: none for banal's count, the body for `body_ends_at`, both with `appendix_in_body`.
298
+ */
299
+ function countedFromText(format: VenueFormat): number {
300
+ if (format.bodyEndsAt !== "references") return 0;
301
+ return format.appendixInBody === true ? 2 : 1;
302
+ }
303
+
264
304
  /**
265
305
  * Body and reference pages, as banal classifies them, against the kind's limits. A venue that counts
266
- * the body before the references is judged on its body by `judgeBodyEnd` instead.
306
+ * the body before the references is judged on its body by `judgeBodyEnd` instead, and one that
307
+ * counts the appendix as body on both.
267
308
  */
268
309
  export function judgePages(g: FlatGeometry, resolved: Resolved): Finding[] {
269
310
  const kind = resolved.kind;
@@ -272,7 +313,7 @@ export function judgePages(g: FlatGeometry, resolved: Resolved): Finding[] {
272
313
  [kind.limits.bodyPagesMax, g.body_pages, "body pages"],
273
314
  [kind.limits.refPagesMax, g.ref_pages, "reference pages"],
274
315
  ];
275
- const from = resolved.format.bodyEndsAt === "references" ? 1 : 0;
316
+ const from = countedFromText(resolved.format);
276
317
  return pages.slice(from).flatMap(([max, got, what]) =>
277
318
  max !== null && got > max
278
319
  ? [
@@ -300,13 +341,11 @@ export function judgeBodyEnd(
300
341
  resolved: Resolved,
301
342
  ): readonly Finding[] {
302
343
  const kind = resolved.kind;
303
- const max = kind?.limits.bodyPagesMax ?? null;
304
- if (
305
- resolved.format.bodyEndsAt !== "references" ||
306
- kind === null ||
307
- max === null
308
- )
309
- return [];
344
+ if (resolved.format.bodyEndsAt !== "references" || kind === null) return [];
345
+ if (resolved.format.appendixInBody === true)
346
+ return judgeSplit(text, resolved, kind);
347
+ const max = kind.limits.bodyPagesMax;
348
+ if (max === null) return [];
310
349
  const end = endOfBody(text);
311
350
  const venue = resolved.venue;
312
351
  switch (end.kind) {
@@ -324,6 +363,54 @@ export function judgeBodyEnd(
324
363
  }
325
364
  }
326
365
 
366
+ /** The findings of a split that was found: the body, and the pages holding only references. */
367
+ function splitFindings(
368
+ split: Extract<PageSplit, { readonly kind: "found" }>,
369
+ limits: KindLimits,
370
+ at: { readonly venue: string; readonly kind: string },
371
+ ): readonly Finding[] {
372
+ const refs = `the references on page ${String(split.page)}`;
373
+ const body =
374
+ split.appendixFrom === null
375
+ ? `body pages (up to ${refs}, appendices included)`
376
+ : `body pages (up to ${refs}, and the appendix from page ${String(split.appendixFrom)})`;
377
+ const counts: readonly [number | null, number, string][] = [
378
+ [limits.bodyPagesMax, split.bodyPages, body],
379
+ [limits.refPagesMax, split.refPages, "pages holding only references"],
380
+ ];
381
+ return counts.flatMap(([max, got, what]) =>
382
+ max !== null && got > max
383
+ ? [finding("pages", { what, got, max, ...at })]
384
+ : [],
385
+ );
386
+ }
387
+
388
+ /**
389
+ * The body, appendices included, and the pages holding only references (`appendix_in_body`),
390
+ * against the kind's two limits. Like `judgeBodyEnd`, an end it cannot find is said, not passed.
391
+ */
392
+ function judgeSplit(
393
+ text: Pick<TextFacts, "pages" | "bibAnchorPage" | "appendixAnchorPage">,
394
+ resolved: Resolved,
395
+ kind: NonNullable<Resolved["kind"]>,
396
+ ): readonly Finding[] {
397
+ const limits = kind.limits;
398
+ if (limits.bodyPagesMax === null && limits.refPagesMax === null) return [];
399
+ const split = pageSplit(text.pages, {
400
+ bib: text.bibAnchorPage,
401
+ appendix: text.appendixAnchorPage,
402
+ });
403
+ const venue = resolved.venue;
404
+ switch (split.kind) {
405
+ case "missing":
406
+ return [finding("noReferences", { venue })];
407
+ case "disagree":
408
+ return [finding("unclear", { venue, why: disagreement(split) })];
409
+ case "found":
410
+ return splitFindings(split, limits, { venue, kind: kind.name });
411
+ }
412
+ }
413
+
327
414
  /** Where the body ends in what the PDF says, and where hyperref anchored the two ends. */
328
415
  const endOfBody = (
329
416
  t: Pick<TextFacts, "pages" | "bibAnchorPage" | "appendixAnchorPage">,
@@ -497,7 +584,11 @@ const JUDGES: Readonly<Record<VenueRuleName, Judge>> = {
497
584
  fresh: (a) => (a.kind === "stale" ? [a.finding] : []),
498
585
  measured: (a, { shown }) =>
499
586
  a.kind === "no-preset"
500
- ? [finding("noPreset", { file: shown(a.file) })]
587
+ ? [
588
+ finding(a.cycles ? "noPresetCycle" : "noPreset", {
589
+ file: shown(a.file),
590
+ }),
591
+ ]
501
592
  : a.kind === "unbuilt"
502
593
  ? [
503
594
  finding("unbuilt", {
@@ -594,6 +685,8 @@ const META: Readonly<Record<VenueRuleName, Meta>> = {
594
685
  "not built yet, so `{{venue}}`'s page limit, fonts and format are not checked — run `paperlint build`",
595
686
  noPreset:
596
687
  'this paper names no venue preset yet, so its page limit, fonts and format are not checked — set "extends" in {{file}} (e.g. "paperlint:agenticdev"; see docs/rules.md)',
688
+ noPresetCycle:
689
+ 'this paper\'s current cycle names no venue preset, so its page limit, fonts and format are not checked — set the venue in the current cycle in {{file}} ("venue": { "kind": "preset", "extends": "paperlint:…" }), or open a new cycle when the last one has ended',
597
690
  noGeometry:
598
691
  "the build measured no page geometry (banal was not found or failed), so page size, columns, page limits and font sizes were NOT checked — run `paperlint toolchain`, then `paperlint build`",
599
692
  },