paperlint 7.1.0 → 8.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/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/build.d.ts.map +1 -1
- package/dist/build.js +1 -6
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +3 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +53 -47
- 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/eslint-rules/latex-language.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.js +10 -13
- package/dist/eslint-rules/latex-language.js.map +1 -1
- package/dist/eslint-rules/paper-context.js +2 -2
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
- package/dist/eslint-rules/paper-typography.js +22 -91
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/eslint-rules/papers.d.ts +1 -3
- package/dist/eslint-rules/papers.d.ts.map +1 -1
- package/dist/eslint-rules/papers.js +2 -4
- package/dist/eslint-rules/papers.js.map +1 -1
- package/dist/eslint-rules/tex-build.d.ts.map +1 -1
- package/dist/eslint-rules/tex-build.js +8 -25
- package/dist/eslint-rules/tex-build.js.map +1 -1
- package/dist/init.d.ts +1 -4
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +3 -9
- package/dist/init.js.map +1 -1
- package/dist/lib/markdown.d.ts +5 -81
- package/dist/lib/markdown.d.ts.map +1 -1
- package/dist/lib/markdown.js +5 -116
- package/dist/lib/markdown.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +2 -2
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/lib/skill-eval-fixture.d.ts +6 -4
- package/dist/lib/skill-eval-fixture.d.ts.map +1 -1
- package/dist/lib/skill-eval-fixture.js +25 -18
- package/dist/lib/skill-eval-fixture.js.map +1 -1
- package/dist/new-paper.d.ts +5 -11
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +12 -23
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +4 -4
- package/dist/paper-prose.d.ts +17 -0
- package/dist/paper-prose.d.ts.map +1 -0
- package/dist/paper-prose.js +46 -0
- package/dist/paper-prose.js.map +1 -0
- 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/structure.d.ts.map +1 -1
- package/dist/structure.js +4 -3
- package/dist/structure.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/types.d.ts +0 -2
- package/dist/types.d.ts.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 +146 -20
- package/docs/e2e.md +4 -6
- package/docs/prior-art/paper-folder-scaffolding.md +24 -29
- 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/tex/claim-provenance.md +0 -1
- package/docs/rules.md +48 -47
- 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-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.ts +10 -13
- package/eslint-rules/paper-context.ts +2 -2
- package/eslint-rules/paper-typography.test.mjs +17 -85
- package/eslint-rules/paper-typography.ts +22 -98
- package/eslint-rules/papers.harness.mjs +1 -1
- package/eslint-rules/papers.ts +2 -4
- package/eslint-rules/tex-build.harness.mjs +2 -2
- package/eslint-rules/tex-build.ts +8 -25
- package/eslint.config.mjs +41 -2
- package/hooks/hooks.harness.ts +15 -15
- package/hooks/paper-edit-guard.hook.mjs +5 -8
- package/hooks/paper-edit-guard.test.ts +12 -3
- package/hooks/paper-skills-nudge.hook.mjs +2 -3
- package/hooks/paper-skills-nudge.test.ts +13 -3
- package/hooks/paper-status-gates.hook.mjs +2 -2
- package/hooks/paper-status-gates.test.ts +14 -2
- package/lib/markdown.test.mjs +5 -60
- package/lib/markdown.ts +9 -136
- package/lib/paper-config.ts +3 -2
- package/lib/skill-checks.test.mjs +4 -1
- package/lib/skill-eval-fixture.ts +25 -18
- package/package.json +1 -3
- 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/realm.jsonc +1 -1
- package/presets/venue-profile.schema.json +39 -0
- package/scripts/check.ts +4 -0
- package/scripts/harness-api.frozen.json +0 -5
- package/scripts/refresh-deadlines.test.ts +431 -0
- package/scripts/refresh-deadlines.ts +387 -0
- package/skills/cold-read-diff/SKILL.md +3 -3
- package/skills/cold-read-diff/SKILL.md.spec.ts +2 -2
- package/skills/grade-paper-writing/SKILL.md +14 -22
- package/skills/grade-paper-writing/SKILL.md.spec.ts +13 -21
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +40 -26
- package/skills/grade-paper-writing/prose-lint.mjs +108 -278
- package/skills/grade-paper-writing/prose-lint.test.ts +184 -81
- package/skills/paper-adversarial-review/SKILL.md +2 -2
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
- package/skills/paper-pipeline/SKILL.md +9 -12
- package/skills/paper-pipeline/SKILL.md.spec.ts +8 -11
- package/skills/paper-pipeline/description-language.eval.mjs +25 -15
- package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +25 -15
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +25 -15
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +27 -16
- package/skills/paper-pipeline/pipeline-language.eval.mjs +25 -15
- package/skills/paper-pipeline/references/acceptance-gate.md +16 -20
- package/skills/paper-pipeline/references/artifact-checklist.md +4 -5
- package/skills/paper-pipeline/references/pipeline-status-template.md +1 -1
- package/skills/paper-pipeline/scripts/README.md +0 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +8 -72
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +48 -156
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +81 -75
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +3 -3
- package/skills/paper-status/SKILL.md +2 -2
- package/skills/paper-status/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/SKILL.md +2 -2
- package/skills/render-paper/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/extract-pdf-facts.mjs +2 -2
- package/skills/tighten-paper/SKILL.md +29 -42
- package/skills/tighten-paper/SKILL.md.spec.ts +28 -41
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +0 -13
- package/skills/verify-citations/scripts/bib-authors.cli.test.ts +23 -11
- package/skills/verify-citations/scripts/bib-authors.mjs +8 -93
- package/skills/verify-citations/scripts/bib-authors.test.mjs +1 -44
- 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/build.harness.mjs +2 -2
- package/src/build.test.ts +1 -1
- package/src/build.ts +1 -6
- package/src/cli.harness.mjs +26 -46
- package/src/cli.test.ts +41 -40
- package/src/cli.ts +57 -58
- 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/folder-venue-rule.test.ts +1 -1
- package/src/init.test.ts +6 -7
- package/src/init.ts +3 -22
- package/src/lint-fix.test.ts +10 -4
- package/src/new-paper.harness.mjs +9 -28
- package/src/new-paper.test.ts +7 -6
- package/src/new-paper.ts +11 -30
- package/src/paper-files.test.ts +5 -4
- package/src/paper-files.ts +4 -4
- package/src/paper-prose.test.ts +49 -0
- package/src/paper-prose.ts +67 -0
- package/src/paper-settings.test.ts +167 -0
- package/src/paper-settings.ts +85 -11
- package/src/parse-args.test.ts +0 -1
- 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/structure.harness.ts +5 -11
- package/src/structure.ts +4 -3
- 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/types.ts +0 -2
- package/src/venue-rules.test.ts +82 -0
- package/src/venue-rules.ts +44 -12
- package/fixtures/real-markdown-paper/baseline.json +0 -22
- package/fixtures/real-markdown-paper/baseline.ts +0 -70
- package/skills/paper-pipeline/PIPELINE-MAP.md +0 -381
- package/skills/paper-pipeline/pipeline-firing.baseline.json +0 -270
- package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +0 -585
- package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +0 -492
- package/skills/paper-pipeline/scripts/artifact-coverage.mjs +0 -397
- package/skills/paper-pipeline/scripts/artifact-coverage.test.ts +0 -86
- package/skills/paper-pipeline/scripts/check-provenance.mjs +0 -184
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +0 -135
- package/skills/paper-pipeline/scripts/population-map.mjs +0 -188
- package/skills/paper-pipeline/scripts/population-map.test.mjs +0 -154
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +0 -266
- package/skills/paper-pipeline/scripts/round-diff.harness.ts +0 -907
- package/skills/paper-pipeline/scripts/round-diff.mjs +0 -583
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +0 -159
- package/skills/tighten-paper/__snapshots__/structure.test.ts.snap +0 -133
- package/skills/tighten-paper/structure.mjs +0 -364
- package/skills/tighten-paper/structure.test.ts +0 -117
- package/templates/paper/paper.md +0 -4
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>>;
|
|
@@ -476,6 +480,28 @@ test("🔴 the file and key to set: no portal in the preset, no submission in th
|
|
|
476
480
|
const noExtends = files({ paper: { submission: { id: 7 } } });
|
|
477
481
|
const t3 = submissionTarget(PAPER, deps(noExtends));
|
|
478
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
|
+
);
|
|
479
505
|
const broken = files({
|
|
480
506
|
paper: { extends: "../../venue.jsonc", submission: { id: "7" } },
|
|
481
507
|
});
|
|
@@ -534,3 +560,81 @@ test("usage: a subcommand is required, one paper folder, update-only flags refus
|
|
|
534
560
|
);
|
|
535
561
|
assert.equal(usageProblem({ ...ARGS, sub: "update", save: true }), null);
|
|
536
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
|
+
});
|
package/src/submission.ts
CHANGED
|
@@ -30,7 +30,19 @@ import {
|
|
|
30
30
|
} from "./domain/submission.ts";
|
|
31
31
|
import type { Files } from "./ports/files.ts";
|
|
32
32
|
import type { SubmissionPortal } from "./ports/submission-portal.ts";
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
paperPreset,
|
|
35
|
+
unusablePresetLine,
|
|
36
|
+
type PaperPreset,
|
|
37
|
+
type PresetDeps,
|
|
38
|
+
} from "./presets.ts";
|
|
39
|
+
import type { PaperSettings } from "./paper-settings.ts";
|
|
40
|
+
import { currentDeadlines } from "./domain/cycle.ts";
|
|
41
|
+
import {
|
|
42
|
+
deadlinesInForce,
|
|
43
|
+
type InForce,
|
|
44
|
+
type Observation,
|
|
45
|
+
} from "./domain/deadline.ts";
|
|
34
46
|
import { CONFIG_FILE } from "#lib/paper-config";
|
|
35
47
|
|
|
36
48
|
/** The environment variable each portal kind's token is read from. */
|
|
@@ -82,22 +94,29 @@ export interface SubmissionTarget {
|
|
|
82
94
|
readonly pdf: string;
|
|
83
95
|
/** The venue's call for papers (the preset's `url`); null when the preset names no venue. */
|
|
84
96
|
readonly call: string | null;
|
|
97
|
+
/**
|
|
98
|
+
* The deadlines in force for the current attempt: the paper's own entries (with `cycles`) over the
|
|
99
|
+
* venue preset's readings. Instants and where each came from — never how long is left.
|
|
100
|
+
*/
|
|
101
|
+
readonly deadlines: readonly InForce[];
|
|
85
102
|
}
|
|
86
103
|
|
|
87
104
|
const configOf = (dir: string): string => join(dir, CONFIG_FILE);
|
|
88
105
|
|
|
89
|
-
/**
|
|
90
|
-
|
|
106
|
+
/** Why a paper without a venue preset has no portal: the key to set, or — with cycles — where. */
|
|
107
|
+
const noPresetLine = (
|
|
91
108
|
paperDir: string,
|
|
92
|
-
|
|
109
|
+
settings: PaperSettings | null,
|
|
110
|
+
): string =>
|
|
111
|
+
settings === null || settings.cycles === null
|
|
112
|
+
? `${configOf(paperDir)}: no "extends" — the venue preset it names declares the portal ("portal": { "kind": …, "url": … })`
|
|
113
|
+
: `${configOf(paperDir)}: 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`;
|
|
114
|
+
|
|
115
|
+
/** A resolved paper → its portal and submission, or the line naming the file and key to set. */
|
|
116
|
+
function targetOf(
|
|
117
|
+
paperDir: string,
|
|
118
|
+
p: Extract<PaperPreset, { readonly kind: "resolved" }>,
|
|
93
119
|
): Result<SubmissionTarget, string> {
|
|
94
|
-
const p = paperPreset(paperDir, deps);
|
|
95
|
-
const problem = paperPresetProblem(paperDir, p);
|
|
96
|
-
if (problem !== null) return err(problem);
|
|
97
|
-
if (p.kind !== "resolved")
|
|
98
|
-
return err(
|
|
99
|
-
`${configOf(paperDir)}: no "extends" — the venue preset it names declares the portal ("portal": { "kind": …, "url": … })`,
|
|
100
|
-
);
|
|
101
120
|
const declared = p.preset.portal;
|
|
102
121
|
if (declared === null)
|
|
103
122
|
return err(
|
|
@@ -113,7 +132,29 @@ export function submissionTarget(
|
|
|
113
132
|
const pdf = join(paperDir, p.settings.pdf ?? "paper.pdf");
|
|
114
133
|
const identity = p.preset.identity;
|
|
115
134
|
const call = identity.type === "venue" ? identity.url : null;
|
|
116
|
-
|
|
135
|
+
const own =
|
|
136
|
+
p.settings.cycles === null
|
|
137
|
+
? []
|
|
138
|
+
: currentDeadlines(p.settings.cycles.current);
|
|
139
|
+
const deadlines = deadlinesInForce(own, p.preset.deadlines);
|
|
140
|
+
return ok({ portal: portal.value, id: sub.id, pdf, call, deadlines });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The paper directory → its portal and submission, or the line naming the file and key to set. */
|
|
144
|
+
export function submissionTarget(
|
|
145
|
+
paperDir: string,
|
|
146
|
+
deps: PresetDeps,
|
|
147
|
+
): Result<SubmissionTarget, string> {
|
|
148
|
+
const p = paperPreset(paperDir, deps);
|
|
149
|
+
switch (p.kind) {
|
|
150
|
+
case "none":
|
|
151
|
+
return err(noPresetLine(paperDir, p.settings));
|
|
152
|
+
case "settings-problem":
|
|
153
|
+
case "preset-problem":
|
|
154
|
+
return err(unusablePresetLine(paperDir, p));
|
|
155
|
+
case "resolved":
|
|
156
|
+
return targetOf(paperDir, p);
|
|
157
|
+
}
|
|
117
158
|
}
|
|
118
159
|
|
|
119
160
|
/** The token from the environment, or the line naming the variable and where to get one. */
|
|
@@ -166,6 +207,25 @@ const pdfLine = (view: SubmissionView): string =>
|
|
|
166
207
|
? "portal PDF none"
|
|
167
208
|
: `portal PDF ${view.pdf.hash}, ${view.pdf.size === null ? "?" : String(view.pdf.size)} bytes, uploaded ${iso(view.pdf.uploadedAt)}`;
|
|
168
209
|
|
|
210
|
+
const readingText = (o: Observation): string =>
|
|
211
|
+
`${o.source} ${o.at}, read ${o.read}`;
|
|
212
|
+
|
|
213
|
+
/** One deadline in force, what set it, and the other readings of it. Pure. */
|
|
214
|
+
export function deadlineLine(d: InForce): string {
|
|
215
|
+
const by = d.by;
|
|
216
|
+
const why =
|
|
217
|
+
by.kind === "override"
|
|
218
|
+
? `override: ${by.override.reason} (${by.override.evidence})`
|
|
219
|
+
: `${by.reading.source}, read ${by.reading.read}`;
|
|
220
|
+
const others = d.readings.filter(
|
|
221
|
+
(r) => by.kind === "override" || r !== by.reading,
|
|
222
|
+
);
|
|
223
|
+
const also = others.length
|
|
224
|
+
? `; also ${others.map(readingText).join("; ")}`
|
|
225
|
+
: "";
|
|
226
|
+
return `deadline ${d.what} ${d.at} — ${why}${also}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
169
229
|
/** `show`'s report. Pure. */
|
|
170
230
|
export function showLines(
|
|
171
231
|
t: SubmissionTarget,
|
|
@@ -182,6 +242,7 @@ export function showLines(
|
|
|
182
242
|
pdfLine(view),
|
|
183
243
|
`local PDF ${local.shown} ${local.sha256 === null ? "(missing)" : `sha2-${local.sha256}`}`,
|
|
184
244
|
matchLine(view, local.sha256),
|
|
245
|
+
...t.deadlines.map(deadlineLine),
|
|
185
246
|
...view.messages.map(messageLine),
|
|
186
247
|
];
|
|
187
248
|
}
|
package/src/tex-requirements.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
type VenueTalkJson,
|
|
36
36
|
} from "./domain/talk.ts";
|
|
37
37
|
import type { VenuePortal } from "./domain/submission.ts";
|
|
38
|
+
import type { Reading } from "./domain/deadline.ts";
|
|
38
39
|
|
|
39
40
|
/** CTAN package name → the names that prove it is installed. */
|
|
40
41
|
export type PackageProofs = Readonly<Record<string, readonly string[]>>;
|
|
@@ -219,6 +220,8 @@ export interface PresetFile {
|
|
|
219
220
|
readonly talk: VenueTalk | null;
|
|
220
221
|
/** Where the venue takes submissions (`paperlint submission`); null when the file does not say. */
|
|
221
222
|
readonly portal: VenuePortal | null;
|
|
223
|
+
/** The edition's deadlines as they were read; null when the file names none (a child's list replaces its parent's). */
|
|
224
|
+
readonly deadlines: readonly Reading[] | null;
|
|
222
225
|
}
|
|
223
226
|
|
|
224
227
|
type KindsJson = Readonly<
|
|
@@ -276,6 +279,7 @@ interface PresetBodyJson {
|
|
|
276
279
|
};
|
|
277
280
|
readonly talk?: VenueTalkJson;
|
|
278
281
|
readonly portal?: VenuePortal;
|
|
282
|
+
readonly deadlines?: readonly Reading[];
|
|
279
283
|
}
|
|
280
284
|
|
|
281
285
|
/** An optional field as the typed preset holds it: absent is null. */
|
|
@@ -394,6 +398,32 @@ const anchorsOf = (
|
|
|
394
398
|
const portalOf = (p: PresetJson["portal"]): Readonly<VenuePortal> | null =>
|
|
395
399
|
p === undefined ? null : { kind: p.kind, url: p.url };
|
|
396
400
|
|
|
401
|
+
/**
|
|
402
|
+
* A preset's `deadlines`, or null when it names none. Two readings of one kind from one source are
|
|
403
|
+
* refused: the schema cannot say it, and a paper could not tell which binds.
|
|
404
|
+
*/
|
|
405
|
+
function deadlinesOf(
|
|
406
|
+
d: PresetJson["deadlines"],
|
|
407
|
+
file: string,
|
|
408
|
+
): readonly Reading[] | null {
|
|
409
|
+
if (d === undefined) return null;
|
|
410
|
+
const twice = d.find(
|
|
411
|
+
(r, i) =>
|
|
412
|
+
d.findIndex((x) => x.what === r.what && x.source === r.source) !== i,
|
|
413
|
+
);
|
|
414
|
+
if (twice !== undefined)
|
|
415
|
+
throw new Error(
|
|
416
|
+
`${file}: "deadlines" holds two "${twice.source}" readings of "${twice.what}" — keep one, the latest`,
|
|
417
|
+
);
|
|
418
|
+
return d.map(({ what, at, source, url, read }) => ({
|
|
419
|
+
what,
|
|
420
|
+
at,
|
|
421
|
+
source,
|
|
422
|
+
url,
|
|
423
|
+
read,
|
|
424
|
+
}));
|
|
425
|
+
}
|
|
426
|
+
|
|
397
427
|
/**
|
|
398
428
|
* The text of one preset file → the typed file, or an Error naming every problem. The ONE parser
|
|
399
429
|
* of a preset: the toolchain reads its `tex`, the venue rules its `format`, the config its `rules`.
|
|
@@ -429,6 +459,7 @@ export function parsePreset(
|
|
|
429
459
|
registerAnchors: anchorsOf(j.register),
|
|
430
460
|
talk: j.talk === undefined ? null : venueTalkOf(j.talk),
|
|
431
461
|
portal: portalOf(j.portal),
|
|
462
|
+
deadlines: deadlinesOf(j.deadlines, file),
|
|
432
463
|
};
|
|
433
464
|
}
|
|
434
465
|
|
|
@@ -10,6 +10,7 @@ import { memoryFiles } from "./adapters/memory/index.ts";
|
|
|
10
10
|
import { presetsDir } from "./package-dirs.ts";
|
|
11
11
|
import { latexReader } from "./adapters/latex/index.ts";
|
|
12
12
|
import {
|
|
13
|
+
cyclePortRules,
|
|
13
14
|
judgeRequiredSections,
|
|
14
15
|
judgeTemplate,
|
|
15
16
|
otherVenues,
|
|
@@ -77,6 +78,9 @@ function lint(
|
|
|
77
78
|
...Object.entries(formatRules(deps)).map(
|
|
78
79
|
([n, r]) => [`format/${n}`, r] as const,
|
|
79
80
|
),
|
|
81
|
+
...Object.entries(cyclePortRules(deps)).map(
|
|
82
|
+
([n, r]) => [`cycle/${n}`, r] as const,
|
|
83
|
+
),
|
|
80
84
|
]) {
|
|
81
85
|
const visitor = rule.create({
|
|
82
86
|
filename,
|
|
@@ -302,6 +306,78 @@ describe("tex/template — the shipped ACM and ACL templates", () => {
|
|
|
302
306
|
});
|
|
303
307
|
});
|
|
304
308
|
|
|
309
|
+
const PORTING = {
|
|
310
|
+
id: "aidc-2026",
|
|
311
|
+
venue: { kind: "preset", extends: "paperlint:aidc" },
|
|
312
|
+
kind: "regular",
|
|
313
|
+
opened: "2026-09-26",
|
|
314
|
+
phase: "porting",
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
describe("tex/template — a cycle in `porting`", () => {
|
|
318
|
+
it("the class line is the previous venue's by declaration: silent, where the flat form reports", () => {
|
|
319
|
+
const src = paper("\\documentclass[sigconf]{acmart}");
|
|
320
|
+
expect(lint(src, AIDC).map((f) => f.messageId)).toEqual(["wrongClass"]);
|
|
321
|
+
expect(lint(src, { cycles: [PORTING] })).toEqual([]);
|
|
322
|
+
expect(
|
|
323
|
+
lint(src, { cycles: [{ ...PORTING, phase: "prepared" }] }).map(
|
|
324
|
+
(f) => f.messageId,
|
|
325
|
+
),
|
|
326
|
+
).toEqual(["wrongClass"]);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe("cycle/port-done — the declared port is finished", () => {
|
|
331
|
+
it("porting declared and the class line already the venue's: the one finding, naming the cycle", () => {
|
|
332
|
+
const fs = lint(OFFICIAL, { cycles: [PORTING] });
|
|
333
|
+
expect(fs.map((f) => `${f.rule}:${f.messageId}`)).toEqual([
|
|
334
|
+
"cycle/port-done:done",
|
|
335
|
+
]);
|
|
336
|
+
expect(fs[0]?.message).toMatch(
|
|
337
|
+
/already `\\documentclass\[conference,compsoc\]\{IEEEtran\}`, which AIDC requires, and the cycle «aidc-2026» still declares "phase": "porting"/,
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("silent while the class line is still the old venue's, once the phase is dropped, and on the flat form", () => {
|
|
342
|
+
const old = paper("\\documentclass[sigconf]{acmart}");
|
|
343
|
+
expect(lint(old, { cycles: [PORTING] })).toEqual([]);
|
|
344
|
+
expect(
|
|
345
|
+
lint(OFFICIAL, { cycles: [{ ...PORTING, phase: "prepared" }] }),
|
|
346
|
+
).toEqual([]);
|
|
347
|
+
expect(lint(OFFICIAL, AIDC)).toEqual([]);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("judges the text when the source carries no `raw`", () => {
|
|
351
|
+
expect(
|
|
352
|
+
lint(OFFICIAL, { cycles: [PORTING] }, { raw: false }).map(
|
|
353
|
+
(f) => `${f.rule}:${f.messageId}`,
|
|
354
|
+
),
|
|
355
|
+
).toContain("cycle/port-done:done");
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("silent when the venue names no template, and when the preset does not resolve", () => {
|
|
359
|
+
const own = `${PAPER}/own.jsonc`;
|
|
360
|
+
const porting = (ext: string) => ({
|
|
361
|
+
cycles: [{ ...PORTING, venue: { kind: "preset", extends: ext } }],
|
|
362
|
+
});
|
|
363
|
+
expect(
|
|
364
|
+
lint(OFFICIAL, porting("./own.jsonc"), {
|
|
365
|
+
extra: {
|
|
366
|
+
[own]: JSON.stringify({
|
|
367
|
+
type: "family",
|
|
368
|
+
tex: { packages: { x: ["x"] } },
|
|
369
|
+
}),
|
|
370
|
+
},
|
|
371
|
+
}).filter((f) => f.rule === "cycle/port-done"),
|
|
372
|
+
).toEqual([]);
|
|
373
|
+
expect(
|
|
374
|
+
lint(OFFICIAL, porting("./missing.jsonc")).filter(
|
|
375
|
+
(f) => f.rule === "cycle/port-done",
|
|
376
|
+
),
|
|
377
|
+
).toEqual([]);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
305
381
|
describe("tex/template — a project's own preset, and nothing to judge", () => {
|
|
306
382
|
it("a preset with no template: nothing to compare, no finding", () => {
|
|
307
383
|
const own = `${PAPER}/own.jsonc`;
|
package/src/tex-venue-rules.ts
CHANGED
|
@@ -52,6 +52,7 @@ import type { PaperSource } from "./domain/paper-source.ts";
|
|
|
52
52
|
import { readPaper, reportInPaper } from "./tex-paper.ts";
|
|
53
53
|
import type { RequiredSection } from "./tex-requirements.ts";
|
|
54
54
|
import type { Finding, VenueRuleDeps } from "./venue-rules.ts";
|
|
55
|
+
import { isPorting } from "./domain/cycle.ts";
|
|
55
56
|
|
|
56
57
|
/** A finding and where it points; null points at the top of the file. */
|
|
57
58
|
export interface Located extends Finding {
|
|
@@ -493,6 +494,10 @@ function readingOf(
|
|
|
493
494
|
const dir = dirname(filename);
|
|
494
495
|
const p = paperPreset(dir, deps);
|
|
495
496
|
if (p.kind !== "resolved") return null;
|
|
497
|
+
// The port to this venue's template is declared open work: the class line is the previous
|
|
498
|
+
// venue's by declaration, and `cycle/port-done` watches that declaration.
|
|
499
|
+
if (p.settings.cycles !== null && isPorting(p.settings.cycles.current))
|
|
500
|
+
return null;
|
|
496
501
|
const preset = p.preset;
|
|
497
502
|
const others = () => otherVenues(preset, deps);
|
|
498
503
|
const paper = readPaper(filename, src, deps);
|
|
@@ -538,3 +543,83 @@ export function formatRules(
|
|
|
538
543
|
): Readonly<Record<"layout-override", TexRuleModule>> {
|
|
539
544
|
return { "layout-override": rule("layout-override", deps) };
|
|
540
545
|
}
|
|
546
|
+
|
|
547
|
+
// ── cycle/port-done: the declared port is finished ───────────────────────────────────────
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* `cycle/port-done` (error): the current cycle declares `"phase": "porting"`, and the paper's
|
|
551
|
+
* `\documentclass` is already the venue's template. The declaration silences every venue rule
|
|
552
|
+
* (`src/venue-rules.ts`, `readingOf` above); once the class line is the venue's, the port is done
|
|
553
|
+
* and the declaration is stale — this is the other half that keeps `porting` from being a mute
|
|
554
|
+
* button. Silent when the preset names no template: there is nothing to compare, as for `tex/template`.
|
|
555
|
+
*/
|
|
556
|
+
export function judgePortDone(
|
|
557
|
+
r: Pick<Reading, "src" | "latex" | "preset">,
|
|
558
|
+
cycleId: string,
|
|
559
|
+
): readonly Located[] {
|
|
560
|
+
if (r.preset.template === null) return [];
|
|
561
|
+
const mismatch = judgeTemplate(
|
|
562
|
+
r.latex.documentClass(r.src),
|
|
563
|
+
r.preset,
|
|
564
|
+
r.latex,
|
|
565
|
+
);
|
|
566
|
+
return mismatch.length > 0
|
|
567
|
+
? []
|
|
568
|
+
: [
|
|
569
|
+
{
|
|
570
|
+
messageId: "done",
|
|
571
|
+
data: {
|
|
572
|
+
venue: r.preset.label,
|
|
573
|
+
template: r.preset.template.text,
|
|
574
|
+
id: cycleId,
|
|
575
|
+
},
|
|
576
|
+
at: null,
|
|
577
|
+
},
|
|
578
|
+
];
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const PORT_DONE_META: TexRuleModule["meta"] = {
|
|
582
|
+
type: "problem",
|
|
583
|
+
docs: {
|
|
584
|
+
description:
|
|
585
|
+
"the current cycle still says `porting` while the \\documentclass is already the venue's template — the port is done, drop the phase",
|
|
586
|
+
url: "https://github.com/zernie/paperlint/blob/main/docs/rules/cycle/port-done.md",
|
|
587
|
+
},
|
|
588
|
+
schema: [],
|
|
589
|
+
messages: {
|
|
590
|
+
done: 'the \\documentclass is already `{{template}}`, which {{venue}} requires, and the cycle «{{id}}» still declares "phase": "porting" — the port is done: remove the phase, so the venue\'s format rules judge this paper again',
|
|
591
|
+
},
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
/** The `cycle` plugin's rule over the source, for every `paper.tex`. */
|
|
595
|
+
export function cyclePortRules(
|
|
596
|
+
deps: TexVenueRuleDeps,
|
|
597
|
+
): Readonly<Record<"port-done", TexRuleModule>> {
|
|
598
|
+
return {
|
|
599
|
+
"port-done": {
|
|
600
|
+
meta: PORT_DONE_META,
|
|
601
|
+
create(context) {
|
|
602
|
+
if (basename(context.filename) !== "paper.tex") return {};
|
|
603
|
+
return {
|
|
604
|
+
root() {
|
|
605
|
+
const p = paperPreset(dirname(context.filename), deps);
|
|
606
|
+
if (p.kind !== "resolved") return;
|
|
607
|
+
const current = p.settings.cycles?.current;
|
|
608
|
+
if (current?.kind !== "cycle" || !isPorting(current)) return;
|
|
609
|
+
const sc = context.sourceCode;
|
|
610
|
+
const paper = readPaper(context.filename, sc.raw ?? sc.text, deps);
|
|
611
|
+
const found = judgePortDone(
|
|
612
|
+
{ src: paper.text, latex: deps.latex, preset: p.preset },
|
|
613
|
+
current.cycle.id,
|
|
614
|
+
);
|
|
615
|
+
reportInPaper(context, PORT_DONE_META.messages, paper, found);
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export const CYCLE_PORT_RULE_LEVELS: Readonly<
|
|
624
|
+
Record<"cycle/port-done", "error">
|
|
625
|
+
> = { "cycle/port-done": "error" };
|
package/src/types.ts
CHANGED
|
@@ -62,8 +62,6 @@ export interface Args {
|
|
|
62
62
|
noHooks: boolean;
|
|
63
63
|
/** `--paper <name>`: `init` creates this paper. */
|
|
64
64
|
paper: string | null;
|
|
65
|
-
/** `--format tex|md` for `new` and `init --paper`. Validated by the command, not here. */
|
|
66
|
-
format: string | null;
|
|
67
65
|
/** `--venue <preset>` for `new`: a shipped preset name, or a path to the project's own. */
|
|
68
66
|
venue: string | null;
|
|
69
67
|
/** `--kind <kind>` for `new`: one of the venue preset's kinds. */
|
package/src/venue-rules.test.ts
CHANGED
|
@@ -231,6 +231,36 @@ describe("a paper that names no venue", () => {
|
|
|
231
231
|
lint({ venue: DECL, facts: null }, `${PAPER}/PIPELINE-STATUS.md`),
|
|
232
232
|
).toEqual([]);
|
|
233
233
|
});
|
|
234
|
+
|
|
235
|
+
it.each([
|
|
236
|
+
[
|
|
237
|
+
"a named venue",
|
|
238
|
+
{
|
|
239
|
+
id: "secdev-2027",
|
|
240
|
+
venue: { kind: "named", name: "SecDev 2027", url: "https://x.example" },
|
|
241
|
+
opened: "2027-01-10",
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
[
|
|
245
|
+
"the last cycle closed, none open",
|
|
246
|
+
{
|
|
247
|
+
id: "first-2026",
|
|
248
|
+
venue: { kind: "preset", extends: "paperlint:aisec" },
|
|
249
|
+
opened: "2026-07-01",
|
|
250
|
+
outcome: { kind: "withdrawn", date: "2026-07-18", evidence: "n.md" },
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
])(
|
|
254
|
+
"with `cycles` and %s: the warning says to set the venue IN THE CYCLE — `extends` beside `cycles` is refused",
|
|
255
|
+
(_, cycle) => {
|
|
256
|
+
const fs = lint({ venue: { cycles: [cycle] }, facts: null, pdf: null });
|
|
257
|
+
expect(ids(fs)).toEqual(["pdf/measured:noPresetCycle"]);
|
|
258
|
+
expect(fs[0]?.message).toMatch(
|
|
259
|
+
/^this paper's current cycle names no venue preset, so its page limit, fonts and format are not checked — set the venue in the current cycle in .*paperlint\.json \("venue": \{ "kind": "preset", "extends": "paperlint:…" \}\), or open a new cycle when the last one has ended$/,
|
|
260
|
+
);
|
|
261
|
+
expect(fs[0]?.message).not.toMatch(/set "extends"/);
|
|
262
|
+
},
|
|
263
|
+
);
|
|
234
264
|
});
|
|
235
265
|
|
|
236
266
|
describe("pdf/profile — the declaration must resolve, or nothing is judged", () => {
|
|
@@ -1193,3 +1223,55 @@ describe("judgeBodyEnd, on a venue it does not apply to", () => {
|
|
|
1193
1223
|
expect(judgeBodyEnd(NO_TEXT, venue(null))).toEqual([]);
|
|
1194
1224
|
});
|
|
1195
1225
|
});
|
|
1226
|
+
|
|
1227
|
+
/** A paper on AIDC through `cycles`: one attempt, open unless `outcome` says otherwise. */
|
|
1228
|
+
const onCycle = (cycle: Record<string, unknown>) => ({
|
|
1229
|
+
identity: ["Ada Example"],
|
|
1230
|
+
cycles: [
|
|
1231
|
+
{
|
|
1232
|
+
id: "aidc-2026",
|
|
1233
|
+
venue: { kind: "preset", extends: "paperlint:aidc" },
|
|
1234
|
+
kind: "regular",
|
|
1235
|
+
opened: "2026-09-26",
|
|
1236
|
+
...cycle,
|
|
1237
|
+
},
|
|
1238
|
+
],
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
describe("the cycle decides what applies", () => {
|
|
1242
|
+
it("an open cycle reads as the flat form: the same findings on the same facts", () => {
|
|
1243
|
+
const facts = said((f) => (f.columns = 1));
|
|
1244
|
+
expect(ids(lint({ venue: onCycle({}), facts }))).toEqual(
|
|
1245
|
+
ids(lint({ venue: BLIND, facts })),
|
|
1246
|
+
);
|
|
1247
|
+
expect(ids(lint({ venue: onCycle({}), facts }))).toContain(
|
|
1248
|
+
"pdf/geometry:columns",
|
|
1249
|
+
);
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
it("`phase: porting` — the source is in the previous venue's template by declaration: every venue rule is silent", () => {
|
|
1253
|
+
const facts = said(
|
|
1254
|
+
(f) => ((f.columns = 1), (f.pages_text = ["Ada Example\n"])),
|
|
1255
|
+
);
|
|
1256
|
+
expect(lint({ venue: onCycle({ phase: "porting" }), facts })).toEqual([]);
|
|
1257
|
+
expect(lint({ venue: onCycle({ phase: "porting" }), facts: null })).toEqual(
|
|
1258
|
+
[],
|
|
1259
|
+
);
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
it("an ACCEPTED cycle at a blind venue: the camera-ready carries the authors, anonymity/identity is silent", () => {
|
|
1263
|
+
const facts = said(
|
|
1264
|
+
(f) =>
|
|
1265
|
+
(f.pages_text = ["Ada Example\n", ...bodyPages(11), "References\n"]),
|
|
1266
|
+
);
|
|
1267
|
+
const accepted = {
|
|
1268
|
+
kind: "accepted",
|
|
1269
|
+
date: "2026-10-16",
|
|
1270
|
+
evidence: "reviews/decision.md",
|
|
1271
|
+
};
|
|
1272
|
+
expect(ids(lint({ venue: onCycle({}), facts }))).toContain(
|
|
1273
|
+
"anonymity/identity:leak",
|
|
1274
|
+
);
|
|
1275
|
+
expect(lint({ venue: onCycle({ outcome: accepted }), facts })).toEqual([]);
|
|
1276
|
+
});
|
|
1277
|
+
});
|