paperlint 7.1.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 (119) hide show
  1. package/.github/workflows/ci.yml +3 -0
  2. package/.github/workflows/deadlines.yml +100 -0
  3. package/README.md +6 -0
  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 +40 -5
  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/cycle.d.ts +148 -0
  24. package/dist/domain/cycle.d.ts.map +1 -0
  25. package/dist/domain/cycle.js +281 -0
  26. package/dist/domain/cycle.js.map +1 -0
  27. package/dist/domain/deadline.d.ts +128 -0
  28. package/dist/domain/deadline.d.ts.map +1 -0
  29. package/dist/domain/deadline.js +281 -0
  30. package/dist/domain/deadline.js.map +1 -0
  31. package/dist/eslint-rules/cycle-stage.d.ts +39 -0
  32. package/dist/eslint-rules/cycle-stage.d.ts.map +1 -0
  33. package/dist/eslint-rules/cycle-stage.js +119 -0
  34. package/dist/eslint-rules/cycle-stage.js.map +1 -0
  35. package/dist/lib/paper-config.d.ts.map +1 -1
  36. package/dist/lib/paper-config.js +2 -0
  37. package/dist/lib/paper-config.js.map +1 -1
  38. package/dist/paper-settings.d.ts +7 -0
  39. package/dist/paper-settings.d.ts.map +1 -1
  40. package/dist/paper-settings.js +61 -8
  41. package/dist/paper-settings.js.map +1 -1
  42. package/dist/ports/venue-deadlines.d.ts +14 -0
  43. package/dist/ports/venue-deadlines.d.ts.map +1 -0
  44. package/dist/ports/venue-deadlines.js +2 -0
  45. package/dist/ports/venue-deadlines.js.map +1 -0
  46. package/dist/presets.d.ts +12 -0
  47. package/dist/presets.d.ts.map +1 -1
  48. package/dist/presets.js +19 -11
  49. package/dist/presets.js.map +1 -1
  50. package/dist/submission.d.ts +8 -0
  51. package/dist/submission.d.ts.map +1 -1
  52. package/dist/submission.js +41 -10
  53. package/dist/submission.js.map +1 -1
  54. package/dist/tex-requirements.d.ts +3 -0
  55. package/dist/tex-requirements.d.ts.map +1 -1
  56. package/dist/tex-requirements.js +19 -0
  57. package/dist/tex-requirements.js.map +1 -1
  58. package/dist/tex-venue-rules.d.ts +19 -0
  59. package/dist/tex-venue-rules.d.ts.map +1 -1
  60. package/dist/tex-venue-rules.js +69 -0
  61. package/dist/tex-venue-rules.js.map +1 -1
  62. package/dist/venue-rules.d.ts +10 -0
  63. package/dist/venue-rules.d.ts.map +1 -1
  64. package/dist/venue-rules.js +26 -10
  65. package/dist/venue-rules.js.map +1 -1
  66. package/docs/configuration.md +139 -11
  67. package/docs/rules/cycle/evidence.md +87 -0
  68. package/docs/rules/cycle/port-done.md +75 -0
  69. package/docs/rules/cycle/record.md +152 -0
  70. package/docs/rules/cycle/stage.md +85 -0
  71. package/docs/rules.md +48 -43
  72. package/docs/submission.md +5 -0
  73. package/eslint-rules/cycle-stage.test.mjs +131 -0
  74. package/eslint-rules/cycle-stage.ts +141 -0
  75. package/eslint.config.mjs +41 -1
  76. package/lib/paper-config.ts +3 -0
  77. package/package.json +1 -1
  78. package/presets/aidc.jsonc +24 -3
  79. package/presets/aidc.md +6 -3
  80. package/presets/msr.jsonc +25 -1
  81. package/presets/msr.md +5 -1
  82. package/presets/venue-profile.schema.json +39 -0
  83. package/scripts/check.ts +4 -0
  84. package/scripts/refresh-deadlines.test.ts +431 -0
  85. package/scripts/refresh-deadlines.ts +387 -0
  86. package/src/adapters/hotcrp/cassette/README.md +22 -0
  87. package/src/adapters/hotcrp/cassette/deadlines-aidc26.html +17 -0
  88. package/src/adapters/hotcrp/cassette/deadlines-fse2027.html +24 -0
  89. package/src/adapters/hotcrp/cassette/deadlines-icse2027.html +25 -0
  90. package/src/adapters/hotcrp/cassette/deadlines-msr2027.html +25 -0
  91. package/src/adapters/hotcrp/cassette/deadlines-nosuch.html +18 -0
  92. package/src/adapters/hotcrp/cassette/deadlines-sosp26.html +26 -0
  93. package/src/adapters/hotcrp/deadlines.io.test.ts +86 -0
  94. package/src/adapters/hotcrp/deadlines.io.ts +27 -0
  95. package/src/adapters/hotcrp/deadlines.test.ts +170 -0
  96. package/src/adapters/hotcrp/deadlines.ts +161 -0
  97. package/src/adapters/hotcrp/index.ts +2 -0
  98. package/src/cli.test.ts +9 -0
  99. package/src/cli.ts +43 -4
  100. package/src/cycle-rules.test.ts +278 -0
  101. package/src/cycle-rules.ts +273 -0
  102. package/src/domain/cycle.test.ts +456 -0
  103. package/src/domain/cycle.ts +432 -0
  104. package/src/domain/deadline.test.ts +386 -0
  105. package/src/domain/deadline.ts +472 -0
  106. package/src/new-paper.test.ts +1 -0
  107. package/src/paper-settings.test.ts +167 -0
  108. package/src/paper-settings.ts +85 -11
  109. package/src/ports/venue-deadlines.ts +14 -0
  110. package/src/presets.test.ts +105 -0
  111. package/src/presets.ts +43 -10
  112. package/src/rule-cases.test.ts +144 -0
  113. package/src/submission.test.ts +105 -1
  114. package/src/submission.ts +73 -12
  115. package/src/tex-requirements.ts +31 -0
  116. package/src/tex-venue-rules.test.ts +76 -0
  117. package/src/tex-venue-rules.ts +85 -0
  118. package/src/venue-rules.test.ts +82 -0
  119. package/src/venue-rules.ts +44 -12
