paperlint 10.4.0 → 11.0.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.
- package/.github/workflows/deadlines.yml +27 -7
- package/README.md +16 -15
- package/dist/CLAUDE.md.spec.d.ts.map +1 -1
- package/dist/CLAUDE.md.spec.js +6 -3
- package/dist/CLAUDE.md.spec.js.map +1 -1
- package/dist/adapters/hotcrp/index.d.ts +6 -4
- package/dist/adapters/hotcrp/index.d.ts.map +1 -1
- package/dist/adapters/hotcrp/index.js +6 -4
- package/dist/adapters/hotcrp/index.js.map +1 -1
- package/dist/adapters/hotcrp/portal.io.d.ts +3 -1
- package/dist/adapters/hotcrp/portal.io.d.ts.map +1 -1
- package/dist/adapters/hotcrp/portal.io.js +36 -11
- package/dist/adapters/hotcrp/portal.io.js.map +1 -1
- package/dist/adapters/hotcrp/response.d.ts +6 -1
- package/dist/adapters/hotcrp/response.d.ts.map +1 -1
- package/dist/adapters/hotcrp/response.js +17 -0
- package/dist/adapters/hotcrp/response.js.map +1 -1
- package/dist/adapters/hotcrp/site.d.ts +9 -0
- package/dist/adapters/hotcrp/site.d.ts.map +1 -0
- package/dist/adapters/hotcrp/site.js +22 -0
- package/dist/adapters/hotcrp/site.js.map +1 -0
- package/dist/adapters/hotcrp/token.d.ts +13 -0
- package/dist/adapters/hotcrp/token.d.ts.map +1 -0
- package/dist/adapters/hotcrp/token.js +48 -0
- package/dist/adapters/hotcrp/token.js.map +1 -0
- package/dist/adapters/latex/abstract.d.ts +22 -0
- package/dist/adapters/latex/abstract.d.ts.map +1 -0
- package/dist/adapters/latex/abstract.js +187 -0
- package/dist/adapters/latex/abstract.js.map +1 -0
- package/dist/adapters/latex/captions.d.ts +9 -0
- package/dist/adapters/latex/captions.d.ts.map +1 -0
- package/dist/adapters/latex/captions.js +40 -0
- package/dist/adapters/latex/captions.js.map +1 -0
- package/dist/adapters/latex/headings.d.ts +6 -0
- package/dist/adapters/latex/headings.d.ts.map +1 -1
- package/dist/adapters/latex/headings.js +5 -4
- package/dist/adapters/latex/headings.js.map +1 -1
- package/dist/adapters/latex/index.d.ts +1 -0
- package/dist/adapters/latex/index.d.ts.map +1 -1
- package/dist/adapters/latex/index.js +3 -0
- package/dist/adapters/latex/index.js.map +1 -1
- package/dist/adapters/latex/parse.d.ts +15 -0
- package/dist/adapters/latex/parse.d.ts.map +1 -1
- package/dist/adapters/latex/parse.js +64 -1
- package/dist/adapters/latex/parse.js.map +1 -1
- package/dist/adapters/node/list-entries.io.d.ts.map +1 -1
- package/dist/adapters/node/list-entries.io.js +3 -1
- package/dist/adapters/node/list-entries.io.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +20 -8
- package/dist/cli.js.map +1 -1
- package/dist/cycle-rules.d.ts.map +1 -1
- package/dist/cycle-rules.js +3 -4
- package/dist/cycle-rules.js.map +1 -1
- package/dist/domain/deadline.d.ts +8 -1
- package/dist/domain/deadline.d.ts.map +1 -1
- package/dist/domain/deadline.js +26 -12
- package/dist/domain/deadline.js.map +1 -1
- package/dist/domain/decide-first.d.ts +11 -1
- package/dist/domain/decide-first.d.ts.map +1 -1
- package/dist/domain/decide-first.js +6 -0
- package/dist/domain/decide-first.js.map +1 -1
- package/dist/domain/index-columns.d.ts.map +1 -1
- package/dist/domain/index-columns.js +2 -0
- package/dist/domain/index-columns.js.map +1 -1
- package/dist/domain/portal-token.d.ts +44 -0
- package/dist/domain/portal-token.d.ts.map +1 -0
- package/dist/domain/portal-token.js +32 -0
- package/dist/domain/portal-token.js.map +1 -0
- package/dist/domain/study-registry.d.ts +65 -8
- package/dist/domain/study-registry.d.ts.map +1 -1
- package/dist/domain/study-registry.js +63 -19
- package/dist/domain/study-registry.js.map +1 -1
- package/dist/domain/submission.d.ts +30 -0
- package/dist/domain/submission.d.ts.map +1 -1
- package/dist/domain/submission.js +68 -0
- package/dist/domain/submission.js.map +1 -1
- package/dist/lib/paper-config.d.ts +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +6 -4
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/outputs-before-decision-rule.d.ts +1 -1
- package/dist/outputs-before-decision-rule.d.ts.map +1 -1
- package/dist/outputs-before-decision-rule.js +21 -17
- package/dist/outputs-before-decision-rule.js.map +1 -1
- package/dist/paper-settings.d.ts +3 -1
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +11 -5
- package/dist/paper-settings.js.map +1 -1
- package/dist/ports/latex.d.ts +23 -0
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/ports/list-entries.d.ts +7 -4
- package/dist/ports/list-entries.d.ts.map +1 -1
- package/dist/ports/study-deps.d.ts +14 -0
- package/dist/ports/study-deps.d.ts.map +1 -0
- package/dist/ports/study-deps.js +2 -0
- package/dist/ports/study-deps.js.map +1 -0
- package/dist/ports/study-folder.d.ts +43 -0
- package/dist/ports/study-folder.d.ts.map +1 -0
- package/dist/ports/study-folder.js +2 -0
- package/dist/ports/study-folder.js.map +1 -0
- package/dist/ports/submission-portal.d.ts +39 -2
- package/dist/ports/submission-portal.d.ts.map +1 -1
- package/dist/ports/venue-deadlines.d.ts +1 -1
- package/dist/presets.d.ts +1 -2
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +1 -2
- package/dist/presets.js.map +1 -1
- package/dist/studies.d.ts.map +1 -1
- package/dist/studies.js +10 -7
- package/dist/studies.js.map +1 -1
- package/dist/study-folder.d.ts +14 -0
- package/dist/study-folder.d.ts.map +1 -0
- package/dist/study-folder.js +123 -0
- package/dist/study-folder.js.map +1 -0
- package/dist/study-gate.d.ts +1 -1
- package/dist/study-gate.d.ts.map +1 -1
- package/dist/study-gate.js +65 -28
- package/dist/study-gate.js.map +1 -1
- package/dist/study-index-input.d.ts +1 -1
- package/dist/study-index-input.d.ts.map +1 -1
- package/dist/study-index-input.js +8 -3
- package/dist/study-index-input.js.map +1 -1
- package/dist/study-modes.d.ts +38 -27
- package/dist/study-modes.d.ts.map +1 -1
- package/dist/study-modes.js +42 -53
- package/dist/study-modes.js.map +1 -1
- package/dist/study-registry-rule.d.ts +2 -6
- package/dist/study-registry-rule.d.ts.map +1 -1
- package/dist/study-registry-rule.js +32 -41
- package/dist/study-registry-rule.js.map +1 -1
- package/dist/submission.d.ts +49 -11
- package/dist/submission.d.ts.map +1 -1
- package/dist/submission.js +332 -44
- package/dist/submission.js.map +1 -1
- package/docs/configuration.md +2 -1
- package/docs/design/study-modes.md +46 -28
- package/docs/rules/cycle/record.md +9 -6
- package/docs/rules/repro/outputs-before-decision.md +3 -0
- package/docs/rules/repro/study-registered.md +3 -2
- package/docs/rules/tex/heading-case.md +1 -1
- package/docs/submission.md +47 -48
- package/eslint.config.mjs +39 -24
- package/lib/paper-config.ts +7 -4
- package/package.json +6 -1
- package/presets/msr.jsonc +9 -7
- package/presets/msr.md +14 -10
- package/scripts/check.ts +1 -1
- package/scripts/refresh-deadlines/compare.test.ts +75 -0
- package/scripts/refresh-deadlines/compare.ts +101 -0
- package/scripts/refresh-deadlines/main.test.ts +140 -0
- package/scripts/refresh-deadlines/main.ts +56 -0
- package/scripts/refresh-deadlines/refresh.test.ts +287 -0
- package/scripts/refresh-deadlines/refresh.ts +183 -0
- package/skills/grade-paper-writing/SKILL.md +4 -2
- package/skills/grade-paper-writing/SKILL.md.spec.ts +3 -1
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +16 -3
- package/skills/grade-paper-writing/prose-lint.mjs +127 -95
- package/skills/grade-paper-writing/prose-lint.test.ts +119 -10
- package/skills/submission-portal/SKILL.md +60 -44
- package/skills/submission-portal/SKILL.md.spec.ts +62 -43
- package/skills/submission-portal/submission-portal.test.ts +4 -73
- package/src/CLAUDE.md +7 -4
- package/src/CLAUDE.md.spec.ts +6 -3
- package/src/adapters/hotcrp/cassette/README.md +4 -4
- package/src/adapters/hotcrp/cassette/deadlines-msr2027.html +9 -9
- package/src/adapters/hotcrp/deadlines.io.test.ts +2 -2
- package/src/adapters/hotcrp/deadlines.test.ts +3 -3
- package/src/adapters/hotcrp/index.ts +15 -3
- package/src/adapters/hotcrp/portal.io.ts +61 -17
- package/src/adapters/hotcrp/portal.test.ts +105 -1
- package/src/adapters/hotcrp/response.ts +21 -0
- package/src/adapters/hotcrp/site.ts +34 -0
- package/src/adapters/hotcrp/token.test.ts +69 -0
- package/src/adapters/hotcrp/token.ts +51 -0
- package/src/adapters/latex/abstract.test.ts +280 -0
- package/src/adapters/latex/abstract.ts +272 -0
- package/src/adapters/latex/captions.test.ts +57 -0
- package/src/adapters/latex/captions.ts +56 -0
- package/src/adapters/latex/headings.ts +5 -4
- package/src/adapters/latex/index.test.ts +31 -1
- package/src/adapters/latex/index.ts +3 -0
- package/src/adapters/latex/parse.ts +71 -3
- package/src/adapters/node/list-entries.io.ts +2 -1
- package/src/adapters/node/list-entries.test.ts +18 -0
- package/src/cli.test.ts +20 -8
- package/src/cli.ts +22 -13
- package/src/cycle-rules.test.ts +6 -6
- package/src/cycle-rules.ts +7 -4
- package/src/domain/deadline.test.ts +31 -2
- package/src/domain/deadline.ts +42 -16
- package/src/domain/decide-first.test.ts +33 -0
- package/src/domain/decide-first.ts +16 -1
- package/src/domain/index-columns.test.ts +49 -4
- package/src/domain/index-columns.ts +2 -0
- package/src/domain/portal-token.test.ts +55 -0
- package/src/domain/portal-token.ts +63 -0
- package/src/domain/study-measurements.test.ts +7 -3
- package/src/domain/study-registry.test.ts +34 -6
- package/src/domain/study-registry.ts +123 -31
- package/src/domain/submission.test.ts +88 -0
- package/src/domain/submission.ts +99 -0
- package/src/new-paper.test.ts +1 -0
- package/src/outputs-before-decision-rule.test.ts +47 -2
- package/src/outputs-before-decision-rule.ts +31 -22
- package/src/paper-settings.test.ts +20 -0
- package/src/paper-settings.ts +17 -5
- package/src/ports/latex.ts +25 -0
- package/src/ports/list-entries.ts +7 -4
- package/src/ports/study-deps.ts +14 -0
- package/src/ports/study-folder.ts +38 -0
- package/src/ports/submission-portal.ts +43 -1
- package/src/ports/venue-deadlines.ts +1 -1
- package/src/presets.test.ts +8 -5
- package/src/presets.ts +1 -2
- package/src/studies-index-modes.test.ts +2 -2
- package/src/studies.ts +19 -10
- package/src/study-folder.test.ts +237 -0
- package/src/study-folder.ts +176 -0
- package/src/study-gate.test.ts +158 -7
- package/src/study-gate.ts +127 -54
- package/src/study-index-input.ts +12 -4
- package/src/study-modes.ts +67 -80
- package/src/study-registry-rule.test.ts +87 -0
- package/src/study-registry-rule.ts +58 -66
- package/src/submission-new.test.ts +569 -0
- package/src/submission-show.test.ts +210 -0
- package/src/submission-update.test.ts +160 -0
- package/src/submission.test.ts +140 -518
- package/src/submission.ts +494 -61
- package/scripts/refresh-deadlines.test.ts +0 -431
- package/scripts/refresh-deadlines.ts +0 -387
package/presets/msr.md
CHANGED
|
@@ -23,16 +23,20 @@ whereas last year it was ACM format.»), and limits and dates change between edi
|
|
|
23
23
|
2027-01-08; camera-ready 2027-01-26. The call's prose names the weekdays «Mon, 20 Oct 2026» and
|
|
24
24
|
«Thurs, 23 Oct 2026», its date table «Tue 20 Oct 2026» and «Fri 23 Oct 2026»; the calendar sides
|
|
25
25
|
with the table (2026-10-20 is a Tuesday).
|
|
26
|
-
- 🔴 **The portal
|
|
27
|
-
`https://msr2027.hotcrp.com/deadlines`: «Submission deadline:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
day
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
`
|
|
26
|
+
- 🔴 **The portal's submission deadline is the call's ABSTRACT day, not its paper day.** Read
|
|
27
|
+
2026-10-19 18:05 UTC on `https://msr2027.hotcrp.com/deadlines`: «Submission deadline: Wednesday
|
|
28
|
+
Oct 21, 2026, 7:59:59 AM EDT — Submissions must be completed by this deadline to be reviewed»
|
|
29
|
+
(`"sub":1792583999`, 2026-10-21 11:59:59 UTC, the end of 2026-10-20 AoE) and «Resubmission
|
|
30
|
+
deadline: Saturday Oct 24, 2026, 7:59:59 AM EDT — Completed submissions may be updated until this
|
|
31
|
+
deadline» (2026-10-24 11:59:59 UTC, the end of 2026-10-23 AoE). So a submission must be complete
|
|
32
|
+
by the abstract day, and the paper day is the last day to update it. The reading of 2026-10-05
|
|
33
|
+
was earlier on both («Tuesday Oct 20, 2026, 12 AM EDT», `"sub":1792468800`, and «Friday Oct 23,
|
|
34
|
+
2026, 12 AM EDT»): the portal moved them. The portal is what refuses the upload: plan for its
|
|
35
|
+
times. The preset carries them as `deadlines` (`source: "portal"`, with the day read), refreshed
|
|
36
|
+
by paperlint's scheduled workflow when the portal changes them; a paper whose current cycle names
|
|
37
|
+
`paperlint:msr` derives them. Recording the call's days in the paper's own cycle maps «abstract»
|
|
38
|
+
to `registration` and «paper» to `submission` — and the portal's `submission`, at the end of the
|
|
39
|
+
abstract day, is then the one in force.
|
|
36
40
|
- **Format**, verbatim from the call: «LaTeX users must use \documentclass[10pt,conference]{IEEEtran}
|
|
37
41
|
without including the compsoc or compsocconf options», «title in 24pt font and full text in 10pt
|
|
38
42
|
type», and «Alterations of spacing, font size, and other changes that deviate from the
|
package/scripts/check.ts
CHANGED
|
@@ -213,7 +213,7 @@ export const NOT_COVERED: Readonly<Record<string, string>> = {
|
|
|
213
213
|
refresh:
|
|
214
214
|
"deadlines.yml reads the venue presets' portals over the network once a day and opens a pull " +
|
|
215
215
|
"request when a deadline moved; npm test replays the recorded portal pages through " +
|
|
216
|
-
"scripts/refresh-deadlines.test.ts, and nothing local should read the live portals on every check.",
|
|
216
|
+
"scripts/refresh-deadlines/main.test.ts, and nothing local should read the live portals on every check.",
|
|
217
217
|
};
|
|
218
218
|
|
|
219
219
|
/**
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** A preset's portal readings against the page: kept, rewritten, added, noted — a call's never touched. */
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import { refreshReadings } from "./compare.ts";
|
|
4
|
+
|
|
5
|
+
const reading = (
|
|
6
|
+
what: "submission" | "resubmission" | "camera-ready",
|
|
7
|
+
at: string,
|
|
8
|
+
source: "portal" | "call" = "portal",
|
|
9
|
+
) => ({
|
|
10
|
+
what,
|
|
11
|
+
at,
|
|
12
|
+
source,
|
|
13
|
+
url: "https://x.example/deadlines",
|
|
14
|
+
read: "2026-10-01",
|
|
15
|
+
});
|
|
16
|
+
const page = (
|
|
17
|
+
deadlines: readonly {
|
|
18
|
+
what: "submission" | "camera-ready" | null;
|
|
19
|
+
at: string;
|
|
20
|
+
}[],
|
|
21
|
+
) => ({
|
|
22
|
+
now: 1,
|
|
23
|
+
open: true,
|
|
24
|
+
blind: true,
|
|
25
|
+
deadlines: deadlines.map((d) => ({ ...d, label: String(d.what) })),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("refreshReadings — what the page says, over what the preset said", () => {
|
|
29
|
+
test("a reading the portal no longer lists is kept and noted; a call's reading is never touched", () => {
|
|
30
|
+
const r = refreshReadings(
|
|
31
|
+
[
|
|
32
|
+
reading("submission", "2026-10-21T11:59:59Z"),
|
|
33
|
+
reading("submission", "2026-10-21T11:59:59Z", "call"),
|
|
34
|
+
reading("camera-ready", "2026-12-01T00:00:00Z"),
|
|
35
|
+
],
|
|
36
|
+
page([{ what: "submission", at: "2026-10-21T11:59:59Z" }]),
|
|
37
|
+
"https://x.example/deadlines",
|
|
38
|
+
"2026-10-06",
|
|
39
|
+
);
|
|
40
|
+
expect(r.changes).toEqual([]);
|
|
41
|
+
expect(r.notes).toEqual([
|
|
42
|
+
"camera-ready: no longer on the page — kept as read 2026-10-01",
|
|
43
|
+
]);
|
|
44
|
+
expect(r.readings).toEqual([
|
|
45
|
+
reading("submission", "2026-10-21T11:59:59Z"),
|
|
46
|
+
reading("submission", "2026-10-21T11:59:59Z", "call"),
|
|
47
|
+
reading("camera-ready", "2026-12-01T00:00:00Z"),
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("a kind the page lists twice binds at the earlier instant; a label paperlint names no kind for is left out", () => {
|
|
52
|
+
const r = refreshReadings(
|
|
53
|
+
[],
|
|
54
|
+
page([
|
|
55
|
+
{ what: "submission", at: "2026-10-22T04:00:00Z" },
|
|
56
|
+
{ what: "submission", at: "2026-10-21T11:59:59Z" },
|
|
57
|
+
{ what: null, at: "2026-09-01T00:00:00Z" },
|
|
58
|
+
]),
|
|
59
|
+
"https://x.example/deadlines",
|
|
60
|
+
"2026-10-06",
|
|
61
|
+
);
|
|
62
|
+
expect(r.readings).toEqual([
|
|
63
|
+
{
|
|
64
|
+
what: "submission",
|
|
65
|
+
at: "2026-10-21T11:59:59Z",
|
|
66
|
+
source: "portal",
|
|
67
|
+
url: "https://x.example/deadlines",
|
|
68
|
+
read: "2026-10-06",
|
|
69
|
+
},
|
|
70
|
+
]);
|
|
71
|
+
expect(r.notes).toEqual([
|
|
72
|
+
"submission: the page lists 2026-10-21T11:59:59Z, 2026-10-22T04:00:00Z — the earliest is recorded",
|
|
73
|
+
]);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A PRESET'S PORTAL READINGS AGAINST WHAT THE PORTAL'S PAGE SAYS NOW — the comparison the scheduled
|
|
3
|
+
* refresh of the shipped presets makes (`main.ts` beside this file). Pure.
|
|
4
|
+
*
|
|
5
|
+
* a kind the page lists at the instant the preset holds kept as it is, its `read` day too
|
|
6
|
+
* a kind the page lists at another instant rewritten, `read` = today
|
|
7
|
+
* a kind the page lists that the preset does not hold added, `read` = today
|
|
8
|
+
* a kind the preset holds that the page no longer lists KEPT and noted — a portal may drop a
|
|
9
|
+
* passed deadline; the reading stays true
|
|
10
|
+
* a reading from the call never touched: it is not the portal's
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
DEADLINE_WHATS,
|
|
14
|
+
type DeadlineWhat,
|
|
15
|
+
type IsoDate,
|
|
16
|
+
type PortalDeadlines,
|
|
17
|
+
type Reading,
|
|
18
|
+
} from "#src/domain/deadline";
|
|
19
|
+
|
|
20
|
+
/** The preset's readings after the page's answer, what changed, and what is worth a note. */
|
|
21
|
+
export interface Refreshed {
|
|
22
|
+
readonly readings: readonly Reading[];
|
|
23
|
+
readonly changes: readonly string[];
|
|
24
|
+
readonly notes: readonly string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The page's instants for one kind, earliest first. */
|
|
28
|
+
const pageAts = (
|
|
29
|
+
page: PortalDeadlines,
|
|
30
|
+
what: DeadlineWhat,
|
|
31
|
+
): readonly string[] =>
|
|
32
|
+
page.deadlines
|
|
33
|
+
.filter((d) => d.what === what)
|
|
34
|
+
.map((d) => d.at)
|
|
35
|
+
.toSorted();
|
|
36
|
+
|
|
37
|
+
/** A kind the page does not list: the preset's reading, if it holds one, is kept and noted. */
|
|
38
|
+
const offThePage = (
|
|
39
|
+
what: DeadlineWhat,
|
|
40
|
+
held: Reading | undefined,
|
|
41
|
+
): Refreshed =>
|
|
42
|
+
held === undefined
|
|
43
|
+
? { readings: [], changes: [], notes: [] }
|
|
44
|
+
: {
|
|
45
|
+
readings: [held],
|
|
46
|
+
changes: [],
|
|
47
|
+
notes: [`${what}: no longer on the page — kept as read ${held.read}`],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** One kind: the preset's portal reading against the page's. */
|
|
51
|
+
function oneKind(
|
|
52
|
+
what: DeadlineWhat,
|
|
53
|
+
held: Reading | undefined,
|
|
54
|
+
ats: readonly string[],
|
|
55
|
+
o: { readonly url: string; readonly today: IsoDate },
|
|
56
|
+
): Refreshed {
|
|
57
|
+
const notes =
|
|
58
|
+
ats.length > 1
|
|
59
|
+
? [`${what}: the page lists ${ats.join(", ")} — the earliest is recorded`]
|
|
60
|
+
: [];
|
|
61
|
+
const at = ats[0];
|
|
62
|
+
if (at === undefined) return offThePage(what, held);
|
|
63
|
+
if (held?.at === at) return { readings: [held], changes: [], notes };
|
|
64
|
+
const fresh: Reading = {
|
|
65
|
+
what,
|
|
66
|
+
at,
|
|
67
|
+
source: "portal",
|
|
68
|
+
url: o.url,
|
|
69
|
+
read: o.today,
|
|
70
|
+
};
|
|
71
|
+
const change =
|
|
72
|
+
held === undefined ? `${what}: added ${at}` : `${what}: ${held.at} → ${at}`;
|
|
73
|
+
return { readings: [fresh], changes: [change], notes };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The preset's readings after the portal answered `page` at `url` on `today`. The call's readings
|
|
78
|
+
* are kept as they are; the portal's are kept, rewritten or added kind by kind. Pure.
|
|
79
|
+
*/
|
|
80
|
+
export function refreshReadings(
|
|
81
|
+
current: readonly Reading[],
|
|
82
|
+
page: PortalDeadlines,
|
|
83
|
+
url: string,
|
|
84
|
+
today: IsoDate,
|
|
85
|
+
): Refreshed {
|
|
86
|
+
const perKind = DEADLINE_WHATS.map((what) => {
|
|
87
|
+
const portal = oneKind(
|
|
88
|
+
what,
|
|
89
|
+
current.find((r) => r.what === what && r.source === "portal"),
|
|
90
|
+
pageAts(page, what),
|
|
91
|
+
{ url, today },
|
|
92
|
+
);
|
|
93
|
+
const call = current.filter((r) => r.what === what && r.source === "call");
|
|
94
|
+
return { ...portal, readings: [...portal.readings, ...call] };
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
readings: perKind.flatMap((k) => k.readings),
|
|
98
|
+
changes: perKind.flatMap((k) => k.changes),
|
|
99
|
+
notes: perKind.flatMap((k) => k.notes),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entry of the scheduled refresh: the npm script the workflow runs, end to end, and the
|
|
3
|
+
* command line and files main.ts hands the logic.
|
|
4
|
+
*/
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import {
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
realpathSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import {
|
|
13
|
+
createServer,
|
|
14
|
+
type IncomingMessage,
|
|
15
|
+
type ServerResponse,
|
|
16
|
+
} from "node:http";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { promisify } from "node:util";
|
|
21
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
22
|
+
import { parsePreset } from "#src/tex-requirements";
|
|
23
|
+
import { argsOf, presetsIn, summaryTo } from "./main.ts";
|
|
24
|
+
|
|
25
|
+
const ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
26
|
+
const PRESETS = join(ROOT, "presets");
|
|
27
|
+
const CASSETTE = join(ROOT, "src", "adapters", "hotcrp", "cassette");
|
|
28
|
+
const MSR = join(PRESETS, "msr.jsonc");
|
|
29
|
+
const msrText = readFileSync(MSR, "utf8");
|
|
30
|
+
const readings = (text: string) => parsePreset(text, MSR).deadlines ?? [];
|
|
31
|
+
/** MSR with its submission deadline two days before the portal's. */
|
|
32
|
+
const earlier = msrText.replace(
|
|
33
|
+
'"at": "2026-10-21T11:59:59Z"',
|
|
34
|
+
'"at": "2026-10-19T04:00:00Z"',
|
|
35
|
+
);
|
|
36
|
+
const tempDir = (prefix: string) =>
|
|
37
|
+
realpathSync(mkdtempSync(join(tmpdir(), prefix)));
|
|
38
|
+
|
|
39
|
+
const MSR_HTML = readFileSync(join(CASSETTE, "deadlines-msr2027.html"), "utf8");
|
|
40
|
+
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
41
|
+
const ok = req.url === "/msr/deadlines";
|
|
42
|
+
res.writeHead(ok ? 200 : 404, { "content-type": "text/html" });
|
|
43
|
+
res.end(ok ? MSR_HTML : '<h1 id="h-title">No such conference</h1>');
|
|
44
|
+
});
|
|
45
|
+
const base = { url: "" };
|
|
46
|
+
beforeAll(async () => {
|
|
47
|
+
await new Promise<void>((r) => server.listen(0, "127.0.0.1", r));
|
|
48
|
+
const a = server.address();
|
|
49
|
+
base.url = `http://127.0.0.1:${String(typeof a === "object" && a !== null ? a.port : 0)}`;
|
|
50
|
+
});
|
|
51
|
+
afterAll(
|
|
52
|
+
() =>
|
|
53
|
+
new Promise<void>((r) =>
|
|
54
|
+
server.close(() => {
|
|
55
|
+
r();
|
|
56
|
+
}),
|
|
57
|
+
),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// The one end-to-end test: main.ts's isMain block wires the real HotCRP adapter, run against the cassette.
|
|
61
|
+
describe("`npm run refresh-deadlines`, the command the workflow runs — with no dist/", () => {
|
|
62
|
+
// A real `node` process: Node strips the types of the script and everything it imports cold —
|
|
63
|
+
// about a second alone, several on a loaded machine, past vitest's 5 s.
|
|
64
|
+
test(
|
|
65
|
+
"rewrites the moved deadline, reports the site it could not read, writes the summary — and exits 0",
|
|
66
|
+
{ timeout: 60_000 },
|
|
67
|
+
async () => {
|
|
68
|
+
const dir = tempDir("deadlines-");
|
|
69
|
+
const atServer = (text: string, path: string) =>
|
|
70
|
+
text.split("https://msr2027.hotcrp.com").join(`${base.url}/${path}`);
|
|
71
|
+
writeFileSync(join(dir, "msr.jsonc"), atServer(earlier, "msr"));
|
|
72
|
+
writeFileSync(join(dir, "gone.jsonc"), atServer(msrText, "gone"));
|
|
73
|
+
writeFileSync(join(dir, "notes.md"), "not a preset\n");
|
|
74
|
+
const summary = join(dir, "summary.md");
|
|
75
|
+
// Asynchronously: the local server answers from THIS process, which a synchronous spawn blocks.
|
|
76
|
+
// The inherited NODE_OPTIONS are replaced by test/no-dist.ts, which refuses any module in
|
|
77
|
+
// `dist/` — the job installs with --ignore-scripts.
|
|
78
|
+
const r = await promisify(execFile)(
|
|
79
|
+
"npm",
|
|
80
|
+
[
|
|
81
|
+
"run",
|
|
82
|
+
"-s",
|
|
83
|
+
"refresh-deadlines",
|
|
84
|
+
"--",
|
|
85
|
+
"--presets",
|
|
86
|
+
dir,
|
|
87
|
+
"--summary",
|
|
88
|
+
summary,
|
|
89
|
+
],
|
|
90
|
+
{
|
|
91
|
+
env: {
|
|
92
|
+
...process.env,
|
|
93
|
+
NODE_OPTIONS: `--import ${join(ROOT, "test", "no-dist.ts")}`,
|
|
94
|
+
},
|
|
95
|
+
cwd: ROOT,
|
|
96
|
+
},
|
|
97
|
+
); // rejects on a non-zero exit, with the output
|
|
98
|
+
expect(
|
|
99
|
+
readings(readFileSync(join(dir, "msr.jsonc"), "utf8"))[0]?.at,
|
|
100
|
+
).toBe("2026-10-21T11:59:59Z");
|
|
101
|
+
expect(r.stdout).toMatch(
|
|
102
|
+
/^::warning title=deadlines::gone\.jsonc: could not read http:\/\/127\.0\.0\.1:\d+\/gone\/deadlines — the portal refused \(HTTP 404\): No such conference$/m,
|
|
103
|
+
);
|
|
104
|
+
expect(readFileSync(summary, "utf8")).toMatch(
|
|
105
|
+
/^- ⚠️ `gone\.jsonc`: could not read /m,
|
|
106
|
+
);
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("argsOf: the repository's presets and no summary by default; a flag without a value is no value", () => {
|
|
112
|
+
expect(argsOf([])).toEqual({ presets: PRESETS, summary: null });
|
|
113
|
+
expect(argsOf(["--presets", "/p", "--summary"])).toEqual({
|
|
114
|
+
presets: "/p",
|
|
115
|
+
summary: null,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("presetsIn: the *.jsonc files of a directory, sorted, read and written there; summaryTo: a file, or nowhere", () => {
|
|
120
|
+
const dir = tempDir("presets-");
|
|
121
|
+
writeFileSync(join(dir, "b.jsonc"), "B");
|
|
122
|
+
writeFileSync(join(dir, "a.jsonc"), "A");
|
|
123
|
+
writeFileSync(join(dir, "notes.md"), "not a preset");
|
|
124
|
+
const p = presetsIn(dir);
|
|
125
|
+
p.write("a.jsonc", "A2");
|
|
126
|
+
summaryTo(join(dir, "s.md"))?.("body\n");
|
|
127
|
+
expect([
|
|
128
|
+
p.names,
|
|
129
|
+
p.read("a.jsonc"),
|
|
130
|
+
p.path("b.jsonc"),
|
|
131
|
+
readFileSync(join(dir, "s.md"), "utf8"),
|
|
132
|
+
summaryTo(null),
|
|
133
|
+
]).toStrictEqual([
|
|
134
|
+
["a.jsonc", "b.jsonc"],
|
|
135
|
+
"A2",
|
|
136
|
+
join(dir, "b.jsonc"),
|
|
137
|
+
"body\n",
|
|
138
|
+
null,
|
|
139
|
+
]);
|
|
140
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `npm run refresh-deadlines [-- --presets <dir>] [-- --summary <file.md>]`, which
|
|
4
|
+
* .github/workflows/deadlines.yml runs from source (no `dist/`). The composition root: the command
|
|
5
|
+
* line, the files, the clock, and HotCRP's adapter plugged into the port `refresh.ts` reads.
|
|
6
|
+
*/
|
|
7
|
+
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { hotcrpDeadlines } from "#src/adapters/hotcrp/index";
|
|
10
|
+
import { presetsDir } from "#src/package-dirs";
|
|
11
|
+
import { isMain } from "../../skills/paper-pipeline/scripts/consumer.mjs";
|
|
12
|
+
import { runRefresh, type PresetFiles } from "./refresh.ts";
|
|
13
|
+
|
|
14
|
+
/** `--presets <dir>` and `--summary <file>`; a flag without a value is no value. */
|
|
15
|
+
export function argsOf(argv: readonly string[]) {
|
|
16
|
+
const value = (flag: string): string | null =>
|
|
17
|
+
argv.includes(flag) ? (argv[argv.indexOf(flag) + 1] ?? null) : null;
|
|
18
|
+
return {
|
|
19
|
+
presets: value("--presets") ?? presetsDir(),
|
|
20
|
+
summary: value("--summary"),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The `*.jsonc` presets in `dir`, on disk. */
|
|
25
|
+
export const presetsIn = (dir: string): PresetFiles => ({
|
|
26
|
+
names: readdirSync(dir)
|
|
27
|
+
.filter((f) => f.endsWith(".jsonc"))
|
|
28
|
+
.toSorted(),
|
|
29
|
+
read: (name) => readFileSync(join(dir, name), "utf8"),
|
|
30
|
+
write: (name, text) => {
|
|
31
|
+
writeFileSync(join(dir, name), text);
|
|
32
|
+
},
|
|
33
|
+
path: (name) => join(dir, name),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/** Where the pull request's body is written: the `--summary` file, or nowhere. */
|
|
37
|
+
export const summaryTo = (path: string | null) =>
|
|
38
|
+
path === null
|
|
39
|
+
? null
|
|
40
|
+
: (text: string) => {
|
|
41
|
+
writeFileSync(path, text);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// `isMain`, not a comparison with `file://${argv[1]}`: that is false through a symlink (consumer.mjs).
|
|
45
|
+
if (isMain(import.meta.url)) {
|
|
46
|
+
const args = argsOf(process.argv.slice(2));
|
|
47
|
+
process.exit(
|
|
48
|
+
await runRefresh({
|
|
49
|
+
presets: presetsIn(args.presets),
|
|
50
|
+
portal: (site) => hotcrpDeadlines({ url: site }),
|
|
51
|
+
today: new Date().toISOString().slice(0, 10), // the clock, read at the edge
|
|
52
|
+
log: console.log,
|
|
53
|
+
summary: summaryTo(args.summary),
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
}
|