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.
- package/.github/workflows/ci.yml +3 -0
- package/.github/workflows/deadlines.yml +100 -0
- package/README.md +6 -0
- package/dist/adapters/hotcrp/deadlines.d.ts +8 -0
- package/dist/adapters/hotcrp/deadlines.d.ts.map +1 -0
- package/dist/adapters/hotcrp/deadlines.io.d.ts +6 -0
- package/dist/adapters/hotcrp/deadlines.io.d.ts.map +1 -0
- package/dist/adapters/hotcrp/deadlines.io.js +26 -0
- package/dist/adapters/hotcrp/deadlines.io.js.map +1 -0
- package/dist/adapters/hotcrp/deadlines.js +134 -0
- package/dist/adapters/hotcrp/deadlines.js.map +1 -0
- package/dist/adapters/hotcrp/index.d.ts +2 -0
- package/dist/adapters/hotcrp/index.d.ts.map +1 -1
- package/dist/adapters/hotcrp/index.js +2 -0
- package/dist/adapters/hotcrp/index.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +40 -5
- package/dist/cli.js.map +1 -1
- package/dist/cycle-rules.d.ts +24 -0
- package/dist/cycle-rules.d.ts.map +1 -0
- package/dist/cycle-rules.js +212 -0
- package/dist/cycle-rules.js.map +1 -0
- package/dist/domain/cycle.d.ts +148 -0
- package/dist/domain/cycle.d.ts.map +1 -0
- package/dist/domain/cycle.js +281 -0
- package/dist/domain/cycle.js.map +1 -0
- package/dist/domain/deadline.d.ts +128 -0
- package/dist/domain/deadline.d.ts.map +1 -0
- package/dist/domain/deadline.js +281 -0
- package/dist/domain/deadline.js.map +1 -0
- package/dist/eslint-rules/cycle-stage.d.ts +39 -0
- package/dist/eslint-rules/cycle-stage.d.ts.map +1 -0
- package/dist/eslint-rules/cycle-stage.js +119 -0
- package/dist/eslint-rules/cycle-stage.js.map +1 -0
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +2 -0
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/paper-settings.d.ts +7 -0
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +61 -8
- package/dist/paper-settings.js.map +1 -1
- package/dist/ports/venue-deadlines.d.ts +14 -0
- package/dist/ports/venue-deadlines.d.ts.map +1 -0
- package/dist/ports/venue-deadlines.js +2 -0
- package/dist/ports/venue-deadlines.js.map +1 -0
- package/dist/presets.d.ts +12 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +19 -11
- package/dist/presets.js.map +1 -1
- package/dist/submission.d.ts +8 -0
- package/dist/submission.d.ts.map +1 -1
- package/dist/submission.js +41 -10
- package/dist/submission.js.map +1 -1
- package/dist/tex-requirements.d.ts +3 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +19 -0
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +19 -0
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +69 -0
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/venue-rules.d.ts +10 -0
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +26 -10
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +139 -11
- package/docs/rules/cycle/evidence.md +87 -0
- package/docs/rules/cycle/port-done.md +75 -0
- package/docs/rules/cycle/record.md +152 -0
- package/docs/rules/cycle/stage.md +85 -0
- package/docs/rules.md +48 -43
- package/docs/submission.md +5 -0
- package/eslint-rules/cycle-stage.test.mjs +131 -0
- package/eslint-rules/cycle-stage.ts +141 -0
- package/eslint.config.mjs +41 -1
- package/lib/paper-config.ts +3 -0
- package/package.json +1 -1
- package/presets/aidc.jsonc +24 -3
- package/presets/aidc.md +6 -3
- package/presets/msr.jsonc +25 -1
- package/presets/msr.md +5 -1
- package/presets/venue-profile.schema.json +39 -0
- package/scripts/check.ts +4 -0
- package/scripts/refresh-deadlines.test.ts +431 -0
- package/scripts/refresh-deadlines.ts +387 -0
- package/src/adapters/hotcrp/cassette/README.md +22 -0
- package/src/adapters/hotcrp/cassette/deadlines-aidc26.html +17 -0
- package/src/adapters/hotcrp/cassette/deadlines-fse2027.html +24 -0
- package/src/adapters/hotcrp/cassette/deadlines-icse2027.html +25 -0
- package/src/adapters/hotcrp/cassette/deadlines-msr2027.html +25 -0
- package/src/adapters/hotcrp/cassette/deadlines-nosuch.html +18 -0
- package/src/adapters/hotcrp/cassette/deadlines-sosp26.html +26 -0
- package/src/adapters/hotcrp/deadlines.io.test.ts +86 -0
- package/src/adapters/hotcrp/deadlines.io.ts +27 -0
- package/src/adapters/hotcrp/deadlines.test.ts +170 -0
- package/src/adapters/hotcrp/deadlines.ts +161 -0
- package/src/adapters/hotcrp/index.ts +2 -0
- package/src/cli.test.ts +9 -0
- package/src/cli.ts +43 -4
- package/src/cycle-rules.test.ts +278 -0
- package/src/cycle-rules.ts +273 -0
- package/src/domain/cycle.test.ts +456 -0
- package/src/domain/cycle.ts +432 -0
- package/src/domain/deadline.test.ts +386 -0
- package/src/domain/deadline.ts +472 -0
- package/src/new-paper.test.ts +1 -0
- package/src/paper-settings.test.ts +167 -0
- package/src/paper-settings.ts +85 -11
- package/src/ports/venue-deadlines.ts +14 -0
- package/src/presets.test.ts +105 -0
- package/src/presets.ts +43 -10
- package/src/rule-cases.test.ts +144 -0
- package/src/submission.test.ts +105 -1
- package/src/submission.ts +73 -12
- package/src/tex-requirements.ts +31 -0
- package/src/tex-venue-rules.test.ts +76 -0
- package/src/tex-venue-rules.ts +85 -0
- package/src/venue-rules.test.ts +82 -0
- package/src/venue-rules.ts +44 -12
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE CYCLE — one attempt of a paper at a venue, parsed from `cycles` in `paperlint.json`, and the
|
|
3
|
+
* current attempt derived from the list. Pure.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { present } from "../../test/support.ts";
|
|
7
|
+
import {
|
|
8
|
+
currentCycle,
|
|
9
|
+
currentDeadlines,
|
|
10
|
+
cycleProblemText,
|
|
11
|
+
effectiveBlind,
|
|
12
|
+
parseCycles,
|
|
13
|
+
type Cycle,
|
|
14
|
+
} from "./cycle.ts";
|
|
15
|
+
|
|
16
|
+
const REJECTED = {
|
|
17
|
+
id: "alpha-2026",
|
|
18
|
+
venue: { kind: "preset", extends: "paperlint:acm-sigconf" },
|
|
19
|
+
kind: "short",
|
|
20
|
+
opened: "2026-07-01",
|
|
21
|
+
deadlines: [
|
|
22
|
+
{
|
|
23
|
+
what: "submission",
|
|
24
|
+
observed: [
|
|
25
|
+
{
|
|
26
|
+
at: "2026-08-06T11:59:00Z",
|
|
27
|
+
source: "call",
|
|
28
|
+
url: "https://example.org/cfp",
|
|
29
|
+
read: "2026-07-01",
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
outcome: {
|
|
35
|
+
kind: "rejected",
|
|
36
|
+
date: "2026-09-08",
|
|
37
|
+
desk: false,
|
|
38
|
+
evidence: "reviews/alpha.md",
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
const OPEN = {
|
|
42
|
+
id: "beta-2027",
|
|
43
|
+
venue: { kind: "preset", extends: "paperlint:ieee-conference" },
|
|
44
|
+
kind: "technical",
|
|
45
|
+
opened: "2026-09-09",
|
|
46
|
+
phase: "porting",
|
|
47
|
+
deadlines: [
|
|
48
|
+
{
|
|
49
|
+
what: "submission",
|
|
50
|
+
observed: [
|
|
51
|
+
{
|
|
52
|
+
at: "2026-10-20T04:00:00Z",
|
|
53
|
+
source: "call",
|
|
54
|
+
url: "https://beta2027.example.org/call",
|
|
55
|
+
read: "2026-09-09",
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
submission: { id: 7 },
|
|
61
|
+
outcome: { kind: "open" },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const cycles = (r: ReturnType<typeof parseCycles>): readonly Cycle[] => {
|
|
65
|
+
if (!r.ok || r.value === null) throw new Error(JSON.stringify(r));
|
|
66
|
+
return r.value;
|
|
67
|
+
};
|
|
68
|
+
/** The parsed cycles of `input`, each present — a list a test's own setup guarantees. */
|
|
69
|
+
const parsed = (input: readonly unknown[]): Cycle[] =>
|
|
70
|
+
cycles(parseCycles(input)).map((c, i) => present(c, `cycle ${String(i)}`));
|
|
71
|
+
|
|
72
|
+
describe("parseCycles", () => {
|
|
73
|
+
it("absent is null: a paper without cycles keeps its flat settings", () => {
|
|
74
|
+
expect(parseCycles(undefined)).toEqual({ ok: true, value: null });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("reads every field, fills the defaults, and keeps the list order", () => {
|
|
78
|
+
const [a, b] = cycles(parseCycles([REJECTED, OPEN]));
|
|
79
|
+
expect(a).toEqual({
|
|
80
|
+
id: "alpha-2026",
|
|
81
|
+
venue: { kind: "preset", extends: "paperlint:acm-sigconf" },
|
|
82
|
+
kind: "short",
|
|
83
|
+
opened: "2026-07-01",
|
|
84
|
+
phase: "prepared",
|
|
85
|
+
deadlines: [
|
|
86
|
+
{
|
|
87
|
+
what: "submission",
|
|
88
|
+
observed: [
|
|
89
|
+
{
|
|
90
|
+
at: "2026-08-06T11:59:00Z",
|
|
91
|
+
source: "call",
|
|
92
|
+
url: "https://example.org/cfp",
|
|
93
|
+
read: "2026-07-01",
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
override: null,
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
submission: null,
|
|
100
|
+
outcome: {
|
|
101
|
+
kind: "rejected",
|
|
102
|
+
date: "2026-09-08",
|
|
103
|
+
desk: false,
|
|
104
|
+
evidence: "reviews/alpha.md",
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
expect(b?.phase).toBe("porting");
|
|
108
|
+
expect(b?.submission).toEqual({ id: 7 });
|
|
109
|
+
expect(b?.outcome).toEqual({ kind: "open" });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("a named venue (no preset yet) carries its name and call", () => {
|
|
113
|
+
const [c] = cycles(
|
|
114
|
+
parseCycles([
|
|
115
|
+
{
|
|
116
|
+
...OPEN,
|
|
117
|
+
venue: {
|
|
118
|
+
kind: "named",
|
|
119
|
+
name: "ExampleSec '27",
|
|
120
|
+
url: "https://example.org/call",
|
|
121
|
+
},
|
|
122
|
+
kind: null,
|
|
123
|
+
},
|
|
124
|
+
]),
|
|
125
|
+
);
|
|
126
|
+
expect(c?.venue).toEqual({
|
|
127
|
+
kind: "named",
|
|
128
|
+
name: "ExampleSec '27",
|
|
129
|
+
url: "https://example.org/call",
|
|
130
|
+
});
|
|
131
|
+
expect(c?.kind).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("parseCycles — the outcomes", () => {
|
|
136
|
+
it("a withdrawn outcome: date and evidence, nothing else", () => {
|
|
137
|
+
const [c] = parsed([
|
|
138
|
+
{
|
|
139
|
+
...REJECTED,
|
|
140
|
+
outcome: { kind: "withdrawn", date: "2026-07-18", evidence: "n.md" },
|
|
141
|
+
},
|
|
142
|
+
]);
|
|
143
|
+
expect(c?.outcome).toEqual({
|
|
144
|
+
kind: "withdrawn",
|
|
145
|
+
date: "2026-07-18",
|
|
146
|
+
evidence: "n.md",
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("parseCycles refuses, naming the entry and the key", () => {
|
|
152
|
+
it.each([
|
|
153
|
+
["not a list", "a cycle", /must be a LIST/],
|
|
154
|
+
["an entry that is not an object", [1], /cycles\[0\] must be an object/],
|
|
155
|
+
[
|
|
156
|
+
"an unknown key",
|
|
157
|
+
[{ ...OPEN, venu: 1 }],
|
|
158
|
+
/cycles\[0\]: unknown key "venu"/,
|
|
159
|
+
],
|
|
160
|
+
["a missing id", [{ ...OPEN, id: undefined }], /cycles\[0\]: "id" must be/],
|
|
161
|
+
[
|
|
162
|
+
"an opened date that is not a date",
|
|
163
|
+
[{ ...OPEN, opened: "September" }],
|
|
164
|
+
/"opened" must be a date YYYY-MM-DD/,
|
|
165
|
+
],
|
|
166
|
+
[
|
|
167
|
+
"a venue of an unknown kind",
|
|
168
|
+
[{ ...OPEN, venue: { kind: "url" } }],
|
|
169
|
+
/"venue" must be \{ "kind": "preset", "extends": … \} or \{ "kind": "named", "name": …, "url": … \}/,
|
|
170
|
+
],
|
|
171
|
+
])("refuses %s, naming the entry and the key", (_what, input, re) => {
|
|
172
|
+
const r = parseCycles(input);
|
|
173
|
+
expect(r.ok).toBe(false);
|
|
174
|
+
if (!r.ok) expect(r.error).toMatch(re);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("parseCycles refuses a malformed deadline, outcome or submission", () => {
|
|
179
|
+
it.each([
|
|
180
|
+
[
|
|
181
|
+
"a deadline the deadline parser refuses, named by its place in the list",
|
|
182
|
+
[{ ...OPEN, deadlines: [{ what: "submission", notes: "x" }] }],
|
|
183
|
+
/^cycles\[0\]\.deadlines\[0\]: unknown key "notes"/,
|
|
184
|
+
],
|
|
185
|
+
[
|
|
186
|
+
"an unknown key in an open outcome",
|
|
187
|
+
[{ ...OPEN, outcome: { kind: "open", foo: 1 } }],
|
|
188
|
+
/^cycles\[0\]\.outcome: unknown key "foo" — an open outcome's keys: kind$/,
|
|
189
|
+
],
|
|
190
|
+
[
|
|
191
|
+
"an unknown key in a decision",
|
|
192
|
+
[{ ...REJECTED, outcome: { ...REJECTED.outcome, reviews: 3 } }],
|
|
193
|
+
/^cycles\[0\]\.outcome: unknown key "reviews" — a rejected outcome's keys: kind, date, evidence, desk$/,
|
|
194
|
+
],
|
|
195
|
+
[
|
|
196
|
+
"a desk flag on an acceptance",
|
|
197
|
+
[
|
|
198
|
+
{
|
|
199
|
+
...REJECTED,
|
|
200
|
+
outcome: {
|
|
201
|
+
kind: "accepted",
|
|
202
|
+
date: "2026-08-21",
|
|
203
|
+
evidence: "r.md",
|
|
204
|
+
desk: false,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
/^cycles\[0\]\.outcome: unknown key "desk" — an accepted outcome's keys: kind, date, evidence$/,
|
|
209
|
+
],
|
|
210
|
+
])("refuses %s, naming the entry and the key", (_what, input, re) => {
|
|
211
|
+
const r = parseCycles(input);
|
|
212
|
+
expect(r.ok).toBe(false);
|
|
213
|
+
if (!r.ok) expect(r.error).toMatch(re);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("parseCycles refuses an unknown key or a bad shape in a venue or a submission", () => {
|
|
218
|
+
it.each([
|
|
219
|
+
[
|
|
220
|
+
"an unknown key in a venue",
|
|
221
|
+
[{ ...OPEN, venue: { ...OPEN.venue, portal: "https://x" } }],
|
|
222
|
+
/^cycles\[0\]\.venue: unknown key "portal" — a preset venue's keys: kind, extends$/,
|
|
223
|
+
],
|
|
224
|
+
[
|
|
225
|
+
"an unknown key in a named venue",
|
|
226
|
+
[
|
|
227
|
+
{
|
|
228
|
+
...OPEN,
|
|
229
|
+
venue: { kind: "named", name: "X", url: "https://x", year: 2027 },
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
/^cycles\[0\]\.venue: unknown key "year" — a named venue's keys: kind, name, url$/,
|
|
233
|
+
],
|
|
234
|
+
[
|
|
235
|
+
"a preset venue with an empty extends",
|
|
236
|
+
[{ ...OPEN, venue: { kind: "preset", extends: "" } }],
|
|
237
|
+
/^cycles\[0\]: "venue" must be \{ "kind": "preset"/,
|
|
238
|
+
],
|
|
239
|
+
[
|
|
240
|
+
"a named venue without its url",
|
|
241
|
+
[{ ...OPEN, venue: { kind: "named", name: "X" } }],
|
|
242
|
+
/^cycles\[0\]: "venue" must be \{ "kind": "preset"/,
|
|
243
|
+
],
|
|
244
|
+
[
|
|
245
|
+
"a submission that is a bare number",
|
|
246
|
+
[{ ...OPEN, submission: 7 }],
|
|
247
|
+
/^cycles\[0\]: "submission" must be \{ "id"/,
|
|
248
|
+
],
|
|
249
|
+
[
|
|
250
|
+
"an unknown key in a submission",
|
|
251
|
+
[{ ...OPEN, submission: { id: 7, url: "https://x" } }],
|
|
252
|
+
/^cycles\[0\]\.submission: unknown key "url" — a submission's keys: id$/,
|
|
253
|
+
],
|
|
254
|
+
])("refuses %s, naming the entry and the key", (_what, input, re) => {
|
|
255
|
+
const r = parseCycles(input);
|
|
256
|
+
expect(r.ok).toBe(false);
|
|
257
|
+
if (!r.ok) expect(r.error).toMatch(re);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe("parseCycles refuses a malformed decision, phase or submission", () => {
|
|
262
|
+
it.each([
|
|
263
|
+
[
|
|
264
|
+
"a decision without evidence",
|
|
265
|
+
[
|
|
266
|
+
{
|
|
267
|
+
...REJECTED,
|
|
268
|
+
outcome: { kind: "rejected", date: "2026-09-08", desk: false },
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
/"outcome" of kind rejected needs "evidence"/,
|
|
272
|
+
],
|
|
273
|
+
[
|
|
274
|
+
"a desk flag that is not a boolean",
|
|
275
|
+
[{ ...REJECTED, outcome: { ...REJECTED.outcome, desk: "yes" } }],
|
|
276
|
+
/"outcome.desk" must be true or false/,
|
|
277
|
+
],
|
|
278
|
+
[
|
|
279
|
+
"an outcome of an unknown kind",
|
|
280
|
+
[{ ...OPEN, outcome: { kind: "desk-reject" } }],
|
|
281
|
+
/"outcome" kind must be one of open, accepted, rejected, withdrawn/,
|
|
282
|
+
],
|
|
283
|
+
[
|
|
284
|
+
"a phase that is not porting or prepared",
|
|
285
|
+
[{ ...OPEN, phase: "draft" }],
|
|
286
|
+
/"phase" must be "porting" or "prepared"/,
|
|
287
|
+
],
|
|
288
|
+
[
|
|
289
|
+
"a submission id that is not a positive integer",
|
|
290
|
+
[{ ...OPEN, submission: { id: 0 } }],
|
|
291
|
+
/"submission" must be \{ "id": <the submission number/,
|
|
292
|
+
],
|
|
293
|
+
])("refuses %s, naming the entry and the key", (_what, input, re) => {
|
|
294
|
+
const r = parseCycles(input);
|
|
295
|
+
expect(r.ok).toBe(false);
|
|
296
|
+
if (!r.ok) expect(r.error).toMatch(re);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
/** The one-line refusal, as every table below asserts it. */
|
|
301
|
+
const refuses = (_what: string, input: unknown, re: RegExp): void => {
|
|
302
|
+
const r = parseCycles(input);
|
|
303
|
+
expect(r.ok).toBe(false);
|
|
304
|
+
if (!r.ok) expect(r.error).toMatch(re);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
describe("parseCycles refuses a value of the wrong shape, naming the entry and the key", () => {
|
|
308
|
+
it.each([
|
|
309
|
+
[
|
|
310
|
+
"a venue that is not an object",
|
|
311
|
+
[{ ...OPEN, venue: "paperlint:ieee-conference" }],
|
|
312
|
+
/"venue" must be \{ "kind": "preset"/,
|
|
313
|
+
],
|
|
314
|
+
[
|
|
315
|
+
"a decision whose date is not a day",
|
|
316
|
+
[{ ...REJECTED, outcome: { ...REJECTED.outcome, date: "08.09.2026" } }],
|
|
317
|
+
/"outcome" of kind rejected needs "date" \(YYYY-MM-DD\)/,
|
|
318
|
+
],
|
|
319
|
+
[
|
|
320
|
+
"a kind that is not a string",
|
|
321
|
+
[{ ...OPEN, kind: 5 }],
|
|
322
|
+
/"kind" must be a string or null/,
|
|
323
|
+
],
|
|
324
|
+
[
|
|
325
|
+
"two broken cycles: the first is named, the second never read",
|
|
326
|
+
[{}, {}],
|
|
327
|
+
/^cycles\[0\]: "id" must be a non-empty string/,
|
|
328
|
+
],
|
|
329
|
+
])("refuses %s", refuses);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe("currentCycle", () => {
|
|
333
|
+
it("no cycles: none; the last cycle open: that cycle; the last cycle closed: parked on it", () => {
|
|
334
|
+
expect(currentCycle([])).toEqual({ ok: true, value: { kind: "none" } });
|
|
335
|
+
const [a, b] = parsed([REJECTED, OPEN]);
|
|
336
|
+
expect(currentCycle(parsed([REJECTED, OPEN]))).toEqual({
|
|
337
|
+
ok: true,
|
|
338
|
+
value: { kind: "cycle", cycle: b },
|
|
339
|
+
});
|
|
340
|
+
expect(currentCycle(parsed([REJECTED]))).toEqual({
|
|
341
|
+
ok: true,
|
|
342
|
+
value: { kind: "parked", last: a },
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe("currentCycle — closed lists", () => {
|
|
348
|
+
it("an ACCEPTED last cycle is not parked: the paper lives on at that venue", () => {
|
|
349
|
+
const [a] = parsed([
|
|
350
|
+
{
|
|
351
|
+
...REJECTED,
|
|
352
|
+
outcome: { kind: "accepted", date: "2026-08-21", evidence: "r.md" },
|
|
353
|
+
},
|
|
354
|
+
]);
|
|
355
|
+
expect(
|
|
356
|
+
currentCycle(
|
|
357
|
+
parsed([
|
|
358
|
+
{
|
|
359
|
+
...REJECTED,
|
|
360
|
+
outcome: { kind: "accepted", date: "2026-08-21", evidence: "r.md" },
|
|
361
|
+
},
|
|
362
|
+
]),
|
|
363
|
+
),
|
|
364
|
+
).toEqual({
|
|
365
|
+
ok: true,
|
|
366
|
+
value: { kind: "accepted", cycle: a },
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("two open cycles is dual submission (or a cycle never closed), by name", () => {
|
|
371
|
+
const r = currentCycle(
|
|
372
|
+
parsed([{ ...REJECTED, outcome: { kind: "open" } }, OPEN]),
|
|
373
|
+
);
|
|
374
|
+
expect(r).toEqual({
|
|
375
|
+
ok: false,
|
|
376
|
+
error: { kind: "two-open", ids: ["alpha-2026", "beta-2027"] },
|
|
377
|
+
});
|
|
378
|
+
if (!r.ok)
|
|
379
|
+
expect(cycleProblemText(r.error)).toMatch(
|
|
380
|
+
/cycles «alpha-2026» and «beta-2027» are both open/,
|
|
381
|
+
);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("an open cycle that is not the last is a record out of order", () => {
|
|
385
|
+
const list = parsed([
|
|
386
|
+
{ ...REJECTED, outcome: { kind: "open" } },
|
|
387
|
+
{ ...OPEN, outcome: REJECTED.outcome },
|
|
388
|
+
]);
|
|
389
|
+
expect(currentCycle(list)).toEqual({
|
|
390
|
+
ok: false,
|
|
391
|
+
error: { kind: "open-not-last", id: "alpha-2026" },
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("two cycles with one id cannot be told apart by a stage", () => {
|
|
396
|
+
expect(
|
|
397
|
+
currentCycle(parsed([REJECTED, { ...OPEN, id: "alpha-2026" }])),
|
|
398
|
+
).toEqual({
|
|
399
|
+
ok: false,
|
|
400
|
+
error: { kind: "duplicate-id", id: "alpha-2026" },
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe("cycleProblemText — one line per problem, naming what to change", () => {
|
|
406
|
+
it.each([
|
|
407
|
+
[
|
|
408
|
+
{ kind: "open-not-last", id: "alpha-2026" },
|
|
409
|
+
/cycle «alpha-2026» is open but is not the last entry/,
|
|
410
|
+
],
|
|
411
|
+
[
|
|
412
|
+
{ kind: "duplicate-id", id: "alpha-2026" },
|
|
413
|
+
/two cycles carry the id «alpha-2026»/,
|
|
414
|
+
],
|
|
415
|
+
] as const)("%j", (problem, text) => {
|
|
416
|
+
expect(cycleProblemText(problem)).toMatch(text);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
describe("currentDeadlines", () => {
|
|
421
|
+
it("the open or accepted attempt's entries; none when parked or with no cycles", () => {
|
|
422
|
+
const [a, b] = parsed([REJECTED, OPEN]);
|
|
423
|
+
const open = present(b, "open cycle");
|
|
424
|
+
const closed = present(a, "closed cycle");
|
|
425
|
+
expect(currentDeadlines({ kind: "cycle", cycle: open })).toBe(
|
|
426
|
+
open.deadlines,
|
|
427
|
+
);
|
|
428
|
+
expect(currentDeadlines({ kind: "accepted", cycle: closed })).toBe(
|
|
429
|
+
closed.deadlines,
|
|
430
|
+
);
|
|
431
|
+
expect(currentDeadlines({ kind: "parked", last: closed })).toEqual([]);
|
|
432
|
+
expect(currentDeadlines({ kind: "none" })).toEqual([]);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
describe("effectiveBlind", () => {
|
|
437
|
+
it("a blind venue stops being blind for an ACCEPTED cycle: the camera-ready carries the authors", () => {
|
|
438
|
+
const [a, b] = parsed([REJECTED, OPEN]);
|
|
439
|
+
const open = present(b, "open cycle");
|
|
440
|
+
const accepted: Cycle = {
|
|
441
|
+
...present(a, "closed cycle"),
|
|
442
|
+
outcome: {
|
|
443
|
+
kind: "accepted",
|
|
444
|
+
date: "2026-08-21",
|
|
445
|
+
evidence: "reviews/a.md",
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
expect(effectiveBlind(true, { kind: "cycle", cycle: open })).toBe(true);
|
|
449
|
+
expect(effectiveBlind(true, { kind: "accepted", cycle: accepted })).toBe(
|
|
450
|
+
false,
|
|
451
|
+
);
|
|
452
|
+
expect(effectiveBlind(true, { kind: "parked", last: accepted })).toBe(true);
|
|
453
|
+
expect(effectiveBlind(true, { kind: "none" })).toBe(true);
|
|
454
|
+
expect(effectiveBlind(false, { kind: "cycle", cycle: open })).toBe(false);
|
|
455
|
+
});
|
|
456
|
+
});
|