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.
- package/.github/workflows/ci.yml +3 -0
- package/.github/workflows/deadlines.yml +100 -0
- package/README.md +24 -11
- 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 +42 -7
- 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/body-pages.d.ts +29 -0
- package/dist/domain/body-pages.d.ts.map +1 -1
- package/dist/domain/body-pages.js +25 -0
- package/dist/domain/body-pages.js.map +1 -1
- 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-includes.d.ts +3 -0
- package/dist/paper-includes.d.ts.map +1 -1
- package/dist/paper-includes.js +17 -2
- package/dist/paper-includes.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 +22 -3
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +26 -17
- package/dist/presets.js.map +1 -1
- package/dist/submission.d.ts +16 -0
- package/dist/submission.d.ts.map +1 -1
- package/dist/submission.js +59 -16
- package/dist/submission.js.map +1 -1
- package/dist/tex-requirements.d.ts +12 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +26 -3
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +24 -2
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +98 -10
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/venue-rules.d.ts +12 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +81 -17
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +140 -12
- package/docs/e2e.md +14 -12
- 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/format/page-limit.md +18 -1
- package/docs/rules/tex/template.md +25 -5
- package/docs/rules/tex/venue-leftover.md +13 -0
- package/docs/rules.md +68 -55
- package/docs/submission.md +10 -1
- package/eslint-rules/cycle-stage.test.mjs +131 -0
- package/eslint-rules/cycle-stage.ts +141 -0
- package/eslint.config.mjs +41 -1
- package/fixtures/build-e2e/msr/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/msr/expect.json +18 -0
- package/fixtures/build-e2e/msr/paper.tex +49 -0
- package/fixtures/build-e2e/msr/paperlint.json +1 -0
- package/fixtures/build-e2e/msr-appendix-after/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/msr-appendix-after/expect.json +20 -0
- package/fixtures/build-e2e/msr-appendix-after/paper.tex +48 -0
- package/fixtures/build-e2e/msr-appendix-after/paperlint.json +1 -0
- 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/ieee-conference.md +1 -1
- package/presets/msr.jsonc +95 -0
- package/presets/msr.md +98 -0
- package/presets/venue-profile.schema.json +64 -0
- package/scripts/check.ts +4 -0
- package/scripts/refresh-deadlines.test.ts +431 -0
- package/scripts/refresh-deadlines.ts +387 -0
- package/skills/camera-ready/SKILL.md +8 -1
- package/skills/camera-ready/SKILL.md.spec.ts +7 -0
- package/skills/submission-portal/SKILL.md +2 -2
- package/skills/submission-portal/SKILL.md.spec.ts +1 -1
- package/skills/submit-paper/SKILL.md +9 -1
- package/skills/submit-paper/SKILL.md.spec.ts +8 -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 +50 -6
- package/src/cycle-rules.test.ts +278 -0
- package/src/cycle-rules.ts +273 -0
- package/src/domain/body-pages.test.ts +144 -0
- package/src/domain/body-pages.ts +61 -1
- 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 +28 -0
- package/src/paper-includes.test.ts +26 -0
- package/src/paper-includes.ts +20 -2
- package/src/paper-settings-commands.test.ts +1 -1
- 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/preset-card-rule.test.ts +1 -0
- package/src/presets.test.ts +171 -1
- package/src/presets.ts +65 -15
- package/src/rule-cases.test.ts +144 -0
- package/src/submission.test.ts +147 -4
- package/src/submission.ts +100 -18
- package/src/tex-requirements.ts +52 -4
- package/src/tex-venue-rules.test.ts +237 -0
- package/src/tex-venue-rules.ts +130 -15
- package/src/venue-rules.test.ts +255 -2
- package/src/venue-rules.ts +115 -22
package/src/presets.ts
CHANGED
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
* tex union — a child never removes a package its parent needs
|
|
26
26
|
* format per key, the child wins; `kinds` by kind name, a child's kind replaces that kind
|
|
27
27
|
* rules per rule id, the child wins; `ruleOrigins` keeps which file set each
|
|
28
|
-
* template the child wins
|
|
29
|
-
* `portal` (each whole)
|
|
28
|
+
* template the child wins, with its `template_forbids`; so does `name`, and `required_sections`, `register`, `talk`,
|
|
29
|
+
* `portal` and `deadlines` (each whole)
|
|
30
30
|
* aliases union, with every `name` — what the venue is called along the chain
|
|
31
31
|
*
|
|
32
32
|
* ── THE ID AND THE LABEL ──────────────────────────────────────────────────────────
|
|
@@ -63,6 +63,7 @@ import { messageOf } from "./domain/text.ts";
|
|
|
63
63
|
import type { RegisterAnchor } from "./domain/register.ts";
|
|
64
64
|
import type { VenueTalk } from "./domain/talk.ts";
|
|
65
65
|
import type { VenuePortal } from "./domain/submission.ts";
|
|
66
|
+
import type { Reading } from "./domain/deadline.ts";
|
|
66
67
|
|
|
67
68
|
/** The prefix of a shipped preset's spec. */
|
|
68
69
|
export const SHIPPED_PREFIX = "paperlint:";
|
|
@@ -75,10 +76,12 @@ export interface PresetDeps {
|
|
|
75
76
|
readonly venuesDir: string;
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
/** A preset's `template`, as its file spells it. */
|
|
79
|
+
/** A preset's `template`, as its file spells it, and the class options that file forbids. */
|
|
79
80
|
export interface PresetTemplate {
|
|
80
81
|
readonly text: string;
|
|
81
82
|
readonly file: string;
|
|
83
|
+
/** `template_forbids` of the same file: options the paper's class line must not carry. */
|
|
84
|
+
readonly forbids: readonly string[];
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
/** A resolved chain, merged. */
|
|
@@ -104,6 +107,11 @@ export interface Preset {
|
|
|
104
107
|
readonly aliases: readonly string[];
|
|
105
108
|
/** Whether the venue reviews double-blind (`anonymity/identity`); false when no file of the chain says. */
|
|
106
109
|
readonly blind: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* When the venue's names count as a mention in a paper's text (`tex/venue-leftover`): `any`
|
|
112
|
+
* whole-word use, or `with-year` beside it; `any` when no file of the chain says.
|
|
113
|
+
*/
|
|
114
|
+
readonly mentions: "any" | "with-year";
|
|
107
115
|
readonly requiredSections: readonly RequiredSection[];
|
|
108
116
|
readonly tex: TexRequirements;
|
|
109
117
|
readonly format: VenueFormat;
|
|
@@ -116,6 +124,11 @@ export interface Preset {
|
|
|
116
124
|
readonly talk: VenueTalk | null;
|
|
117
125
|
/** Where the venue takes submissions (`paperlint submission`); null when no file of the chain says. */
|
|
118
126
|
readonly portal: VenuePortal | null;
|
|
127
|
+
/**
|
|
128
|
+
* The edition's deadlines as they were read (the portal's, refreshed by paperlint's scheduled
|
|
129
|
+
* workflow); empty when no file of the chain names any. A paper's current cycle derives them.
|
|
130
|
+
*/
|
|
131
|
+
readonly deadlines: readonly Reading[];
|
|
119
132
|
}
|
|
120
133
|
|
|
121
134
|
/** Why a spec does not resolve. */
|
|
@@ -254,6 +267,7 @@ export function mergeFormat(
|
|
|
254
267
|
fontsText: pick("fontsText"),
|
|
255
268
|
fontsTitle: pick("fontsTitle"),
|
|
256
269
|
bodyEndsAt: pick("bodyEndsAt"),
|
|
270
|
+
appendixInBody: pick("appendixInBody"),
|
|
257
271
|
kinds: new Map([...parent.kinds, ...child.kinds]),
|
|
258
272
|
};
|
|
259
273
|
}
|
|
@@ -276,6 +290,7 @@ const NOTHING_MERGED: Merging = {
|
|
|
276
290
|
template: null,
|
|
277
291
|
aliases: [],
|
|
278
292
|
blind: false,
|
|
293
|
+
mentions: "any",
|
|
279
294
|
requiredSections: [],
|
|
280
295
|
tex: NO_REQUIREMENTS,
|
|
281
296
|
format: NO_FORMAT,
|
|
@@ -284,18 +299,37 @@ const NOTHING_MERGED: Merging = {
|
|
|
284
299
|
registerAnchors: [],
|
|
285
300
|
talk: null,
|
|
286
301
|
portal: null,
|
|
302
|
+
deadlines: [],
|
|
287
303
|
};
|
|
288
304
|
|
|
305
|
+
/** The blocks a child replaces whole: the child's when it names one, else its parents'. Pure. */
|
|
306
|
+
const wholeBlocks = (
|
|
307
|
+
acc: Merging,
|
|
308
|
+
p: PresetFile,
|
|
309
|
+
): Pick<
|
|
310
|
+
Merging,
|
|
311
|
+
"requiredSections" | "registerAnchors" | "talk" | "portal" | "deadlines"
|
|
312
|
+
> => ({
|
|
313
|
+
requiredSections: p.requiredSections ?? acc.requiredSections,
|
|
314
|
+
registerAnchors: p.registerAnchors ?? acc.registerAnchors,
|
|
315
|
+
talk: p.talk ?? acc.talk,
|
|
316
|
+
portal: p.portal ?? acc.portal,
|
|
317
|
+
deadlines: p.deadlines ?? acc.deadlines,
|
|
318
|
+
});
|
|
319
|
+
|
|
289
320
|
/** One file of the chain, `file`, merged over what its parents gave. Pure. */
|
|
290
321
|
const mergeOne = (
|
|
291
322
|
acc: Merging,
|
|
292
323
|
{ preset: p, file }: { readonly preset: PresetFile; readonly file: string },
|
|
293
324
|
): Merging => ({
|
|
294
325
|
name: nameOf(p.identity) ?? acc.name,
|
|
295
|
-
template:
|
|
326
|
+
template:
|
|
327
|
+
p.template === null
|
|
328
|
+
? acc.template
|
|
329
|
+
: { text: p.template, file, forbids: p.templateForbids },
|
|
296
330
|
aliases: [...new Set([...acc.aliases, ...namesOf(p.identity), ...p.aliases])],
|
|
297
331
|
blind: p.blind ?? acc.blind,
|
|
298
|
-
|
|
332
|
+
mentions: p.mentions ?? acc.mentions,
|
|
299
333
|
tex: p.tex ? mergeRequirements(acc.tex, p.tex) : acc.tex,
|
|
300
334
|
format: mergeFormat(acc.format, p.format),
|
|
301
335
|
rules: { ...acc.rules, ...p.rules },
|
|
@@ -303,9 +337,7 @@ const mergeOne = (
|
|
|
303
337
|
...acc.ruleOrigins,
|
|
304
338
|
...Object.fromEntries(Object.keys(p.rules).map((id) => [id, file])),
|
|
305
339
|
},
|
|
306
|
-
|
|
307
|
-
talk: p.talk ?? acc.talk,
|
|
308
|
-
portal: p.portal ?? acc.portal,
|
|
340
|
+
...wholeBlocks(acc, p),
|
|
309
341
|
});
|
|
310
342
|
|
|
311
343
|
/** The chain, root first, merged into one preset. Pure. */
|
|
@@ -368,17 +400,21 @@ export function resolveShipped(deps: PresetDeps): readonly Preset[] {
|
|
|
368
400
|
}
|
|
369
401
|
|
|
370
402
|
/** A shipped venue as a name knows it, and the files of its chain: what tells it from a paper's own. */
|
|
371
|
-
export type ShippedVenue = Pick<
|
|
403
|
+
export type ShippedVenue = Pick<
|
|
404
|
+
Preset,
|
|
405
|
+
"label" | "aliases" | "chain" | "mentions"
|
|
406
|
+
>;
|
|
372
407
|
|
|
373
408
|
/**
|
|
374
409
|
* A resolved preset as a name knows it: its label, its aliases, its chain. Pure. (Not Remeda's
|
|
375
410
|
* `pick`: the app layer may not import it — `boundaries/dependencies`, `APP_EXTERNALS`.)
|
|
376
411
|
*/
|
|
377
|
-
export const venueOf = ({
|
|
412
|
+
export const venueOf = ({
|
|
378
413
|
label,
|
|
379
414
|
aliases,
|
|
380
415
|
chain,
|
|
381
|
-
|
|
416
|
+
mentions,
|
|
417
|
+
}: Preset): ShippedVenue => ({ label, aliases, chain, mentions });
|
|
382
418
|
|
|
383
419
|
/**
|
|
384
420
|
* THE ONE SOURCE OF VENUE NAMES: every shipped venue's label and `aliases` (with each `name` along
|
|
@@ -440,15 +476,29 @@ export function paperPreset(paperDir: string, deps: PresetDeps): PaperPreset {
|
|
|
440
476
|
: { kind: "preset-problem", settings, problem: r.error };
|
|
441
477
|
}
|
|
442
478
|
|
|
479
|
+
/** A paper's settings or preset that cannot be used. */
|
|
480
|
+
export type UnusablePreset = Extract<
|
|
481
|
+
PaperPreset,
|
|
482
|
+
{ readonly kind: "settings-problem" | "preset-problem" }
|
|
483
|
+
>;
|
|
484
|
+
|
|
485
|
+
/** The one-line reason a paper's settings or preset cannot be used. */
|
|
486
|
+
export const unusablePresetLine = (
|
|
487
|
+
paperDir: string,
|
|
488
|
+
p: UnusablePreset,
|
|
489
|
+
): string =>
|
|
490
|
+
p.kind === "preset-problem"
|
|
491
|
+
? `${join(paperDir, CONFIG_FILE)}: ${presetProblemText(p.problem)}`
|
|
492
|
+
: settingsProblemLine(paperDir, p.problem);
|
|
493
|
+
|
|
443
494
|
/** The one-line reason a paper's settings or preset cannot be used, or null when they can. */
|
|
444
495
|
export function paperPresetProblem(
|
|
445
496
|
paperDir: string,
|
|
446
497
|
p: PaperPreset,
|
|
447
498
|
): string | null {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
return settingsProblemLine(paperDir, p.problem);
|
|
499
|
+
return p.kind === "preset-problem" || p.kind === "settings-problem"
|
|
500
|
+
? unusablePresetLine(paperDir, p)
|
|
501
|
+
: null;
|
|
452
502
|
}
|
|
453
503
|
|
|
454
504
|
/** The one line naming a paper's `paperlint.json` that does not parse, and why. */
|
package/src/rule-cases.test.ts
CHANGED
|
@@ -316,6 +316,88 @@ const onPdf = (tree: Record<string, string>, line = 1) => ({
|
|
|
316
316
|
line,
|
|
317
317
|
});
|
|
318
318
|
|
|
319
|
+
/** One open cycle on the ACM family: what a flat `{ "extends": "paperlint:acm-sigconf" }` says, as a record. */
|
|
320
|
+
const OPEN_CYCLE = JSON.stringify({
|
|
321
|
+
cycles: [
|
|
322
|
+
{
|
|
323
|
+
id: "open-2027",
|
|
324
|
+
venue: { kind: "preset", extends: "paperlint:acm-sigconf" },
|
|
325
|
+
opened: "2026-09-09",
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
});
|
|
329
|
+
/**
|
|
330
|
+
* One open cycle whose registration deadline is after its submission deadline: a date typed wrong.
|
|
331
|
+
* The record's STRUCTURAL problems — two open cycles, a bare date, an unknown key — never reach a
|
|
332
|
+
* rule: `paperRuleBlocks` refuses the settings file before lint runs (measured 2026-10-05: one line
|
|
333
|
+
* naming the file and both ids, exit 2), as it refuses any unreadable `paperlint.json`. The order of
|
|
334
|
+
* the deadlines is what the record parses with and the rule still judges.
|
|
335
|
+
*/
|
|
336
|
+
const DISORDERED = JSON.stringify({
|
|
337
|
+
cycles: [
|
|
338
|
+
{
|
|
339
|
+
id: "open-2027",
|
|
340
|
+
venue: { kind: "preset", extends: "paperlint:acm-sigconf" },
|
|
341
|
+
opened: "2026-09-09",
|
|
342
|
+
// The call's own abstract day after its paper day: a date typed wrong. (The portal's and the
|
|
343
|
+
// call's readings disagreeing is not a finding: the earlier binds.)
|
|
344
|
+
deadlines: [
|
|
345
|
+
{
|
|
346
|
+
what: "registration",
|
|
347
|
+
observed: [
|
|
348
|
+
{
|
|
349
|
+
at: "2026-10-25 AoE",
|
|
350
|
+
source: "call",
|
|
351
|
+
url: "https://conf.example/cfp",
|
|
352
|
+
read: "2026-09-09",
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
what: "submission",
|
|
358
|
+
observed: [
|
|
359
|
+
{
|
|
360
|
+
at: "2026-10-20T04:00:00Z",
|
|
361
|
+
source: "portal",
|
|
362
|
+
url: "https://conf.example/deadlines",
|
|
363
|
+
read: "2026-09-09",
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
at: "2026-10-23 AoE",
|
|
367
|
+
source: "call",
|
|
368
|
+
url: "https://conf.example/cfp",
|
|
369
|
+
read: "2026-09-09",
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
});
|
|
377
|
+
/** One closed cycle whose evidence is `reviews/decision.md`. */
|
|
378
|
+
const CLOSED_CYCLE = JSON.stringify({
|
|
379
|
+
cycles: [
|
|
380
|
+
{
|
|
381
|
+
id: "first-2026",
|
|
382
|
+
venue: { kind: "preset", extends: "paperlint:acm-sigconf" },
|
|
383
|
+
opened: "2026-07-01",
|
|
384
|
+
outcome: {
|
|
385
|
+
kind: "rejected",
|
|
386
|
+
date: "2026-09-08",
|
|
387
|
+
evidence: "reviews/decision.md",
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
],
|
|
391
|
+
});
|
|
392
|
+
/** A BlindConf cycle still declared porting — on a paper whose class line is already BlindConf's. */
|
|
393
|
+
const BLIND_PORTING = {
|
|
394
|
+
id: "blindconf-2027",
|
|
395
|
+
venue: { kind: "preset", extends: "./blindconf.jsonc" },
|
|
396
|
+
opened: "2026-09-09",
|
|
397
|
+
phase: "porting",
|
|
398
|
+
// What `blind()` declares flat; with cycles it moves into the record's siblings, not the cycle.
|
|
399
|
+
};
|
|
400
|
+
|
|
319
401
|
const CASES: Readonly<Record<string, RuleCases>> = {
|
|
320
402
|
"paper/stages": {
|
|
321
403
|
reports: {
|
|
@@ -326,6 +408,68 @@ const CASES: Readonly<Record<string, RuleCases>> = {
|
|
|
326
408
|
},
|
|
327
409
|
silent: paper(tex("x")),
|
|
328
410
|
},
|
|
411
|
+
// ── the cycle rules: the paper's attempts in paperlint.json, judged on PIPELINE-STATUS.md ──────
|
|
412
|
+
"cycle/record": {
|
|
413
|
+
reports: {
|
|
414
|
+
tree: { ...paper(tex("x")), [`${P}/paperlint.json`]: DISORDERED },
|
|
415
|
+
file: STATUS_FILE,
|
|
416
|
+
severity: 2,
|
|
417
|
+
line: 1,
|
|
418
|
+
},
|
|
419
|
+
silent: paper(tex("x")),
|
|
420
|
+
},
|
|
421
|
+
"cycle/evidence": {
|
|
422
|
+
reports: {
|
|
423
|
+
tree: { ...paper(tex("x")), [`${P}/paperlint.json`]: CLOSED_CYCLE },
|
|
424
|
+
file: STATUS_FILE,
|
|
425
|
+
severity: 2,
|
|
426
|
+
line: 1,
|
|
427
|
+
},
|
|
428
|
+
silent: {
|
|
429
|
+
...paper(tex("x")),
|
|
430
|
+
[`${P}/paperlint.json`]: CLOSED_CYCLE,
|
|
431
|
+
[`${P}/reviews/decision.md`]: "# decision\n",
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
"cycle/stage": {
|
|
435
|
+
reports: {
|
|
436
|
+
tree: {
|
|
437
|
+
...paper(tex("x")),
|
|
438
|
+
[STATUS_FILE]: SHIPPED_STATUS,
|
|
439
|
+
[`${P}/paperlint.json`]: OPEN_CYCLE,
|
|
440
|
+
[`${P}/versions/a.pdf`]: "x",
|
|
441
|
+
},
|
|
442
|
+
file: STATUS_FILE,
|
|
443
|
+
severity: 2,
|
|
444
|
+
line: 1,
|
|
445
|
+
},
|
|
446
|
+
silent: {
|
|
447
|
+
...paper(tex("x")),
|
|
448
|
+
[STATUS_FILE]: SHIPPED_STATUS.replace(
|
|
449
|
+
"date: 2026-07-22",
|
|
450
|
+
"cycle: open-2027\n date: 2026-07-22",
|
|
451
|
+
),
|
|
452
|
+
[`${P}/paperlint.json`]: OPEN_CYCLE,
|
|
453
|
+
[`${P}/versions/a.pdf`]: "x",
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
"cycle/port-done": {
|
|
457
|
+
reports: {
|
|
458
|
+
tree: {
|
|
459
|
+
...blind("Text."),
|
|
460
|
+
[`${P}/paperlint.json`]: JSON.stringify({ cycles: [BLIND_PORTING] }),
|
|
461
|
+
},
|
|
462
|
+
file: TEX_FILE,
|
|
463
|
+
severity: 2,
|
|
464
|
+
line: 1,
|
|
465
|
+
},
|
|
466
|
+
silent: {
|
|
467
|
+
...blind("Text."),
|
|
468
|
+
[`${P}/paperlint.json`]: JSON.stringify({
|
|
469
|
+
cycles: [{ ...BLIND_PORTING, phase: "prepared" }],
|
|
470
|
+
}),
|
|
471
|
+
},
|
|
472
|
+
},
|
|
329
473
|
"paper/source": {
|
|
330
474
|
reports: {
|
|
331
475
|
tree: { ...paper(tex("x")), [STATUS_FILE]: SHIPPED_STATUS },
|
package/src/submission.test.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type { SubmissionPortal } from "./ports/submission-portal.ts";
|
|
|
22
22
|
import {
|
|
23
23
|
failureText,
|
|
24
24
|
runSubmission,
|
|
25
|
+
deadlineLine,
|
|
25
26
|
submissionTarget,
|
|
26
27
|
tokenFor,
|
|
27
28
|
usageProblem,
|
|
@@ -41,8 +42,11 @@ const withPresets = (fs: Files): Files => ({
|
|
|
41
42
|
readBytes: (p) => fs.readBytes(p) ?? nodeFiles.readBytes(p),
|
|
42
43
|
});
|
|
43
44
|
|
|
45
|
+
/** A venue over aidc's format with a portal of its own — so none of aidc's deadline readings. */
|
|
44
46
|
const venue = (portal: unknown): string =>
|
|
45
|
-
JSON.stringify(
|
|
47
|
+
JSON.stringify(
|
|
48
|
+
venuePreset("venue", { extends: "paperlint:aidc", portal, deadlines: [] }),
|
|
49
|
+
);
|
|
46
50
|
const files = (o: {
|
|
47
51
|
readonly portal?: unknown;
|
|
48
52
|
readonly paper?: Readonly<Record<string, unknown>>;
|
|
@@ -117,6 +121,10 @@ const fakePortal = (answers: {
|
|
|
117
121
|
return { seen, portalFor };
|
|
118
122
|
};
|
|
119
123
|
|
|
124
|
+
/** The line `update` prints before it sends `pdf`, naming `call`. */
|
|
125
|
+
const CHECK = (pdf: string, call: string): string =>
|
|
126
|
+
`before you rely on it, open ${pdf} yourself and check it against ${call}: the page count and what counts toward the limit, the template and its class options, anonymity, and that every figure, table and reference renders — paperlint passing is not the venue accepting the format`;
|
|
127
|
+
|
|
120
128
|
const ARGS: SubmissionArgs = {
|
|
121
129
|
sub: "show",
|
|
122
130
|
paperDir: "papers/p",
|
|
@@ -287,6 +295,7 @@ test("update: a dry run by default — the paper's PDF sent, nothing saved, exit
|
|
|
287
295
|
],
|
|
288
296
|
]);
|
|
289
297
|
assert.deepEqual(r.out, [
|
|
298
|
+
CHECK("papers/p/paper.pdf", "the call for papers, https://example.org/cfp"),
|
|
290
299
|
"dry run — the portal checked the change and kept nothing; --save sends it for real",
|
|
291
300
|
"HTTP 200",
|
|
292
301
|
"valid yes",
|
|
@@ -322,7 +331,41 @@ test("update --save --submit --abstract --pdf: all of it sent, and said to be sa
|
|
|
322
331
|
{ save: true },
|
|
323
332
|
],
|
|
324
333
|
]);
|
|
325
|
-
|
|
334
|
+
// Guards: the PDF that is sent is the one named, and --save is told to check it too.
|
|
335
|
+
assert.deepEqual(r.out.slice(0, 2), [
|
|
336
|
+
CHECK("other.pdf", "the call for papers, https://example.org/cfp"),
|
|
337
|
+
"saved",
|
|
338
|
+
]);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("🔴 update: the check line names the venue's call from the preset's `url`; a preset that names no venue gets the generic line", async () => {
|
|
342
|
+
const family = memoryFiles({
|
|
343
|
+
"/w/package.json": "{}",
|
|
344
|
+
"/w/house.jsonc": JSON.stringify({
|
|
345
|
+
type: "family",
|
|
346
|
+
extends: "paperlint:ieee-conference",
|
|
347
|
+
portal: { kind: "hotcrp", url: `${SITE}/` },
|
|
348
|
+
}),
|
|
349
|
+
[`${PAPER}/paperlint.json`]: JSON.stringify({
|
|
350
|
+
extends: "../../house.jsonc",
|
|
351
|
+
submission: { id: 7 },
|
|
352
|
+
}),
|
|
353
|
+
[`${PAPER}/paper.pdf`]: PDF,
|
|
354
|
+
});
|
|
355
|
+
const r = await run({ sub: "update" }, { files: family });
|
|
356
|
+
assert.equal(r.code, 0);
|
|
357
|
+
assert.equal(
|
|
358
|
+
r.out[0],
|
|
359
|
+
CHECK("papers/p/paper.pdf", "the venue's call for papers"),
|
|
360
|
+
);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("show sends nothing, so it asks for no check", async () => {
|
|
364
|
+
const r = await run({});
|
|
365
|
+
assert.equal(
|
|
366
|
+
r.out.some((l) => l.includes("call for papers")),
|
|
367
|
+
false,
|
|
368
|
+
);
|
|
326
369
|
});
|
|
327
370
|
|
|
328
371
|
test("update: valid:false, or a non-2xx status, exits 1 with the portal's messages", async () => {
|
|
@@ -338,7 +381,7 @@ test("update: valid:false, or a non-2xx status, exits 1 with the portal's messag
|
|
|
338
381
|
{ portal: fakePortal({ update: invalid }) },
|
|
339
382
|
);
|
|
340
383
|
assert.equal(r1.code, 1);
|
|
341
|
-
assert.deepEqual(r1.out.slice(
|
|
384
|
+
assert.deepEqual(r1.out.slice(3), [
|
|
342
385
|
"valid NO",
|
|
343
386
|
"changes —",
|
|
344
387
|
`local PDF papers/p/paper.pdf sha2-${HEX}`,
|
|
@@ -380,7 +423,7 @@ test("update --save that the portal withheld says so", async () => {
|
|
|
380
423
|
{ sub: "update", save: true },
|
|
381
424
|
{ portal: fakePortal({ update: withheld }) },
|
|
382
425
|
);
|
|
383
|
-
assert.equal(r.out[
|
|
426
|
+
assert.equal(r.out[1], "SAVE WITHHELD by the portal — nothing changed");
|
|
384
427
|
});
|
|
385
428
|
|
|
386
429
|
test("update: no PDF to send, or no abstract file, is refused before the portal is asked", async () => {
|
|
@@ -437,6 +480,28 @@ test("🔴 the file and key to set: no portal in the preset, no submission in th
|
|
|
437
480
|
const noExtends = files({ paper: { submission: { id: 7 } } });
|
|
438
481
|
const t3 = submissionTarget(PAPER, deps(noExtends));
|
|
439
482
|
assert.match(!t3.ok ? t3.error : "", /paperlint\.json: no "extends"/);
|
|
483
|
+
// With cycles, `extends` beside them is refused: the message names the cycle, not the key.
|
|
484
|
+
const named = files({
|
|
485
|
+
paper: {
|
|
486
|
+
cycles: [
|
|
487
|
+
{
|
|
488
|
+
id: "secdev-2027",
|
|
489
|
+
venue: {
|
|
490
|
+
kind: "named",
|
|
491
|
+
name: "SecDev 2027",
|
|
492
|
+
url: "https://x.example",
|
|
493
|
+
},
|
|
494
|
+
opened: "2027-01-10",
|
|
495
|
+
submission: { id: 3 },
|
|
496
|
+
},
|
|
497
|
+
],
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
const t5 = submissionTarget(PAPER, deps(named));
|
|
501
|
+
assert.equal(
|
|
502
|
+
!t5.ok ? t5.error : "",
|
|
503
|
+
`${PAPER}/paperlint.json: the current cycle names no venue preset, and the preset is what declares the portal — set the venue in the current cycle ("venue": { "kind": "preset", "extends": "paperlint:…" }), or upload by hand`,
|
|
504
|
+
);
|
|
440
505
|
const broken = files({
|
|
441
506
|
paper: { extends: "../../venue.jsonc", submission: { id: "7" } },
|
|
442
507
|
});
|
|
@@ -495,3 +560,81 @@ test("usage: a subcommand is required, one paper folder, update-only flags refus
|
|
|
495
560
|
);
|
|
496
561
|
assert.equal(usageProblem({ ...ARGS, sub: "update", save: true }), null);
|
|
497
562
|
});
|
|
563
|
+
|
|
564
|
+
test("show prints the current attempt's deadlines in force: the preset's portal readings, the paper's call readings and override over them", () => {
|
|
565
|
+
const fs = memoryFiles({
|
|
566
|
+
"/w/package.json": "{}",
|
|
567
|
+
[`${PAPER}/paperlint.json`]: JSON.stringify({
|
|
568
|
+
cycles: [
|
|
569
|
+
{
|
|
570
|
+
id: "msr-2027",
|
|
571
|
+
venue: { kind: "preset", extends: "paperlint:msr" },
|
|
572
|
+
kind: "technical",
|
|
573
|
+
opened: "2026-10-05",
|
|
574
|
+
submission: { id: 7 },
|
|
575
|
+
deadlines: [
|
|
576
|
+
{
|
|
577
|
+
what: "registration",
|
|
578
|
+
observed: [
|
|
579
|
+
{
|
|
580
|
+
at: "2026-10-20 AoE",
|
|
581
|
+
source: "call",
|
|
582
|
+
url: "https://2027.msrconf.org/cfp",
|
|
583
|
+
read: "2026-10-05",
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
what: "submission",
|
|
589
|
+
observed: [
|
|
590
|
+
{
|
|
591
|
+
at: "2026-10-23 AoE",
|
|
592
|
+
source: "call",
|
|
593
|
+
url: "https://2027.msrconf.org/cfp",
|
|
594
|
+
read: "2026-10-05",
|
|
595
|
+
},
|
|
596
|
+
],
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
what: "resubmission",
|
|
600
|
+
override: {
|
|
601
|
+
at: "2026-10-27T04:00:00Z",
|
|
602
|
+
reason: "the chairs extended updates by mail",
|
|
603
|
+
evidence: "mail/extension.eml",
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
],
|
|
607
|
+
},
|
|
608
|
+
],
|
|
609
|
+
}),
|
|
610
|
+
});
|
|
611
|
+
const t = submissionTarget(PAPER, {
|
|
612
|
+
files: withPresets(fs),
|
|
613
|
+
venuesDir: presetsDir(),
|
|
614
|
+
});
|
|
615
|
+
assert.ok(t.ok, JSON.stringify(t));
|
|
616
|
+
assert.deepEqual(t.value.deadlines.map(deadlineLine), [
|
|
617
|
+
"deadline registration 2026-10-21T11:59:59Z — call, read 2026-10-05",
|
|
618
|
+
"deadline submission 2026-10-20T04:00:00Z — portal, read 2026-10-05; also call 2026-10-24T11:59:59Z, read 2026-10-05",
|
|
619
|
+
"deadline resubmission 2026-10-27T04:00:00Z — override: the chairs extended updates by mail (mail/extension.eml); also portal 2026-10-23T04:00:00Z, read 2026-10-05",
|
|
620
|
+
]);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
test("show, in the flat form: the deadlines in force are the preset's readings alone", () => {
|
|
624
|
+
const flat = memoryFiles({
|
|
625
|
+
"/w/package.json": "{}",
|
|
626
|
+
[`${PAPER}/paperlint.json`]: JSON.stringify({
|
|
627
|
+
extends: "paperlint:msr",
|
|
628
|
+
kind: "technical",
|
|
629
|
+
submission: { id: 7 },
|
|
630
|
+
}),
|
|
631
|
+
});
|
|
632
|
+
const f = submissionTarget(PAPER, {
|
|
633
|
+
files: withPresets(flat),
|
|
634
|
+
venuesDir: presetsDir(),
|
|
635
|
+
});
|
|
636
|
+
assert.deepEqual(f.ok ? f.value.deadlines.map((d) => [d.what, d.at]) : f, [
|
|
637
|
+
["submission", "2026-10-20T04:00:00Z"],
|
|
638
|
+
["resubmission", "2026-10-23T04:00:00Z"],
|
|
639
|
+
]);
|
|
640
|
+
});
|