package/eslint.config.mjs CHANGED
@@ -413,6 +413,40 @@ export const IO_GLOBALS = {
413
413
  },
414
414
  };
415
415
 
416
+ /**
417
+ * `paperlint lint` NEVER READS THE CLOCK. Its findings are a function of the files and the installed
418
+ * package: the same checkout lints the same tomorrow, and a deadline's distance belongs to a status
419
+ * view or a calendar, not to a gate (a lint that turns red on an untouched tree gets muted). The
420
+ * clock is an effect like the disk, so it is read where effects are — an adapter's `*.io.ts` and the
421
+ * root, `src/cli.ts` — and passed in. Matched on the AST: `Date.now` (called or passed along),
422
+ * `new Date()` and `Date()` without an argument. `new Date(ms)` converts a given time and passes.
423
+ * `toolchain.ts` and `build-engine.ts` keep `Date.now` as a default for their own timing (build and
424
+ * install only, neither runs under `lint`); that legacy I/O moves behind a port in #76.
425
+ */
426
+ const READS_THE_CLOCK = [
427
+ "MemberExpression[object.name='Date'][property.name='now']",
428
+ "NewExpression[callee.name='Date'][arguments.length=0]",
429
+ "CallExpression[callee.name='Date']",
430
+ ].map((selector) => ({
431
+ selector,
432
+ message:
433
+ "Lint never reads the clock: its findings are a function of the files. Take the time as an argument; read it in an adapter's *.io.ts or src/cli.ts.",
434
+ }));
435
+
436
+ /** Every source file the checks are made of, but the places effects are allowed. Same scope as axis B. */
437
+ export const NO_CLOCK = {
438
+ files: ["src/**/*.ts"],
439
+ ignores: [
440
+ ...IO_GLOBALS.ignores,
441
+ // Legacy I/O, moves behind a port in #76 (see READS_THE_CLOCK).
442
+ "src/toolchain.ts",
443
+ "src/build-engine.ts",
444
+ ],
445
+ rules: {
446
+ "no-restricted-syntax": ["error", AS_UNKNOWN_AS, ...READS_THE_CLOCK],
447
+ },
448
+ };
449
+
416
450
  /**
417
451
  * EVERY PRESET HAS A CARD, AND EVERY CARD'S RULES TABLE IS THE PRESET'S. Two checks, one block each:
418
452
  *
@@ -608,6 +642,7 @@ export default [
608
642
  },
609
643
  // Hexagonal layers, both axes (src/CLAUDE.md): knowledge by element, purity by file category.
610
644
  IO_GLOBALS,
645
+ NO_CLOCK,
611
646
  layerBoundaries(dirname(fileURLToPath(import.meta.url))),
612
647
  // The ratchet: per-file ceilings for the files written before the limits existed. See RATCHET.
613
648
  ...Object.entries(RATCHET).map(([file, max]) => ({
@@ -805,7 +840,12 @@ export default [
805
840
  files: ["eslint-rules/*.ts", "eslint-rules/*.mjs"],
806
841
  ignores: ["eslint-rules/*.test.*", "eslint-rules/*.harness.*"],
807
842
  rules: {
808
- "no-restricted-syntax": ["error", AS_UNKNOWN_AS, ...GIT_IN_A_RULE],
843
+ "no-restricted-syntax": [
844
+ "error",
845
+ AS_UNKNOWN_AS,
846
+ ...GIT_IN_A_RULE,
847
+ ...READS_THE_CLOCK,
848
+ ],
809
849
  },
810
850
  },
811
851
  // LAST, so no block above can replace its `no-restricted-syntax` options for these files (none
@@ -143,6 +143,8 @@ export const SETTINGS_KEYS: Readonly<Record<string, string>> = Object.freeze({
143
143
  talk: 'how this paper is presented: { "mode": "remote-video", "dir"?, "files"?, "one_slide"? } — the talk/ rules. A paper\'s own; refused in the root file',
144
144
  submission:
145
145
  "which submission on the venue's portal is this paper's: { \"id\": <number> } — paperlint submission, with the preset's \"portal\". A paper's own; refused in the root file",
146
+ cycles:
147
+ "the paper's attempts at venues, oldest first: [{ id, venue, kind, opened, phase?, deadlines, submission?, outcome }] — extends, kind and submission are then the current attempt's, and the cycle/ rules judge the record. A paper's own; refused in the root file",
146
148
  scripts: "how skill prose names the scripts — consumer.mjs",
147
149
  timezone: "the zone deadline anchors are written in — consumer.mjs",
148
150
  contactEmail: "the polite-pool address for citation lookups — consumer.mjs",
@@ -155,6 +157,7 @@ export const SETTINGS_KEYS: Readonly<Record<string, string>> = Object.freeze({
155
157
  export const PAPER_ONLY_KEYS: readonly string[] = Object.freeze([
156
158
  "talk",
157
159
  "submission",
160
+ "cycles",
158
161
  ]);
159
162
 
160
163
  /** The keys that only mean something for the whole project: refused in a paper's `paperlint.json`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperlint",
3
- "version": "7.1.0",
3
+ "version": "7.2.0",
4
4
  "description": "A linter for scientific papers written in LaTeX: catches mistakes before you submit to a conference or journal.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -13,10 +13,12 @@
13
13
  // and appendices.»
14
14
  // «Submissions must be properly anonymized for double-blind review.»
15
15
  //
16
- // THE DEADLINE PEOPLE GET WRONG (not a lint fact): the HotCRP portal closes 2026-10-02 11:59:59 UTC
16
+ // THE DEADLINE PEOPLE GET WRONG: the HotCRP portal closed 2026-10-02 11:59:59 UTC first
17
17
  // (`"sub":1790942399` on https://aidc.submit.acsac.org/deadlines, recorded in paperlint#90), while
18
- // the workshop page says «October 2, 2026 (Anywhere-on-Earth, AoE)» — a day later. The portal is
19
- // what refuses the upload.
18
+ // the workshop page said «October 2, 2026 (Anywhere-on-Earth, AoE)» — a day later; by 2026-10-05
19
+ // the portal had moved it a day (`"sub":1791028799`). The portal is what refuses the upload, so its
20
+ // readings are `deadlines` below, rewritten by the scheduled refresh in paperlint's repository
21
+ // (.github/workflows/deadlines.yml) when the portal changes them.
20
22
  //
21
23
  // THE REVIEW IS DOUBLE-BLIND: `"blind":true` in the same portal data, and the call above.
22
24
  //
@@ -38,6 +40,25 @@
38
40
  // the HotCRP API, there.
39
41
  "portal": { "kind": "hotcrp", "url": "https://aidc.submit.acsac.org" },
40
42
 
43
+ // Read from the portal's public deadlines page; the camera-ready only in its status object
44
+ // (`"final":{"done":1793447999}`).
45
+ "deadlines": [
46
+ {
47
+ "what": "submission",
48
+ "at": "2026-10-03T11:59:59Z",
49
+ "source": "portal",
50
+ "url": "https://aidc.submit.acsac.org/deadlines",
51
+ "read": "2026-10-05",
52
+ },
53
+ {
54
+ "what": "camera-ready",
55
+ "at": "2026-10-31T11:59:59Z",
56
+ "source": "portal",
57
+ "url": "https://aidc.submit.acsac.org/deadlines",
58
+ "read": "2026-10-05",
59
+ },
60
+ ],
61
+
41
62
  // What the venue is called in a paper's text (tex/venue-leftover): the workshop, and the
42
63
  // conference it is held at.
43
64
  "aliases": ["AIDC", "ACSAC"],
package/presets/aidc.md CHANGED
@@ -21,9 +21,12 @@ from it. The `submit-paper` skill reads this card for the venue's specifics.
21
21
  - **Double-blind**: «Submissions must be properly anonymized for double-blind review.», and
22
22
  `"blind":true` in the portal's data.
23
23
  - **Portal**: HotCRP at `https://aidc.submit.acsac.org`.
24
- - 🔴 **The deadline people get wrong**: the HotCRP portal closes 2026-10-02 11:59:59 UTC
25
- (`"sub":1790942399` on `https://aidc.submit.acsac.org/deadlines`), while the workshop page says
26
- «October 2, 2026 (Anywhere-on-Earth, AoE)» — a day later. The portal is what refuses the upload.
24
+ - 🔴 **The deadline people get wrong**: the HotCRP portal closed 2026-10-02 11:59:59 UTC first
25
+ (`"sub":1790942399` on `https://aidc.submit.acsac.org/deadlines`), while the workshop page said
26
+ «October 2, 2026 (Anywhere-on-Earth, AoE)» — a day later; on 2026-10-05 the portal read
27
+ 2026-10-03 11:59:59 UTC (`"sub":1791028799`). The portal is what refuses the upload. The preset
28
+ carries its readings as `deadlines`, refreshed by paperlint's scheduled workflow when the portal
29
+ changes them.
27
30
 
28
31
  ## Paper kinds and limits
29
32
 
package/presets/msr.jsonc CHANGED
@@ -15,7 +15,10 @@
15
15
  // camera-ready version.»
16
16
  // «The MSR 2027 Technical Track will employ a double-anonymous review process.»
17
17
  //
18
- // The dates are not lint facts; they are in the card, msr.md beside this file.
18
+ // THE DEADLINES ARE THE PORTAL'S, as read: `deadlines` below is rewritten by the scheduled refresh
19
+ // in paperlint's repository (.github/workflows/deadlines.yml) when the portal changes them, and a
20
+ // paper reaches the new dates through its normal update of paperlint. The call's own wording
21
+ // (abstract and paper days, AoE) is in the card, msr.md beside this file.
19
22
  {
20
23
  "type": "venue",
21
24
  "name": "MSR",
@@ -34,6 +37,27 @@
34
37
  // token — the HotCRP API, there.
35
38
  "portal": { "kind": "hotcrp", "url": "https://msr2027.hotcrp.com" },
36
39
 
40
+ // Read from the portal's public deadlines page (no account) by the adapter in src/adapters/hotcrp/.
41
+ // «Submissions must be completed by this deadline» is the `submission`: on the CALL's abstract
42
+ // day, 04:00 UTC — four days before the call's paper day. «Completed submissions may be updated
43
+ // until this deadline» is the `resubmission`.
44
+ "deadlines": [
45
+ {
46
+ "what": "submission",
47
+ "at": "2026-10-20T04:00:00Z",
48
+ "source": "portal",
49
+ "url": "https://msr2027.hotcrp.com/deadlines",
50
+ "read": "2026-10-05",
51
+ },
52
+ {
53
+ "what": "resubmission",
54
+ "at": "2026-10-23T04:00:00Z",
55
+ "source": "portal",
56
+ "url": "https://msr2027.hotcrp.com/deadlines",
57
+ "read": "2026-10-05",
58
+ },
59
+ ],
60
+
37
61
  // What the venue is called in a paper's text (tex/venue-leftover) — and only beside a year
38
62
  // («MSR 2027», «MSR'27»): «MSR» is also Microsoft Research, and an acronym papers define for
39
63
  // themselves («Manual Speech Recognition (MSR)», in an accepted ACSAC paper of
package/presets/msr.md CHANGED
@@ -28,7 +28,11 @@ whereas last year it was ACM format.»), and limits and dates change between edi
28
28
  (`"sub":1792468800`, 2026-10-20 04:00 UTC) and «Resubmission deadline: Friday Oct 23, 2026, 12 AM
29
29
  EDT» («Completed submissions may be updated until this deadline»). Midnight EDT at the start of
30
30
  each day is about a day and a half before the end of that day AoE. The portal is what refuses the
31
- upload: plan for its times, and re-read the page, since chairs change them.
31
+ upload: plan for its times. The preset carries them as `deadlines` (`source: "portal"`, with the
32
+ day read), refreshed by paperlint's scheduled workflow when the portal changes them; a paper whose
33
+ current cycle names `paperlint:msr` derives them. Recording the call's days in the paper's own
34
+ cycle maps «abstract» to `registration` and «paper» to `submission` — and the portal's earlier
35
+ `submission` is then the one in force.
32
36
  - **Format**, verbatim from the call: «LaTeX users must use \documentclass[10pt,conference]{IEEEtran}
33
37
  without including the compsoc or compsocconf options», «title in 24pt font and full text in 10pt
34
38
  type», and «Alterations of spacing, font size, and other changes that deviate from the
@@ -78,6 +78,45 @@
78
78
  }
79
79
  }
80
80
  },
81
+ "deadlines": {
82
+ "description": "The edition's deadlines as they were READ — the portal's readings, rewritten by the scheduled refresh in paperlint's repository when the portal changes them (a patch release), each with the page and the day it was read. A paper whose current cycle names this preset derives them; its own record adds the call's readings and overrides on top, and the earlier reading binds. `what`: registration (abstract registration — a call's \"abstract\" date), submission (the full paper), resubmission (updates to a completed submission), notification, camera-ready. At most one reading per kind and source. A child's list replaces its parent's.",
83
+ "type": "array",
84
+ "items": {
85
+ "type": "object",
86
+ "additionalProperties": false,
87
+ "required": ["what", "at", "source", "url", "read"],
88
+ "properties": {
89
+ "what": {
90
+ "type": "string",
91
+ "enum": [
92
+ "registration",
93
+ "submission",
94
+ "resubmission",
95
+ "notification",
96
+ "camera-ready"
97
+ ]
98
+ },
99
+ "at": {
100
+ "description": "An instant in UTC, as the portal publishes it: 2026-10-20T04:00:00Z.",
101
+ "type": "string",
102
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$"
103
+ },
104
+ "source": {
105
+ "type": "string",
106
+ "enum": ["portal", "call"]
107
+ },
108
+ "url": {
109
+ "type": "string",
110
+ "pattern": "^https?://[^\\s/]+[^\\s]*$"
111
+ },
112
+ "read": {
113
+ "description": "The day the page was read, YYYY-MM-DD.",
114
+ "type": "string",
115
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
116
+ }
117
+ }
118
+ }
119
+ },
81
120
  "required_sections": {
82
121
  "description": "Sections the venue requires, read by `tex/required-section`. Each is a `\\section` or `\\section*` whose title equals `title` once whitespace is collapsed. `position: \"last\"`: after every section of the body (the ones before the appendix and the bibliography); it may stand after the bibliography. A child preset's list replaces its parent's.",
83
122
  "type": "array",
package/scripts/check.ts CHANGED
@@ -202,6 +202,10 @@ export const NOT_COVERED: Readonly<Record<string, string>> = {
202
202
  validate:
203
203
  "pr-title.yml checks the pull request's TITLE (the squash commit semantic-release reads), " +
204
204
  "which exists only on GitHub, not in the tree.",
205
+ refresh:
206
+ "deadlines.yml reads the venue presets' portals over the network once a day and opens a pull " +
207
+ "request when a deadline moved; npm test replays the recorded portal pages through " +
208
+ "scripts/refresh-deadlines.test.ts, and nothing local should read the live portals on every check.",
205
209
  };
206
210
 
207
211
  /**