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
@@ -0,0 +1,387 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * THE SHIPPED PRESETS' DEADLINES, RE-READ FROM THEIR PORTALS — run once a day by
4
+ * `.github/workflows/deadlines.yml`, which opens or updates one pull request when a value changed.
5
+ *
6
+ * node scripts/refresh-deadlines.ts [--presets <dir>] [--summary <file.md>]
7
+ *
8
+ * For every preset in the directory whose `portal` has an adapter (HotCRP today), the portal's public
9
+ * deadlines page is read and compared with the preset's `deadlines`:
10
+ *
11
+ * a kind the page lists at the instant the preset holds kept as it is, its `read` day too
12
+ * a kind the page lists at another instant rewritten, `read` = today
13
+ * a kind the page lists that the preset does not hold added, `read` = today
14
+ * a kind the preset holds that the page no longer lists KEPT and noted — a portal may drop a
15
+ * passed deadline; the reading stays true
16
+ * a reading from the call never touched: it is not the portal's
17
+ *
18
+ * Only a changed value rewrites the file, so an unchanged portal produces no diff and no pull
19
+ * request: a `read` day says when the value was last READ DIFFERENT, not when the job last ran.
20
+ *
21
+ * ── IT REPORTS, IT DOES NOT BLOCK ─────────────────────────────────────────────────────────────
22
+ * A portal that is down, gone or no longer a HotCRP page is a `::warning::` line and a line in the
23
+ * summary, and the run exits 0 — the venue's outage is not this repository's defect, and the next
24
+ * day's run reads it again. A preset that does not parse IS this repository's defect: `::error::`,
25
+ * exit 1. The main CI never runs this script against the network; its tests replay the pages
26
+ * recorded in `src/adapters/hotcrp/cassette/`.
27
+ *
28
+ * The block is found on the JSONC's syntax tree (TypeScript's JSON parser, the one the presets are
29
+ * read with) and replaced; everything else in the file — its comments above all — is left as it
30
+ * is, and Prettier formats the result as `npm run fmt` would.
31
+ */
32
+ import { readdirSync, readFileSync, writeFileSync } from "node:fs";
33
+ import { dirname, join } from "node:path";
34
+ import { fileURLToPath } from "node:url";
35
+ import { format } from "prettier";
36
+ import ts from "typescript";
37
+ import { isMain } from "../skills/paper-pipeline/scripts/consumer.mjs";
38
+ import { hotcrpDeadlines } from "../src/adapters/hotcrp/index.ts";
39
+ import {
40
+ DEADLINE_WHATS,
41
+ type DeadlineWhat,
42
+ type IsoDate,
43
+ type PortalDeadlines,
44
+ type Reading,
45
+ } from "../src/domain/deadline.ts";
46
+ import type { Result } from "../src/domain/result.ts";
47
+ import {
48
+ supportedPortal,
49
+ type PortalFailure,
50
+ } from "../src/domain/submission.ts";
51
+ import { messageOf } from "../src/domain/text.ts";
52
+ import { presetsDir } from "../src/package-dirs.ts";
53
+ import { parsePreset } from "../src/tex-requirements.ts";
54
+
55
+ const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
56
+
57
+ /** Reads the deadlines page of the portal site at `site` (no trailing slash). */
58
+ export type ReadPortal = (
59
+ site: string,
60
+ ) => Promise<Result<PortalDeadlines, PortalFailure>>;
61
+
62
+ /** What happened to one preset. */
63
+ export type Outcome =
64
+ | { readonly kind: "skipped"; readonly why: string }
65
+ | { readonly kind: "broken"; readonly why: string }
66
+ | { readonly kind: "unreadable"; readonly url: string; readonly why: string }
67
+ | {
68
+ readonly kind: "unchanged";
69
+ readonly url: string;
70
+ readonly notes: readonly string[];
71
+ }
72
+ | {
73
+ readonly kind: "changed";
74
+ readonly url: string;
75
+ readonly text: string;
76
+ readonly changes: readonly string[];
77
+ readonly notes: readonly string[];
78
+ };
79
+
80
+ /** The preset's readings after the page's answer, what changed, and what is worth a note. */
81
+ export interface Refreshed {
82
+ readonly readings: readonly Reading[];
83
+ readonly changes: readonly string[];
84
+ readonly notes: readonly string[];
85
+ }
86
+
87
+ /** The page's instants for one kind, earliest first. */
88
+ const pageAts = (page: PortalDeadlines, what: DeadlineWhat): string[] =>
89
+ page.deadlines
90
+ .filter((d) => d.what === what)
91
+ .map((d) => d.at)
92
+ .toSorted();
93
+
94
+ /** A kind the page does not list: the preset's reading, if it holds one, is kept and noted. */
95
+ const offThePage = (
96
+ what: DeadlineWhat,
97
+ held: Reading | undefined,
98
+ ): Refreshed =>
99
+ held === undefined
100
+ ? { readings: [], changes: [], notes: [] }
101
+ : {
102
+ readings: [held],
103
+ changes: [],
104
+ notes: [`${what}: no longer on the page — kept as read ${held.read}`],
105
+ };
106
+
107
+ /** One kind: the preset's portal reading against the page's. */
108
+ function oneKind(
109
+ what: DeadlineWhat,
110
+ held: Reading | undefined,
111
+ ats: readonly string[],
112
+ o: { readonly url: string; readonly today: IsoDate },
113
+ ): Refreshed {
114
+ const notes =
115
+ ats.length > 1
116
+ ? [`${what}: the page lists ${ats.join(", ")} — the earliest is recorded`]
117
+ : [];
118
+ const at = ats[0];
119
+ if (at === undefined) return offThePage(what, held);
120
+ if (held?.at === at) return { readings: [held], changes: [], notes };
121
+ const fresh: Reading = {
122
+ what,
123
+ at,
124
+ source: "portal",
125
+ url: o.url,
126
+ read: o.today,
127
+ };
128
+ const change =
129
+ held === undefined ? `${what}: added ${at}` : `${what}: ${held.at} → ${at}`;
130
+ return { readings: [fresh], changes: [change], notes };
131
+ }
132
+
133
+ /**
134
+ * The preset's readings after the portal answered `page` at `url` on `today`. The call's readings
135
+ * are kept as they are; the portal's are kept, rewritten or added kind by kind. Pure.
136
+ */
137
+ export function refreshReadings(
138
+ current: readonly Reading[],
139
+ page: PortalDeadlines,
140
+ url: string,
141
+ today: IsoDate,
142
+ ): Refreshed {
143
+ const perKind = DEADLINE_WHATS.map((what) => {
144
+ const portal = oneKind(
145
+ what,
146
+ current.find((r) => r.what === what && r.source === "portal"),
147
+ pageAts(page, what),
148
+ { url, today },
149
+ );
150
+ const call = current.filter((r) => r.what === what && r.source === "call");
151
+ return { ...portal, readings: [...portal.readings, ...call] };
152
+ });
153
+ return {
154
+ readings: perKind.flatMap((k) => k.readings),
155
+ changes: perKind.flatMap((k) => k.changes),
156
+ notes: perKind.flatMap((k) => k.notes),
157
+ };
158
+ }
159
+
160
+ /** The top-level property `name` of a JSONC object, on its syntax tree; undefined when absent. */
161
+ function property(
162
+ file: ts.JsonSourceFile,
163
+ name: string,
164
+ ): ts.PropertyAssignment | undefined {
165
+ const top = file.statements[0]?.expression;
166
+ if (top === undefined || !ts.isObjectLiteralExpression(top)) return undefined;
167
+ return top.properties
168
+ .filter(ts.isPropertyAssignment)
169
+ .find((p) => ts.isStringLiteral(p.name) && p.name.text === name);
170
+ }
171
+
172
+ /**
173
+ * The preset's text with `deadlines` set to `readings`: the value replaced where the key is, or the
174
+ * key inserted after `portal`. Formatted as the repository formats a preset.
175
+ */
176
+ export async function withDeadlines(
177
+ text: string,
178
+ file: string,
179
+ readings: readonly Reading[],
180
+ ): Promise<string> {
181
+ const tree = ts.parseJsonText(file, text);
182
+ const json = JSON.stringify(readings, null, 2);
183
+ const held = property(tree, "deadlines");
184
+ const spliced =
185
+ held === undefined
186
+ ? insertAfterPortal(tree, text, json)
187
+ : text.slice(0, held.initializer.getStart(tree)) +
188
+ json +
189
+ text.slice(held.initializer.end);
190
+ return format(spliced, { filepath: file });
191
+ }
192
+
193
+ /** `"deadlines": …` after the `portal` property — only a preset with a portal is ever refreshed. */
194
+ function insertAfterPortal(
195
+ tree: ts.JsonSourceFile,
196
+ text: string,
197
+ json: string,
198
+ ): string {
199
+ const portal = property(tree, "portal");
200
+ if (portal === undefined)
201
+ throw new Error(`${tree.fileName}: no "portal" to put "deadlines" after`);
202
+ return `${text.slice(0, portal.end)},\n\n "deadlines": ${json}${text.slice(portal.end)}`;
203
+ }
204
+
205
+ /** A failure to read the page, in one line. */
206
+ function failureText(f: PortalFailure): string {
207
+ switch (f.kind) {
208
+ case "unreachable":
209
+ return `unreachable: ${f.detail}`;
210
+ case "refused":
211
+ return `HTTP ${String(f.httpStatus)}: ${f.messages.map((m) => m.message).join("; ")}`;
212
+ case "malformed":
213
+ return `not the page expected (HTTP ${String(f.httpStatus)}): ${f.detail}`;
214
+ }
215
+ }
216
+
217
+ /** One preset: its portal read, its readings compared, its new text when one changed. */
218
+ export async function refreshPreset(o: {
219
+ readonly file: string;
220
+ readonly text: string;
221
+ readonly read: ReadPortal;
222
+ readonly today: IsoDate;
223
+ }): Promise<Outcome> {
224
+ const parsed = ((): ReturnType<typeof parsePreset> | string => {
225
+ try {
226
+ return parsePreset(o.text, o.file, presetsDir());
227
+ } catch (e) {
228
+ return messageOf(e);
229
+ }
230
+ })();
231
+ if (typeof parsed === "string") return { kind: "broken", why: parsed };
232
+ if (parsed.portal === null) return { kind: "skipped", why: "no portal" };
233
+ const portal = supportedPortal(parsed.portal);
234
+ if (!portal.ok) return { kind: "skipped", why: portal.error };
235
+ const url = `${portal.value.url}/deadlines`;
236
+ const page = await o.read(portal.value.url);
237
+ if (!page.ok)
238
+ return { kind: "unreadable", url, why: failureText(page.error) };
239
+ const r = refreshReadings(parsed.deadlines ?? [], page.value, url, o.today);
240
+ if (r.changes.length === 0) return { kind: "unchanged", url, notes: r.notes };
241
+ const text = await withDeadlines(o.text, o.file, r.readings);
242
+ return { kind: "changed", url, text, changes: r.changes, notes: r.notes };
243
+ }
244
+
245
+ /** The lines one preset's outcome contributes: to the log (annotations included) and to the summary. */
246
+ function report(
247
+ name: string,
248
+ o: Outcome,
249
+ ): { readonly log: readonly string[]; readonly md: readonly string[] } {
250
+ switch (o.kind) {
251
+ case "skipped":
252
+ return { log: [], md: [] };
253
+ case "broken":
254
+ return {
255
+ log: [`::error title=deadlines::${name} does not parse: ${o.why}`],
256
+ md: [`- 🔴 \`${name}\` does not parse: ${o.why}`],
257
+ };
258
+ case "unreadable":
259
+ return {
260
+ log: [
261
+ `::warning title=deadlines::${name}: could not read ${o.url} — ${o.why}`,
262
+ ],
263
+ md: [`- ⚠️ \`${name}\`: could not read ${o.url} — ${o.why}`],
264
+ };
265
+ case "unchanged":
266
+ return { log: o.notes.map((n) => `${name}: ${n}`), md: [] };
267
+ case "changed":
268
+ return changedReport(name, o);
269
+ }
270
+ }
271
+
272
+ /** A changed preset: each change and note, in the log and as a list in the summary. */
273
+ const changedReport = (
274
+ name: string,
275
+ o: Extract<Outcome, { kind: "changed" }>,
276
+ ): { readonly log: readonly string[]; readonly md: readonly string[] } => {
277
+ const lines = [...o.changes, ...o.notes];
278
+ return {
279
+ log: lines.map((c) => `${name}: ${c}`),
280
+ md: [`- \`${name}\` (${o.url})`, ...lines.map((c) => ` - ${c}`)],
281
+ };
282
+ };
283
+
284
+ export interface MainOptions {
285
+ readonly presetsDir: string;
286
+ readonly read: ReadPortal;
287
+ readonly today: IsoDate;
288
+ readonly log: (line: string) => void;
289
+ readonly write: (file: string, text: string) => void;
290
+ /** Where the pull request's body goes; null: nowhere. */
291
+ readonly summary: string | null;
292
+ }
293
+
294
+ /** Every preset of the directory, refreshed — a changed one written back. */
295
+ async function outcomesOf(
296
+ o: MainOptions,
297
+ ): Promise<readonly { readonly name: string; readonly out: Outcome }[]> {
298
+ const names = readdirSync(o.presetsDir)
299
+ .filter((f) => f.endsWith(".jsonc"))
300
+ .toSorted();
301
+ return Promise.all(
302
+ names.map(async (name) => {
303
+ const file = join(o.presetsDir, name);
304
+ const text = readFileSync(file, "utf8");
305
+ const out = await refreshPreset({
306
+ file,
307
+ text,
308
+ read: o.read,
309
+ today: o.today,
310
+ });
311
+ if (out.kind === "changed") o.write(file, out.text);
312
+ return { name, out };
313
+ }),
314
+ );
315
+ }
316
+
317
+ /** Every preset of the directory, refreshed. 0 unless a preset does not parse. */
318
+ export async function main(o: MainOptions): Promise<number> {
319
+ const outcomes = await outcomesOf(o);
320
+ const reports = outcomes.map(({ name, out }) => report(name, out));
321
+ reports
322
+ .flatMap((r) => r.log)
323
+ .forEach((l) => {
324
+ o.log(l);
325
+ });
326
+ const count = (k: Outcome["kind"]) =>
327
+ outcomes.filter((x) => x.out.kind === k).length;
328
+ const read = count("changed") + count("unchanged");
329
+ o.log(
330
+ count("changed") === 0
331
+ ? `✓ no deadline changed: ${String(read)} preset(s) with a portal read, ${String(count("skipped"))} skipped`
332
+ : `${String(count("changed"))} preset(s) changed`,
333
+ );
334
+ if (o.summary !== null)
335
+ o.write(
336
+ o.summary,
337
+ summaryOf(
338
+ o.today,
339
+ reports.flatMap((r) => r.md),
340
+ ),
341
+ );
342
+ return count("broken") > 0 ? 1 : 0;
343
+ }
344
+
345
+ /** The pull request's body. */
346
+ const summaryOf = (today: IsoDate, lines: readonly string[]): string =>
347
+ [
348
+ `The venue presets' deadlines, re-read on ${today} from each preset's portal by \`.github/workflows/deadlines.yml\` (\`scripts/refresh-deadlines.ts\`). A reading whose value changed carries today's \`read\` day; the others keep theirs.`,
349
+ "",
350
+ ...lines,
351
+ "",
352
+ "Merging releases a patch; papers get the new dates through their normal update of paperlint.",
353
+ "",
354
+ ].join("\n");
355
+
356
+ /** `--presets <dir>` and `--summary <file>` from the command line. */
357
+ export function argsOf(argv: readonly string[]): {
358
+ readonly presets: string;
359
+ readonly summary: string | null;
360
+ } {
361
+ const value = (flag: string): string | null => {
362
+ const i = argv.indexOf(flag);
363
+ return i === -1 ? null : (argv[i + 1] ?? null);
364
+ };
365
+ return {
366
+ presets: value("--presets") ?? join(ROOT, "presets"),
367
+ summary: value("--summary"),
368
+ };
369
+ }
370
+
371
+ // `isMain`, not a comparison with `file://${argv[1]}`: that is false through a symlink (consumer.mjs).
372
+ if (isMain(import.meta.url)) {
373
+ const args = argsOf(process.argv.slice(2));
374
+ process.exit(
375
+ await main({
376
+ presetsDir: args.presets,
377
+ read: (site) => hotcrpDeadlines({ url: site }).read(),
378
+ // The clock is read here, at the edge: the day a reading was taken.
379
+ today: new Date().toISOString().slice(0, 10),
380
+ log: console.log,
381
+ write: (file, text) => {
382
+ writeFileSync(file, text);
383
+ },
384
+ summary: args.summary,
385
+ }),
386
+ );
387
+ }
@@ -4,7 +4,7 @@ description: On acceptance, turn the anonymized-for-review paper into the de-ano
4
4
  allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, WebSearch, WebFetch]
