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
@@ -1,3 +1,5 @@
1
+ | `deadlines` | one entry per deadline, `{ "what", "observed", "override" }` — see [Deadlines](#deadlines) below |
2
+
1
3
  # Configuration reference
2
4
 
3
5
  paperlint reads one file name at two levels, `paperlint.json`, with one schema. Both files are
@@ -96,16 +98,17 @@ The same keys as the root file, minus the project-only ones (`papersDir`, `struc
96
98
  skills' keys), which are refused here by name, plus `talk` and `submission`, which only a paper has. **It merges over the root file:** its `extends`,
97
99
  `kind` and `pdf` win; one it does not set comes from the root.
98
100
 
99
- | key | what it is |
100
- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
101
- | `extends` | the venue preset the built PDF is judged against: `paperlint:<name>` (shipped) or `./path` / `../path` (your own, relative to this file) — [`rules.md`](rules.md#checks-against-the-venue) |
102
- | `kind` | the kind of paper (`short`, `research`, …) whose page limit applies |
103
- | `pdf` | where the built PDF is, relative to the paper, when it is not `paper.pdf` |
104
- | `identity` | what identifies the authors — names, handles, emails, affiliations, your own project names. A blind venue's PDF must say none of it ([`anonymity/identity`](rules/anonymity/identity.md)). Joined with the root's list |
105
- | `talk` | how this paper is presented, `{ "mode": "remote-video" }` plus optional `dir`, `files`, `one_slide` — the `talk/*` rules judge its finished files ([`talk.md`](talk.md)). Only here: the root file refuses it |
106
- | `submission` | which submission on the venue's portal is this paper's, `{ "id": 7 }` — read by [`paperlint submission`](submission.md), with the preset's `portal`. Only here: the root file refuses it |
107
- | `rules` | rule overrides for this paper alone — `{ "<rule>": "<severity>" }`, or blocks with globs relative to the paper |
108
- | `$comment` | a note for humans; ignored |
101
+ | key | what it is |
102
+ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
103
+ | `extends` | the venue preset the built PDF is judged against: `paperlint:<name>` (shipped) or `./path` / `../path` (your own, relative to this file) — [`rules.md`](rules.md#checks-against-the-venue) |
104
+ | `kind` | the kind of paper (`short`, `research`, …) whose page limit applies |
105
+ | `pdf` | where the built PDF is, relative to the paper, when it is not `paper.pdf` |
106
+ | `identity` | what identifies the authors — names, handles, emails, affiliations, your own project names. A blind venue's PDF must say none of it ([`anonymity/identity`](rules/anonymity/identity.md)). Joined with the root's list |
107
+ | `talk` | how this paper is presented, `{ "mode": "remote-video" }` plus optional `dir`, `files`, `one_slide` — the `talk/*` rules judge its finished files ([`talk.md`](talk.md)). Only here: the root file refuses it |
108
+ | `submission` | which submission on the venue's portal is this paper's, `{ "id": 7 }` — read by [`paperlint submission`](submission.md), with the preset's `portal`. Only here: the root file refuses it |
109
+ | `cycles` | the paper's attempts at venues, one entry per attempt, oldest first — see [The paper's cycles](#the-papers-cycles) below. With it, `extends`, `kind` and `submission` are the current attempt's and may not be written beside it. Only here: the root file refuses it |
110
+ | `rules` | rule overrides for this paper alone — `{ "<rule>": "<severity>" }`, or blocks with globs relative to the paper |
111
+ | `$comment` | a note for humans; ignored |
109
112
 
110
113
  `paperlint new` writes this file from the template (`templates/paper/paperlint.json`, or your
111
114
  `<papers>/.template/paperlint.json` if you keep one). With `--venue` it writes the venue into it:
@@ -132,7 +135,8 @@ npx paperlint new my-paper --venue ./venues/my-workshop.jsonc # "extends": "
132
135
  Without a venue the file has `"extends": null` and a `$comment` saying what goes there. Until
133
136
  `extends` names a preset (here or in the root file), `paperlint lint` gives that paper one warning,
134
137
  `pdf/measured`: "this paper names no venue preset yet … set "extends" in
135
- papers/my-paper/paperlint.json".
138
+ papers/my-paper/paperlint.json". A paper keeping `cycles` is told instead to set the venue in its
139
+ current cycle — `extends` beside `cycles` is refused.
136
140
 
137
141
  **Any other key is an error**, in either file, named in the message: `paperlint.json: unknown key
138
142
  "papersdir"`. A misspelt key would otherwise read as "not set", and the setting you meant would
@@ -144,6 +148,130 @@ the root `paperlint.json`'s `rules` → the paper's own `rules`. So a venue can
144
148
  project can change that for every paper, and one paper can still change it for itself. Only rules
145
149
  paperlint ships may be named, at every level.
146
150
 
151
+ ## The paper's cycles
152
+
153
+ A paper goes to a venue, is rejected, and goes to the next one. `cycles` records each attempt as
154
+ one entry, oldest first; `extends`, `kind` and `submission` are then **derived** from the current
155
+ attempt — the one open cycle, or the accepted last one — and writing any of them beside `cycles`
156
+ is refused as a second source for one fact.
157
+
158
+ ```jsonc
159
+ {
160
+ "pdf": "build/paper.pdf",
161
+ "cycles": [
162
+ {
163
+ "id": "alpha-2026", // what a stage's `cycle:` names
164
+ "venue": { "kind": "preset", "extends": "paperlint:acm-sigconf" },
165
+ "kind": "short",
166
+ "opened": "2026-07-01", // the day this attempt was decided
167
+ "deadlines": [
168
+ {
169
+ "what": "submission",
170
+ "observed": [
171
+ {
172
+ "at": "2026-08-06 AoE",
173
+ "source": "call",
174
+ "url": "https://alpha.example/cfp",
175
+ "read": "2026-07-01",
176
+ },
177
+ ],
178
+ },
179
+ ],
180
+ "outcome": {
181
+ "kind": "rejected",
182
+ "date": "2026-09-08",
183
+ "desk": false,
184
+ "evidence": "reviews/alpha-decision.md",
185
+ },
186
+ },
187
+ {
188
+ "id": "beta-2027",
189
+ "venue": { "kind": "preset", "extends": "paperlint:ieee-conference" },
190
+ "opened": "2026-09-09",
191
+ "phase": "porting", // the source is still in alpha's template
192
+ "deadlines": [
193
+ {
194
+ "what": "submission",
195
+ "observed": [
196
+ {
197
+ "at": "2026-10-20T04:00:00Z",
198
+ "source": "portal",
199
+ "url": "https://beta.example/deadlines",
200
+ "read": "2026-09-09",
201
+ },
202
+ {
203
+ "at": "2026-10-23 AoE",
204
+ "source": "call",
205
+ "url": "https://beta.example/cfp",
206
+ "read": "2026-09-09",
207
+ },
208
+ ],
209
+ "override": {
210
+ "at": "2026-10-27T04:00:00Z",
211
+ "reason": "the chairs extended the deadline by mail",
212
+ "evidence": "mail/extension.eml",
213
+ },
214
+ },
215
+ ],
216
+ "submission": { "id": 7 },
217
+ },
218
+ ],
219
+ }
220
+ ```
221
+
222
+ | field | what it is |
223
+ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
224
+ | `id` | unique in the list; a scorecard stage names it as `cycle: <id>` ([`cycle/stage`](rules/cycle/stage.md)) |
225
+ | `venue` | `{ "kind": "preset", "extends": "paperlint:<name>" \| "./path" }` — resolved as `extends` is; or `{ "kind": "named", "name", "url" }` for a venue with no preset yet. A named venue has no format rules |
226
+ | `kind` | a key of the preset's `format.kinds`; omit when the venue has one kind |
227
+ | `opened` | `YYYY-MM-DD`, the day this attempt was decided — the boundary a consumer judges the scorecard's rows against |
228
+ | `phase` | `"porting"` while the source is still in the previous venue's template: every `pdf/*`, `format/*`, `anonymity/*` and `tex/*` venue rule is silent for the paper, and [`cycle/port-done`](rules/cycle/port-done.md) fires once the class line is the venue's. Default `"prepared"` |
229
+ | `deadlines` | a list of `{ "what", "at", "source", "url" }`: `what` is `registration`, `submission`, `resubmission`, `notification` or `camera-ready`; `at` an instant with its zone (`2026-10-20T04:00:00Z`) or a call's `"YYYY-MM-DD AoE"`; `source` is `"portal"` or `"call"` — where it was read ([`cycle/record`](rules/cycle/record.md)) |
230
+ | `submission` | which submission on the venue's portal is this paper's, `{ "id": 7 }` — what the flat `submission` key is |
231
+ | `outcome` | `{ "kind": "open" }` (the default), or `accepted` \| `rejected` \| `withdrawn` with `date` and `evidence`, a file in the paper folder ([`cycle/evidence`](rules/cycle/evidence.md)); `rejected` may add `"desk": true` |
232
+
233
+ **One open cycle at most, and it is the last entry.** Two open cycles is dual submission or a
234
+ decision nobody recorded; `paperlint lint` refuses the file with one line naming both ids. A paper
235
+ whose last cycle is rejected or withdrawn and has no open one is between venues: `extends` derives
236
+ to null and the venue rules say nothing. An accepted last cycle keeps the paper at that venue —
237
+ camera-ready and talk are judged against it, and the venue's blindness no longer applies.
238
+
239
+ Papers without `cycles` read exactly as before.
240
+
241
+ ### Deadlines
242
+
243
+ A deadline records **what each page said**, and optionally **a human decision that overrides it**:
244
+
245
+ | field | what it is |
246
+ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
247
+ | `what` | `registration` — the abstract registration, a call's "abstract" date · `submission` — the full paper, a call's "paper" date, HotCRP's "submissions must be completed by" · `resubmission` — updates to a completed submission · `notification` · `camera-ready` |
248
+ | `observed` | the readings: `{ "at", "source", "url", "read" }` — `at` an instant with its zone (`2026-10-20T04:00:00Z`) or a call's `"YYYY-MM-DD AoE"` (paperlint converts it to the end of that day at UTC−12); `source` `"portal"` or `"call"`; `url` the page; `read` the day it was read. One reading per source |
249
+ | `override` | `{ "at", "reason", "evidence" }` — an instant that binds instead, why, and a file in the paper folder it rests on (the chairs' mail). [`cycle/evidence`](rules/cycle/evidence.md) requires the file |
250
+
251
+ **Which instant is in force:** the `override` when there is one; otherwise the **earliest** reading.
252
+ The portal and the call often disagree — at MSR 2027 the portal's "submissions must be completed by"
253
+ is 04:00 UTC on the call's _abstract_ day, four days before the call's _paper_ day — and both are kept:
254
+ the earlier one is the safe one, and recording both is not an error. The kinds map a call's words
255
+ honestly: the call's "abstract" is `registration`, its "paper" is `submission`; the portal's earlier
256
+ `submission` then binds. A deadline moved by the chairs' mail is an `override` with the mail as its
257
+ evidence, never a reading with a made-up source. Any key the record does not know — in a deadline, a
258
+ reading, an override, an outcome, a venue — is an error, so an override typed under another name
259
+ cannot look recorded and not be in force.
260
+
261
+ **Where the readings come from.** A shipped venue preset whose venue takes submissions on HotCRP
262
+ carries the portal's readings itself, as `deadlines` (`presets/msr.jsonc`). A cycle whose venue is that
263
+ preset **derives** them: write only what the call says and any override; a portal reading the preset
264
+ already carries is reported by [`cycle/record`](rules/cycle/record.md) as a second copy. A scheduled
265
+ workflow in paperlint's repository re-reads every such portal once a day and, when a value changed,
266
+ opens a pull request that releases a patch — **so new dates reach your papers through your normal
267
+ dependency update** (Dependabot or Renovate bumping `paperlint`); there is nothing to run. A venue
268
+ without a preset (`"venue": { "kind": "named", … }`) has no one reading it for you: type its
269
+ deadlines from its call, `"source": "call"`, and re-read the call yourself.
270
+
271
+ **`paperlint lint` never reads the clock.** It checks the record — instants with zones, each source's
272
+ deadlines in order, the evidence on disk — and gives the same answer tomorrow for the same files.
273
+ How many days are left is a status view's or a calendar's question, not a gate's.
274
+
147
275
  ## Records in frontmatter, checked by shipped JSON Schemas
148
276
 
149
277
  Two kinds of file under a paper keep a record in their YAML frontmatter, and paperlint validates
@@ -0,0 +1,87 @@
1
+ # cycle/evidence
2
+
3
+ **Level:** error · **Reads:** the paper's `paperlint.json` (`cycles`) and the paper folder ·
4
+ **Reported on:** the first line of the paper's `PIPELINE-STATUS.md`
5
+
6
+ ## What it catches
7
+
8
+ A closed cycle — `outcome.kind` is `accepted`, `rejected` or `withdrawn` — names an `evidence`
9
+ file that is not in the paper folder: the decision mail, the reviews, or the note that records the
10
+ withdrawal was never saved, or the path is wrong.
11
+
12
+ The same for a deadline's `override`: the instant a human decided binds instead of what the pages
13
+ said rests on its `evidence` — the chairs' mail that extended the deadline, a saved page — and that
14
+ file must be in the paper folder.
15
+
16
+ ## Why
17
+
18
+ The outcome of an attempt is a claim about what a venue decided, and the record is read by people
19
+ and programs that were not there when the mail arrived: a status command, a scorecard check, the
20
+ next attempt's prior-work delta. A decision whose evidence is not in the folder is a claim with
21
+ nothing behind it, and the day it is doubted there is nowhere to look. The record therefore refuses
22
+ an outcome without `evidence` at all, and this rule requires the file to exist.
23
+
24
+ ## Examples
25
+
26
+ Failing — `paperlint.json`:
27
+
28
+ ```json
29
+ {
30
+ "cycles": [
31
+ {
32
+ "id": "first-2026",
33
+ "venue": { "kind": "preset", "extends": "paperlint:acm-sigconf" },
34
+ "opened": "2026-07-01",
35
+ "outcome": {
36
+ "kind": "rejected",
37
+ "date": "2026-09-08",
38
+ "evidence": "reviews/decision.md"
39
+ }
40
+ }
41
+ ]
42
+ }
43
+ ```
44
+
45
+ with no `reviews/decision.md` in the paper folder:
46
+
47
+ ```text
48
+ papers/my-paper/PIPELINE-STATUS.md
49
+ 1:1 error cycle «first-2026» is rejected (2026-09-08) with evidence `reviews/decision.md`, which is
50
+ not in the paper folder — save the decision mail, the reviews or the note that records
51
+ the decision there, or correct the path cycle/evidence
52
+ ```
53
+
54
+ An override whose evidence was never saved:
55
+
56
+ ```text
57
+ 1:1 error cycle «msr-2027»: the override of the "submission" deadline (2026-10-27T04:00:00Z)
58
+ rests on `mail/extension.eml`, which is not in the paper folder — save the mail or the
59
+ page that grants it there, or correct the path cycle/evidence
60
+ ```
61
+
62
+ Passing:
63
+
64
+ - the same record with `reviews/decision.md` present;
65
+ - an open cycle (`"outcome": { "kind": "open" }`, or no `outcome`): nothing has been decided;
66
+ - a paper whose `paperlint.json` has no `cycles`.
67
+
68
+ ## Options / preset fields
69
+
70
+ No rule options. `evidence` is a path relative to the paper folder; it may name any file — the
71
+ saved mail, a markdown note with the reviews, a PDF.
72
+
73
+ ## What it does not check
74
+
75
+ - What the file says. A rejection recorded as `accepted` with the rejection mail as evidence passes
76
+ here; the person who records the outcome reads the mail. Likewise an override's mail is not read
77
+ for its date.
78
+ - The `date`. It must be a day (`YYYY-MM-DD`), nothing more.
79
+ - Whether a `desk` rejection is marked as such (`"desk": true`); it changes what work follows, not
80
+ whether the record is consistent.
81
+
82
+ ## How to fix
83
+
84
+ Save the decision in the paper folder — the mail as it arrived, the reviews as the portal shows
85
+ them, or a dated note that records a withdrawal and why — and name that file in `evidence`. If the
86
+ file exists under another name, correct the path. For an override, save the mail or page that moved
87
+ the deadline and name it in the override's `evidence`.
@@ -0,0 +1,75 @@
1
+ # cycle/port-done
2
+
3
+ **Level:** error · **Reads:** `paper.tex` → the paper's `paperlint.json` (`cycles`) → the venue
4
+ preset's `template` · **Reported on:** `paper.tex`
5
+
6
+ ## What it catches
7
+
8
+ The current cycle declares `"phase": "porting"` — the source is still in the previous venue's
9
+ template, so the venue's format rules are silent — and the paper's `\documentclass` is already the
10
+ one the venue's preset requires. The port is done and the declaration is stale.
11
+
12
+ ## Why
13
+
14
+ `"phase": "porting"` is a record of open work, not a softer severity: the day a new venue is chosen
15
+ the source is still in the old venue's template, and a page count under the wrong template says
16
+ nothing about the new limit. While the phase is declared, every `pdf/*`, `format/*`, `anonymity/*`
17
+ and `tex/*` venue rule stays silent for that paper. A declaration that silences rules must not be
18
+ able to stay on after its reason is gone: once the class line is the venue's, this rule fires, and
19
+ the only way to be quiet again is to drop the phase — which turns the format rules back on.
20
+
21
+ ## Examples
22
+
23
+ Failing — `paperlint.json`:
24
+
25
+ ```json
26
+ {
27
+ "cycles": [
28
+ {
29
+ "id": "blind-2027",
30
+ "venue": { "kind": "preset", "extends": "./blindconf.jsonc" },
31
+ "opened": "2026-09-09",
32
+ "phase": "porting"
33
+ }
34
+ ]
35
+ }
36
+ ```
37
+
38
+ where the preset's `template` is `\documentclass[conference,compsoc]{IEEEtran}`, and `paper.tex`
39
+ begins with that very line:
40
+
41
+ ```text
42
+ papers/my-paper/paper.tex
43
+ 1:1 error the \documentclass is already `\documentclass[conference,compsoc]{IEEEtran}`, which
44
+ BlindConf requires, and the cycle «blind-2027» still declares "phase": "porting" — the
45
+ port is done: remove the phase, so the venue's format rules judge this paper again
46
+ cycle/port-done
47
+ ```
48
+
49
+ Passing:
50
+
51
+ - the same paper with the phase removed (or `"phase": "prepared"`);
52
+ - a cycle still porting whose `paper.tex` still has the old venue's class line — the port is open
53
+ work, as declared;
54
+ - a preset that names no `template`: there is nothing to compare, as for `tex/template`;
55
+ - a paper whose `paperlint.json` has no `cycles`.
56
+
57
+ ## Options / preset fields
58
+
59
+ No rule options. It reads the preset's `template`, the field [`tex/template`](../tex/template.md)
60
+ reads, and compares the way that rule does.
61
+
62
+ ## What it does not check
63
+
64
+ - Whether the port is complete beyond the class line — the page limit, the fonts, the sections. Those
65
+ are the format rules' to judge, and they judge again the moment the phase is dropped.
66
+ - A cycle declared `porting` whose class line is still the old one: that is the state the phase
67
+ describes.
68
+ - A `porting` cycle that has already sent a PDF — [`cycle/stage`](stage.md) reports that on the
69
+ scorecard.
70
+
71
+ ## How to fix
72
+
73
+ Remove `"phase": "porting"` from the current cycle in `paperlint.json`, then run `paperlint lint`
74
+ and `paperlint build`: the venue's format rules judge the paper again, and what they report is the
75
+ rest of the port.
@@ -0,0 +1,152 @@
1
+ # cycle/record
2
+
3
+ **Level:** error · **Reads:** the paper's `paperlint.json` (`cycles`) and the venue presets it names ·
4
+ **Reported on:** the first line of the paper's `PIPELINE-STATUS.md`
5
+
6
+ ## What it catches
7
+
8
+ The paper's `cycles` — its attempts at venues, one entry each, oldest first — do not describe one
9
+ consistent history:
10
+
11
+ - **one source's deadlines are out of order.** The order a venue's deadlines keep is
12
+
13
+ ```text
14
+ registration ≤ submission ≤ resubmission < notification < camera-ready
15
+ ```
16
+
17
+ and it is checked within each source: the portal's readings among themselves, the call's among
18
+ themselves, the overrides among themselves. A call's abstract day after its own paper day is a
19
+ date typed wrong or an AoE day miscounted.
20
+
21
+ - **a portal reading the venue's preset already carries.** A cycle whose venue is a preset derives
22
+ the portal's readings from it (`presets/msr.jsonc` carries MSR's); the same reading typed into the
23
+ paper is a second copy, and it goes stale the day paperlint updates the preset.
24
+
25
+ The structural problems of the record — a bare date where an instant is needed, two open cycles, an
26
+ open cycle that is not the last, two cycles with one id, one deadline in two entries, two readings
27
+ from one source, an override without its reason or evidence, an unknown key anywhere in the record
28
+ — are refused earlier, by `paperlint lint` itself before any rule runs, with one line naming the
29
+ file, the place and the problem, and exit code 2, the same way an unknown key in `paperlint.json` is
30
+ refused. This rule judges what the record parses with.
31
+
32
+ ## Why
33
+
34
+ A deadline is the one fact in the record whose error costs the attempt. The record keeps what each
35
+ page said — the portal, which refuses the upload, and the call, which only announces — with where and
36
+ when it was read, and the **earlier** reading binds unless a human override says otherwise. That the
37
+ portal and the call disagree is expected and is not a finding: at MSR 2027 the portal's "submissions
38
+ must be completed by" falls at 04:00 UTC on the call's _abstract_ day, four days before the call's
39
+ _paper_ day, and keeping both is how the earlier one is seen. What is a finding is a source that
40
+ contradicts itself, and a fact recorded in two places.
41
+
42
+ ## Examples
43
+
44
+ Failing — `paperlint.json`, the call's own dates inverted:
45
+
46
+ ```json
47
+ {
48
+ "cycles": [
49
+ {
50
+ "id": "conf-2027",
51
+ "venue": { "kind": "preset", "extends": "paperlint:acm-sigconf" },
52
+ "opened": "2026-09-09",
53
+ "deadlines": [
54
+ {
55
+ "what": "registration",
56
+ "observed": [
57
+ {
58
+ "at": "2026-10-25 AoE",
59
+ "source": "call",
60
+ "url": "https://conf.example/cfp",
61
+ "read": "2026-09-09"
62
+ }
63
+ ]
64
+ },
65
+ {
66
+ "what": "submission",
67
+ "observed": [
68
+ {
69
+ "at": "2026-10-23 AoE",
70
+ "source": "call",
71
+ "url": "https://conf.example/cfp",
72
+ "read": "2026-09-09"
73
+ }
74
+ ]
75
+ }
76
+ ]
77
+ }
78
+ ]
79
+ }
80
+ ```
81
+
82
+ ```text
83
+ papers/my-paper/PIPELINE-STATUS.md
84
+ 1:1 error paperlint.json, cycle «conf-2027»: the call's "registration" (2026-10-26T11:59:59Z) is
85
+ after its "submission" (2026-10-24T11:59:59Z) — a date typed wrong, or an AoE day
86
+ miscounted (write a call's date as "YYYY-MM-DD AoE" and paperlint converts it)
87
+ cycle/record
88
+ ```
89
+
90
+ Failing — a cycle at `paperlint:msr` that copies the portal's reading the preset carries:
91
+
92
+ ```text
93
+ 1:1 error paperlint.json, cycle «msr-2027»: the "submission" deadline records a portal reading,
94
+ and the preset paperlint:msr carries the portal's reading of it (2026-10-20T04:00:00Z,
95
+ read 2026-10-05) — a second copy drifts when paperlint updates the preset: delete this
96
+ one (the call's reading and an override stay) cycle/record
97
+ ```
98
+
99
+ Passing:
100
+
101
+ - the first record with the registration on `2026-10-20 AoE`;
102
+ - a cycle at `paperlint:msr` recording the call's abstract day as `registration` (`2026-10-20 AoE`)
103
+ and its paper day as `submission` (`2026-10-23 AoE`): the preset's portal `submission`
104
+ (`2026-10-20T04:00:00Z`) is earlier than both, binds, and is not a contradiction;
105
+ - a cycle with no `deadlines` at all;
106
+ - a paper whose `paperlint.json` has no `cycles` (the flat form, `"extends": "paperlint:…"`).
107
+
108
+ ## Options / preset fields
109
+
110
+ No rule options. One deadline entry:
111
+
112
+ ```jsonc
113
+ {
114
+ "what": "submission", // registration (the call's abstract) · submission (the full paper) · resubmission · notification · camera-ready
115
+ "observed": [
116
+ {
117
+ "at": "2026-10-23 AoE", // or an instant with its zone: 2026-10-20T04:00:00Z
118
+ "source": "call", // or "portal"
119
+ "url": "https://conf.example/cfp",
120
+ "read": "2026-10-05", // the day the page was read
121
+ },
122
+ ],
123
+ // optional; wins over every reading
124
+ "override": {
125
+ "at": "2026-10-27T04:00:00Z",
126
+ "reason": "the chairs extended the deadline by mail",
127
+ "evidence": "mail/extension.eml",
128
+ },
129
+ }
130
+ ```
131
+
132
+ The preset field is `deadlines` in a venue preset: a list of readings `{ "what", "at", "source",
133
+ "url", "read" }`, `at` in UTC. Shipped presets carry their portal's readings and are refreshed by a
134
+ scheduled workflow in paperlint's repository; your own preset may carry readings the same way.
135
+
136
+ ## What it does not check
137
+
138
+ - Whether a reading is still what the page says. The rule has no network and no clock; a shipped
139
+ preset's readings are re-read daily in paperlint's repository and reach you as a patch release.
140
+ A venue without a preset (`"kind": "named"`) is re-read by you.
141
+ - Whether a deadline has passed, or how long is left.
142
+ - A disagreement between sources — that is what the record holds; the earlier reading binds.
143
+ - Whether an override's evidence exists — that is [`cycle/evidence`](evidence.md).
144
+ - Whether the scorecard's stages name these cycles — that is [`cycle/stage`](stage.md).
145
+
146
+ ## How to fix
147
+
148
+ Correct the date. For a deadline read from a call for papers, write it as the call prints it,
149
+ `"YYYY-MM-DD AoE"`, and let paperlint do the arithmetic; for one read from a portal, write the instant
150
+ the portal shows with its zone. Delete a portal reading the venue's preset already carries. A
151
+ deadline the chairs moved by mail is an `override`, with the mail saved in the paper folder as its
152
+ `evidence`.
@@ -0,0 +1,85 @@
1
+ # cycle/stage
2
+
3
+ **Level:** error · **Reads:** the paper's `PIPELINE-STATUS.md` frontmatter (`stages`) and its
4
+ `paperlint.json` (`cycles`) · **Reported on:** the frontmatter of `PIPELINE-STATUS.md`
5
+
6
+ ## What it catches
7
+
8
+ With `cycles` declared in the paper's `paperlint.json`, a `stages` entry in the scorecard's
9
+ frontmatter must say which attempt it belongs to, `cycle: <id>`, and that attempt must exist. Three
10
+ things can be wrong:
11
+
12
+ - a stage names no `cycle`;
13
+ - a stage names a `cycle` no declared cycle has as its `id`;
14
+ - a `submitted` or `camera-ready` stage belongs to a cycle whose `phase` is still `porting` — a PDF
15
+ was sent to the venue while the record says the source is still in the previous venue's template.
16
+
17
+ Without `cycles` (the flat form) the rule is silent and `venue:` stays what it was.
18
+
19
+ ## Why
20
+
21
+ A paper's attempts are recorded once, in `paperlint.json`; the scorecard's stages are the evidence
22
+ — which PDF, how many bytes, when. A stage that names its cycle says which attempt the evidence
23
+ belongs to, and the venue's name is then spelled once, in the preset, instead of as a free string
24
+ retyped per stage (`REALM @ EMNLP 2026`, `AISec 2026 @ ACM CCS`, …). A stage whose cycle does not
25
+ exist is evidence of nothing.
26
+
27
+ The `porting` case is the other half of a declaration: `"phase": "porting"` silences the venue's
28
+ format rules because the source is, by declaration, not yet in the venue's template. A PDF sent
29
+ from such a cycle contradicts that declaration — either the port is done and the phase is stale, or
30
+ the stage is wrong.
31
+
32
+ ## Examples
33
+
34
+ Failing — `paperlint.json` declares one cycle, `open-2027`, and `PIPELINE-STATUS.md` has
35
+
36
+ ```yaml
37
+ ---
38
+ stages:
39
+ - stage: submitted
40
+ date: 2026-07-22
41
+ pdf: versions/a.pdf
42
+ bytes: 1
43
+ ---
44
+ ```
45
+
46
+ ```text
47
+ papers/my-paper/PIPELINE-STATUS.md
48
+ 1:1 error the «submitted» stage (2026-07-22) names no `cycle` — this paper declares `cycles` in
49
+ paperlint.json, so each stage says which attempt it belongs to: `cycle: <id>`, one of
50
+ open-2027 cycle/stage
51
+ ```
52
+
53
+ Passing:
54
+
55
+ ```yaml
56
+ ---
57
+ stages:
58
+ - stage: submitted
59
+ cycle: open-2027
60
+ date: 2026-07-22
61
+ pdf: versions/a.pdf
62
+ bytes: 1
63
+ ---
64
+ ```
65
+
66
+ Also passing: a paper without `cycles`, whatever its stages say.
67
+
68
+ ## Options / preset fields
69
+
70
+ No rule options.
71
+
72
+ ## What it does not check
73
+
74
+ - The PDF, its bytes, or the frozen source — [`paper/stages`](../../rules.md) and `paper/source`
75
+ do.
76
+ - Whether a stage's `date` falls inside its cycle (after `opened`, before the outcome's `date`).
77
+ - Whether a cycle without any `submitted` stage was in fact submitted; that is the portal's record
78
+ (`paperlint submission`), not the scorecard's.
79
+ - The rest of the cycle record — [`cycle/record`](record.md) does.
80
+
81
+ ## How to fix
82
+
83
+ Add `cycle: <id>` to each stage, with the id of the cycle the PDF was sent in. For a stage whose
84
+ cycle is still `porting`: if the port is done, remove `"phase": "porting"` from the cycle
85
+ ([`cycle/port-done`](port-done.md) says when); if no PDF was sent, remove the stage.