5
5
  ---
6
6
 
7
- <!-- vigiles:sha256:5c85ef4004c31956 compiled from skills/camera-ready/SKILL.md.spec.ts -->
7
+ <!-- vigiles:sha256:1c9ad89e55c3e89a compiled from skills/camera-ready/SKILL.md.spec.ts -->
8
8
 
9
9
  # camera-ready — de-anonymize an accepted paper into the final proceedings version
10
10
 
@@ -94,6 +94,13 @@ and going over is a hard reject at the proceedings stage. Recompile
94
94
  If tight, ACM allows references to spill into the reference-only extra pages.
95
95
 
96
96
  ## 5. Ship
97
+ - **Before the final PDF goes up, hand the last check to the author, in plain words.** Tell them that
98
+ paperlint passing is not the venue or the publisher accepting the format, and ask them to open the
99
+ exact PDF they are about to upload and check it against the venue's call for papers and its
100
+ camera-ready instructions themselves — the page count and what counts toward the limit (the
101
+ camera-ready's extra page, if any), the template and its class options, the author block and
102
+ acknowledgements now de-anonymized, and that every figure, table and reference renders. Upload once
103
+ they say they have looked.
97
104
  - Rebuild the final PDF, upload to the camera-ready system (often the same HotCRP, or an ACM eRights /
98
105
  TAPS pipeline — follow the copyright email).
99
106
  - Complete the **ACM/IEEE eRights** copyright form → paste the returned copyright block + DOI into the
@@ -109,6 +109,13 @@ and going over is a hard reject at the proceedings stage. Recompile
109
109
  If tight, ACM allows references to spill into the reference-only extra pages.
110
110
 
111
111
  ## 5. Ship
112
+ - **Before the final PDF goes up, hand the last check to the author, in plain words.** Tell them that
113
+ paperlint passing is not the venue or the publisher accepting the format, and ask them to open the
114
+ exact PDF they are about to upload and check it against the venue's call for papers and its
115
+ camera-ready instructions themselves — the page count and what counts toward the limit (the
116
+ camera-ready's extra page, if any), the template and its class options, the author block and
117
+ acknowledgements now de-anonymized, and that every figure, table and reference renders. Upload once
118
+ they say they have looked.
112
119
  - Rebuild the final PDF, upload to the camera-ready system (often the same HotCRP, or an ACM eRights /
113
120
  TAPS pipeline — follow the copyright email).
114
121
  - Complete the **ACM/IEEE eRights** copyright form → paste the returned copyright block + DOI into the
@@ -4,7 +4,7 @@ description: "Read or change a paper's submission on the venue's HotCRP portal v
4
4
  allowed-tools: [Read, Grep, Glob, Bash]
5
5
  ---
6
6
 
7
- <!-- vigiles:sha256:deb3d74f5c3f93a8 compiled from skills/submission-portal/SKILL.md.spec.ts -->
7
+ <!-- vigiles:sha256:4b3a8c0b51ebe206 compiled from skills/submission-portal/SKILL.md.spec.ts -->
8
8
 
9
9
  # submission-portal — read and update the submission on the portal, then prove it landed
10
10
 
@@ -14,7 +14,7 @@ supported; for any other portal the command says so and the upload is done by ha
14
14
  ## What must be in place
15
15
 
16
16
  - **The venue preset declares the portal**: `"portal": { "kind": "hotcrp", "url": "<the site>" }`
17
- (`paperlint:aidc` has it). For your own venue preset, add it there.
17
+ (`paperlint:aidc` and `paperlint:msr` have it). For your own venue preset, add it there.
18
18
  - **The paper's `paperlint.json` names its submission**: `"submission": { "id": <number> }` — the
19
19
  number HotCRP shows for the paper.
20
20
  - **A token in `HOTCRP_TOKEN`**, created by the author on the HotCRP site under Account settings →
@@ -15,7 +15,7 @@ supported; for any other portal the command says so and the upload is done by ha
15
15
  ## What must be in place
16
16
 
17
17
  - **The venue preset declares the portal**: \`"portal": { "kind": "hotcrp", "url": "<the site>" }\`
18
- (\`paperlint:aidc\` has it). For your own venue preset, add it there.
18
+ (\`paperlint:aidc\` and \`paperlint:msr\` have it). For your own venue preset, add it there.
19
19
  - **The paper's \`paperlint.json\` names its submission**: \`"submission": { "id": <number> }\` — the
20
20
  number HotCRP shows for the paper.
21
21
  - **A token in \`HOTCRP_TOKEN\`**, created by the author on the HotCRP site under Account settings →
@@ -4,7 +4,7 @@ description: End-to-end playbook for submitting a peer-reviewed paper to a doubl
4
4
  allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, WebSearch, WebFetch, Skill]
5
5
  ---
6
6
 
7
- <!-- vigiles:sha256:8f53cae32633b002 compiled from skills/submit-paper/SKILL.md.spec.ts -->
7
+ <!-- vigiles:sha256:81d5777962c2864a compiled from skills/submit-paper/SKILL.md.spec.ts -->
8
8
 
9
9
  # submit-paper — get a reviewed paper from "done" to "ready for review"
10
10
 
@@ -20,6 +20,7 @@ paper against:
20
20
  - `presets/agenticdev.md` — AgenticDev @ ASE (workshop; ASE workshop proceedings).
21
21
  - `presets/aisec.md` — AISec @ ACM CCS (security workshop; ACM DL; harder bar).
22
22
  - `presets/aidc.md` — AIDC @ IEEE ACSAC (workshop; IEEE template, LLM Usage Statement).
23
+ - `presets/msr.md` — MSR Technical Papers (conference; IEEE template from 2027, appendices count in the page limit).
23
24
  - `presets/realm.md` — REALM @ EMNLP (ACL family, OpenReview).
24
25
 
25
26
  **The card is prose; the machine-checked format is the PRESET beside it.** A paper declares its venue
@@ -110,6 +111,13 @@ Many workshop HotCRPs have **no supplementary-upload field** — check first; if
110
111
  An independent researcher with no ties usually checks **none** — that's honest.
111
112
 
112
113
  ## 5. Submit
114
+ - **Before the PDF goes up — and before every Replace — hand the last check to the author, in plain
115
+ words.** Tell them that paperlint passing is not the venue accepting the format: the preset is
116
+ transcribed from a call that can change, and any check can miss something. Ask them to open the
117
+ exact PDF they are about to upload and check it against the venue's call for papers themselves —
118
+ the page count and what counts toward the limit (appendices, references), the template and its
119
+ class options, anonymity, and that every figure, table and reference renders. Upload once they say
120
+ they have looked. (`paperlint submission update` prints the same request before it sends.)
113
121
  - **Save and submit**, then **mark it "ready for review."** A saved draft alone is NOT evaluated — this
114
122
  is the step people miss.
115
123
  - You can edit until the deadline: use **Replace** on the Submission field to swap the PDF for a late
@@ -36,6 +36,7 @@ paper against:
36
36
  - \`presets/agenticdev.md\` — AgenticDev @ ASE (workshop; ASE workshop proceedings).
37
37
  - \`presets/aisec.md\` — AISec @ ACM CCS (security workshop; ACM DL; harder bar).
38
38
  - \`presets/aidc.md\` — AIDC @ IEEE ACSAC (workshop; IEEE template, LLM Usage Statement).
39
+ - \`presets/msr.md\` — MSR Technical Papers (conference; IEEE template from 2027, appendices count in the page limit).
39
40
  - \`presets/realm.md\` — REALM @ EMNLP (ACL family, OpenReview).
40
41
 
41
42
  **The card is prose; the machine-checked format is the PRESET beside it.** A paper declares its venue
@@ -126,6 +127,13 @@ Many workshop HotCRPs have **no supplementary-upload field** — check first; if
126
127
  An independent researcher with no ties usually checks **none** — that's honest.
127
128
 
128
129
  ## 5. Submit
130
+ - **Before the PDF goes up — and before every Replace — hand the last check to the author, in plain
131
+ words.** Tell them that paperlint passing is not the venue accepting the format: the preset is
132
+ transcribed from a call that can change, and any check can miss something. Ask them to open the
133
+ exact PDF they are about to upload and check it against the venue's call for papers themselves —
134
+ the page count and what counts toward the limit (appendices, references), the template and its
135
+ class options, anonymity, and that every figure, table and reference renders. Upload once they say
136
+ they have looked. (\`paperlint submission update\` prints the same request before it sends.)
129
137
  - **Save and submit**, then **mark it "ready for review."** A saved draft alone is NOT evaluated — this
130
138
  is the step people miss.
131
139
  - You can edit until the deadline: use **Replace** on the Submission field to swap the PDF for a late
@@ -22,3 +22,25 @@ To re-record: run the same four requests with `curl` against a HotCRP site where
22
22
  submission and a token (`Authorization: bearer $HOTCRP_TOKEN`), always with `dry_run=1` on a `POST`,
23
23
  save each body, then replace every identifying value with a fake of the same type before committing.
24
24
  These files ship in the npm package with the rest of `src/`, like the tests beside them.
25
+
26
+ ## The deadlines pages
27
+
28
+ `deadlines-<site>.html` are five live sites' `GET /deadlines` pages and one site that does not exist,
29
+ recorded whole on 2026-10-05 (`curl`, no account): the page is public and carries no personal data,
30
+ so nothing is replaced. `deadlines.test.ts` parses each through the adapter's real parser; the
31
+ expected values were read off the pages' text by hand first. `/api/deadlines` on the same sites
32
+ answers 401 «Missing credentials», so the HTML page is the source.
33
+ `scripts/refresh-deadlines.test.ts` replays the MSR and AIDC pages through the scheduled refresh of
34
+ the presets' `deadlines` (`.github/workflows/deadlines.yml`). Re-read live through the adapter at
35
+ 14:38 UTC the same day, both pages differed from these files only in the script nonces and `now`.
36
+
37
+ | file | site | HTTP | what it shows |
38
+ | ------------------------- | --------------------- | ---: | ------------------------------------------------------------------ |
39
+ | `deadlines-msr2027.html` | msr2027.hotcrp.com | 200 | submission and resubmission; the resubmission only in the list |
40
+ | `deadlines-aidc26.html` | aidc.submit.acsac.org | 200 | submission, and a final-version deadline only in the status object |
41
+ | `deadlines-sosp26.html` | sosp26.hotcrp.com | 200 | registration, submission, a late resubmission, final version |
42
+ | `deadlines-icse2027.html` | icse2027.hotcrp.com | 200 | a round that is not open (`"open":false`); dates printed as AoE |
43
+ | `deadlines-fse2027.html` | fse2027.hotcrp.com | 200 | one submission deadline printed as AoE |
44
+ | `deadlines-nosuch.html` | acsac2026.hotcrp.com | 404 | «No such conference» |
45
+
46
+ To re-record: `curl -sS -D - -o deadlines-<site>.html https://<site>/deadlines`.
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
5
+ <link href="stylesheets/style.css?mtime=1782300564" rel="stylesheet">
6
+ <link rel="icon" type="image/png" href="images/review48.png">
7
+ <title>Deadlines - AIDC&#039;26</title>
8
+ </head>
9
+ <body id="m-deadlines" data-upload-limit="33554432"><div id="p-page" class="need-banner-offset"><header id="p-header"><hr class="c"><div id="h-site" class="header-site-page"><a class="q" href="/"><span class="header-site-name">AIDC&#039;26</span></a></div><div id="h-right"></div><div id="h-page"><h1 id="h-title">Deadlines</h1></div><nav id="p-quicklinks" aria-label="Submission search"></nav><hr class="c">
10
+ <div id="h-messages" class="want-mx-auto"></div></header>
11
+ <main id="p-body">
12
+ <h3>Past</h3><dl class="bsp"><dt><strong>Submission deadline</strong>: <span class="need-usertime" data-ts="1791028799">Saturday Oct 3, 2026, 11:59:59 UTC</span></dt><dd> submissions must be completed by this deadline to be reviewed.</dd></dl>
13
+ <hr class="c"></main></div><footer id="p-footer" class="need-banner-offset banner-bottom"><a class="noq" href="https://hotcrp.com/">HotCRP</a></footer><!--[if lt IE 9]><script src="scripts/jquery-1.12.4.min.js?mtime=1782300564" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ="></script><![endif]-->
14
+ <![if !IE|gte IE 9]><script src="scripts/jquery-3.7.1.min.js?mtime=1782300564" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="></script><![endif]>
15
+ <script>window.siteinfo={"site_relative":"","base":"\/","suffix":"","assets":"","cookie_params":"; SameSite=Lax","postvalue":".empty","snouns":["submission","submissions","Submission","Submissions"],"user":[]}</script><script src="scripts/script.js?mtime=1782300564"></script>
16
+ <script>hotcrp.onload.time(0,1);hotcrp.init_deadlines({"now":1791194489.473936,"email":null,"sub":{"open":true,"sub":1791028799,"blind":true},"final":{"open":true,"done":1793447999}})</script></body>
17
+ </html>
@@ -0,0 +1,24 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
5
+ <meta name="google" content="notranslate">
6
+ <meta name="theme-color" content="#ffb7ca">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <link href="https://static.hotcrp.com/@1790985506/stylesheets/style.css" rel="stylesheet">
9
+ <link href="https://static.hotcrp.com/@1786633883/stylesheets/mobile.css" media="screen and (max-width: 1100px)" rel="stylesheet">
10
+ <link href="https://static.hotcrp.com/@1788721683/stylesheets/plugin.css" rel="stylesheet">
11
+ <link rel="icon" type="image/svg+xml" href="https://static.hotcrp.com/images/review48.svg">
12
+ <title>Deadlines - FSE 2027</title>
13
+ </head>
14
+ <body id="m-deadlines" data-upload-limit="104857600" data-document-max-size="600000000"><div id="p-page" class="need-banner-offset"><header id="p-header"><hr class="c"><div id="h-site" class="header-site-page"><a class="q" href="/"><span class="header-site-name">FSE 2027</span></a></div><div id="h-right"></div><div id="h-page"><h1 id="h-title">Deadlines</h1></div><nav id="p-quicklinks" aria-label="Submission search"></nav><hr class="c">
15
+ <div id="h-messages" class="want-mx-auto"></div></header>
16
+ <main id="p-body">
17
+ <h3>Past</h3><dl class="bsp"><dt><strong>Submission deadline</strong>: <span class="need-usertime" data-ts="1791115140">Saturday Oct 3, 2026, 11:59 PM AoE</span></dt><dd>Submissions must be completed by this deadline to be reviewed.</dd></dl>
18
+ <hr class="c"></main></div><footer id="p-footer" class="need-banner-offset banner-bottom"><a class="noq" href="https://hotcrp.com/">HotCRP</a><!-- Version 3.4 --></footer><!--[if lt IE 9]><script nonce="P6MeSP0WsZYikm82vBcVfQ==" src="https://static.hotcrp.com/@1749780400/scripts/jquery-1.12.4.min.js" crossorigin="anonymous" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ="></script><![endif]-->
19
+ <![if !IE|gte IE 9]><script nonce="P6MeSP0WsZYikm82vBcVfQ==" src="https://static.hotcrp.com/@1749780400/scripts/jquery-3.7.1.min.js" crossorigin="anonymous" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="></script><![endif]>
20
+ <script nonce="P6MeSP0WsZYikm82vBcVfQ==">window.siteinfo={"site_relative":"","base":"\/","suffix":"","assets":"https:\/\/static.hotcrp.com\/","icons_url":"@1787797517\/images\/icons.svg","cookie_params":"; Domain=.hotcrp.com; Secure; SameSite=Lax","postvalue":".empty","download_prefix":"fse2027-","snouns":["submission","submissions","Submission","Submissions"],"user":[]}</script><script nonce="P6MeSP0WsZYikm82vBcVfQ==" src="https://static.hotcrp.com/@1790985505/scripts/script.js" crossorigin="anonymous"></script>
21
+ <script nonce="P6MeSP0WsZYikm82vBcVfQ==" src="https://static.hotcrp.com/@1777496734/scripts/markdown-it.min.js" crossorigin="anonymous"></script>
22
+ <script nonce="P6MeSP0WsZYikm82vBcVfQ==" src="https://static.hotcrp.com/@1787955704/scripts/plugin.js" crossorigin="anonymous"></script>
23
+ <script nonce="P6MeSP0WsZYikm82vBcVfQ==">hotcrp.onload.time(720,0);hotcrp.init_deadlines({"now":1791194490.16279,"email":null,"sub":{"open":true,"sub":1791115140,"blind":true}})</script></body>
24
+ </html>
@@ -0,0 +1,25 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
5
+ <meta name="google" content="notranslate">
6
+ <meta name="theme-color" content="#ffb7ca">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <link href="https://static.hotcrp.com/@1790985506/stylesheets/style.css" rel="stylesheet">
9
+ <link href="https://static.hotcrp.com/@1786633883/stylesheets/mobile.css" media="screen and (max-width: 1100px)" rel="stylesheet">
10
+ <link href="https://static.hotcrp.com/@1788721683/stylesheets/plugin.css" rel="stylesheet">
11
+ <link rel="icon" type="image/svg+xml" href="https://static.hotcrp.com/images/review48.svg">
12
+ <title>Deadlines - ICSE 2027</title>
13
+ </head>
14
+ <body id="m-deadlines" data-upload-limit="104857600" data-document-max-size="600000000"><div id="p-page" class="need-banner-offset"><header id="p-header"><hr class="c"><div id="h-site" class="header-site-page"><a class="q" href="/"><span class="header-site-name">ICSE 2027</span></a></div><div id="h-right"></div><div id="h-page"><h1 id="h-title">Deadlines</h1></div><nav id="p-quicklinks" aria-label="Submission search"></nav><hr class="c">
15
+ <div id="h-messages" class="want-mx-auto"></div></header>
16
+ <main id="p-body">
17
+ <h3>Past</h3><dl class="bsp"><dt><strong>Submission deadline</strong>: <span class="need-usertime" data-ts="1783598340">Wednesday Jul 8, 2026, 11:59 PM AoE</span></dt><dd>Submissions must be completed by this deadline to be reviewed.</dd>
18
+ <dt><strong>Registration deadline</strong>: <span class="need-usertime" data-ts="1782302399">Tuesday Jun 23, 2026, 11:59:59 PM AoE</span></dt><dd>You can register new submissions until this deadline.</dd></dl>
19
+ <hr class="c"></main></div><footer id="p-footer" class="need-banner-offset banner-bottom"><a class="noq" href="https://hotcrp.com/">HotCRP</a><!-- Version 3.4 --></footer><!--[if lt IE 9]><script nonce="FL4FfJ6tspuiYI3NwY6I0w==" src="https://static.hotcrp.com/@1749780400/scripts/jquery-1.12.4.min.js" crossorigin="anonymous" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ="></script><![endif]-->
20
+ <![if !IE|gte IE 9]><script nonce="FL4FfJ6tspuiYI3NwY6I0w==" src="https://static.hotcrp.com/@1749780400/scripts/jquery-3.7.1.min.js" crossorigin="anonymous" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="></script><![endif]>
21
+ <script nonce="FL4FfJ6tspuiYI3NwY6I0w==">window.siteinfo={"site_relative":"","base":"\/","suffix":"","assets":"https:\/\/static.hotcrp.com\/","icons_url":"@1787797517\/images\/icons.svg","cookie_params":"; Domain=.hotcrp.com; Secure; SameSite=Lax","postvalue":".empty","download_prefix":"icse2027-","snouns":["submission","submissions","Submission","Submissions"],"user":[]}</script><script nonce="FL4FfJ6tspuiYI3NwY6I0w==" src="https://static.hotcrp.com/@1790985505/scripts/script.js" crossorigin="anonymous"></script>
22
+ <script nonce="FL4FfJ6tspuiYI3NwY6I0w==" src="https://static.hotcrp.com/@1777496734/scripts/markdown-it.min.js" crossorigin="anonymous"></script>
23
+ <script nonce="FL4FfJ6tspuiYI3NwY6I0w==" src="https://static.hotcrp.com/@1787955704/scripts/plugin.js" crossorigin="anonymous"></script>
24
+ <script nonce="FL4FfJ6tspuiYI3NwY6I0w==">hotcrp.onload.time(720,0);hotcrp.init_deadlines({"now":1791194489.960642,"email":null,"sub":{"open":false,"sub":1783598340,"reg":1782302399,"blind":true}})</script></body>
25
+ </html>
@@ -0,0 +1,25 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
5
+ <meta name="google" content="notranslate">
6
+ <meta name="theme-color" content="#ffb7ca">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <link href="https://static.hotcrp.com/@1790985506/stylesheets/style.css" rel="stylesheet">
9
+ <link href="https://static.hotcrp.com/@1786633883/stylesheets/mobile.css" media="screen and (max-width: 1100px)" rel="stylesheet">
10
+ <link href="https://static.hotcrp.com/@1788721683/stylesheets/plugin.css" rel="stylesheet">
11
+ <link rel="icon" type="image/svg+xml" href="https://static.hotcrp.com/images/review48.svg">
12
+ <title>Deadlines - MSR 2027</title>
13
+ </head>
14
+ <body id="m-deadlines" data-upload-limit="104857600" data-document-max-size="600000000"><div id="p-page" class="need-banner-offset"><header id="p-header"><hr class="c"><div id="h-site" class="header-site-page"><a class="q" href="/"><span class="header-site-name">MSR 2027</span></a></div><div id="h-right"></div><div id="h-page"><h1 id="h-title">Deadlines</h1></div><nav id="p-quicklinks" aria-label="Submission search"></nav><hr class="c">
15
+ <div id="h-messages" class="want-mx-auto"></div></header>
16
+ <main id="p-body">
17
+ <h3>Upcoming</h3><dl class="bsp"><dt><strong>Submission deadline</strong>: <span class="need-usertime" data-ts="1792468800">Tuesday Oct 20, 2026, 12 AM EDT</span></dt><dd>Submissions must be completed by this deadline to be reviewed.</dd>
18
+ <dt><strong>Resubmission deadline</strong>: <span class="need-usertime" data-ts="1792728000">Friday Oct 23, 2026, 12 AM EDT</span></dt><dd>Completed submissions may be updated until this deadline.</dd></dl>
19
+ <hr class="c"></main></div><footer id="p-footer" class="need-banner-offset banner-bottom"><a class="noq" href="https://hotcrp.com/">HotCRP</a><!-- Version 3.4 --></footer><!--[if lt IE 9]><script nonce="8XgRxNPYP5XebPvVSL6MyA==" src="https://static.hotcrp.com/@1749780400/scripts/jquery-1.12.4.min.js" crossorigin="anonymous" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ="></script><![endif]-->
20
+ <![if !IE|gte IE 9]><script nonce="8XgRxNPYP5XebPvVSL6MyA==" src="https://static.hotcrp.com/@1749780400/scripts/jquery-3.7.1.min.js" crossorigin="anonymous" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="></script><![endif]>
21
+ <script nonce="8XgRxNPYP5XebPvVSL6MyA==">window.siteinfo={"site_relative":"","base":"\/","suffix":"","assets":"https:\/\/static.hotcrp.com\/","icons_url":"@1787797517\/images\/icons.svg","cookie_params":"; Domain=.hotcrp.com; Secure; SameSite=Lax","postvalue":".empty","download_prefix":"msr2027-","snouns":["submission","submissions","Submission","Submissions"],"user":[]}</script><script nonce="8XgRxNPYP5XebPvVSL6MyA==" src="https://static.hotcrp.com/@1790985505/scripts/script.js" crossorigin="anonymous"></script>
22
+ <script nonce="8XgRxNPYP5XebPvVSL6MyA==" src="https://static.hotcrp.com/@1777496734/scripts/markdown-it.min.js" crossorigin="anonymous"></script>
23
+ <script nonce="8XgRxNPYP5XebPvVSL6MyA==" src="https://static.hotcrp.com/@1787955704/scripts/plugin.js" crossorigin="anonymous"></script>
24
+ <script nonce="8XgRxNPYP5XebPvVSL6MyA==">hotcrp.onload.time(240,0);hotcrp.init_deadlines({"now":1791194488.329003,"email":null,"sub":{"open":true,"sub":1792468800,"blind":true}})</script></body>
25
+ </html>