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
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE CYCLE — one attempt of a paper at one venue: where it goes, as what kind, when the attempt was
|
|
3
|
+
* decided, the venue's deadlines as they were read, which submission on the portal is this paper's,
|
|
4
|
+
* and how the attempt ended. A paper's `paperlint.json` holds them as a LIST, `cycles`, oldest first:
|
|
5
|
+
*
|
|
6
|
+
* {
|
|
7
|
+
* "id": "beta-2027",
|
|
8
|
+
* "venue": { "kind": "preset", "extends": "paperlint:beta" }, // or { "kind": "named", "name", "url" }
|
|
9
|
+
* "kind": "technical",
|
|
10
|
+
* "opened": "2026-09-09",
|
|
11
|
+
* "phase": "porting", // default "prepared"
|
|
12
|
+
* "deadlines": [{ "what": "submission", "observed": [{ "at": "2026-10-23 AoE", "source": "call", "url": "…", "read": "2026-09-09" }] }],
|
|
13
|
+
* "submission": { "id": 7 },
|
|
14
|
+
* "outcome": { "kind": "open" }
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* ── WHY A LIST OF ATTEMPTS, NOT ONE VENUE ───────────────────────────────────────────────
|
|
18
|
+
* A paper goes to a venue, is rejected, and goes to the next one; the settings that every reader
|
|
19
|
+
* takes from the file — `extends`, `kind`, `submission` — are the CURRENT attempt's, and change
|
|
20
|
+
* together when the attempt changes. Held flat, they lag the decision (the preset is switched "when
|
|
21
|
+
* the port lands", weeks later) and the decision itself has no field, so it is written as a comment.
|
|
22
|
+
* Here it is a record: the new attempt is a new entry, the old one gets its `outcome`.
|
|
23
|
+
*
|
|
24
|
+
* ── THE PHASE, and why it is DECLARED ────────────────────────────────────────────────────
|
|
25
|
+
* The day a new venue is chosen, the source is still in the old venue's template. The venue's format
|
|
26
|
+
* checks are then not wrong, but not applicable: a page count under the wrong template says nothing
|
|
27
|
+
* about the new limit. `"phase": "porting"` says the port is open work; the format rules are silent
|
|
28
|
+
* under it, and `cycle/port-done` fires when the class line already matches the venue's template
|
|
29
|
+
* and the phase still says porting. The declaration cannot lie for long in either direction, and it
|
|
30
|
+
* is a record of work, not a softer severity. Absent, the phase is `prepared`.
|
|
31
|
+
*
|
|
32
|
+
* ── THE DEADLINES ARE READINGS ───────────────────────────────────────────────────────────
|
|
33
|
+
* Each deadline records what the portal and the call said, where and when, and an optional human
|
|
34
|
+
* override; the earlier reading binds unless overridden (`deadline.ts`). For a venue with a preset,
|
|
35
|
+
* the portal's readings come from the preset — updated with the package — and the paper adds the
|
|
36
|
+
* call's readings and its overrides on top.
|
|
37
|
+
*
|
|
38
|
+
* Parsed by hand: the domain imports no schema library (see `src/CLAUDE.md`).
|
|
39
|
+
*/
|
|
40
|
+
import { err, ok, type Result } from "./result.ts";
|
|
41
|
+
import { fieldOf, isRecord } from "./record.ts";
|
|
42
|
+
import {
|
|
43
|
+
isIsoDate,
|
|
44
|
+
parseDeadlines,
|
|
45
|
+
unknownKey,
|
|
46
|
+
type Deadline,
|
|
47
|
+
type IsoDate,
|
|
48
|
+
} from "./deadline.ts";
|
|
49
|
+
|
|
50
|
+
/** The venue of an attempt: a preset paperlint resolves, or a venue named by hand (no preset yet). */
|
|
51
|
+
export type CycleVenue =
|
|
52
|
+
| { readonly kind: "preset"; readonly extends: string }
|
|
53
|
+
| { readonly kind: "named"; readonly name: string; readonly url: string };
|
|
54
|
+
|
|
55
|
+
/** How an attempt ended; `open` while it has not. */
|
|
56
|
+
export type Outcome =
|
|
57
|
+
| { readonly kind: "open" }
|
|
58
|
+
| {
|
|
59
|
+
readonly kind: "accepted";
|
|
60
|
+
readonly date: IsoDate;
|
|
61
|
+
readonly evidence: string;
|
|
62
|
+
}
|
|
63
|
+
| {
|
|
64
|
+
readonly kind: "rejected";
|
|
65
|
+
readonly date: IsoDate;
|
|
66
|
+
readonly evidence: string;
|
|
67
|
+
/** A desk rejection (format, scope) — different work follows than after reviews. */
|
|
68
|
+
readonly desk: boolean;
|
|
69
|
+
}
|
|
70
|
+
| {
|
|
71
|
+
readonly kind: "withdrawn";
|
|
72
|
+
readonly date: IsoDate;
|
|
73
|
+
readonly evidence: string;
|
|
74
|
+
};
|
|
75
|
+
export const OUTCOME_KINDS: readonly Outcome["kind"][] = [
|
|
76
|
+
"open",
|
|
77
|
+
"accepted",
|
|
78
|
+
"rejected",
|
|
79
|
+
"withdrawn",
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
export type Phase = "porting" | "prepared";
|
|
83
|
+
|
|
84
|
+
export interface Cycle {
|
|
85
|
+
/** What a stage refers to (`stages[].cycle`): unique in the list. */
|
|
86
|
+
readonly id: string;
|
|
87
|
+
readonly venue: CycleVenue;
|
|
88
|
+
/** A key of the preset's `format.kinds`; null when the venue has one kind or none is chosen. */
|
|
89
|
+
readonly kind: string | null;
|
|
90
|
+
/** The day this attempt was decided: the rows of the scorecard are judged against it. */
|
|
91
|
+
readonly opened: IsoDate;
|
|
92
|
+
readonly phase: Phase;
|
|
93
|
+
readonly deadlines: readonly Deadline[];
|
|
94
|
+
/** Which submission on the venue's portal is this paper's; null before one exists. */
|
|
95
|
+
readonly submission: { readonly id: number } | null;
|
|
96
|
+
readonly outcome: Outcome;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Which attempt the paper is on. An ACCEPTED last cycle is not "parked": the paper lives on at that
|
|
101
|
+
* venue — camera-ready, talk, proceedings — so its venue and kind stay the paper's.
|
|
102
|
+
*/
|
|
103
|
+
export type Current =
|
|
104
|
+
| { readonly kind: "cycle"; readonly cycle: Cycle }
|
|
105
|
+
| { readonly kind: "accepted"; readonly cycle: Cycle }
|
|
106
|
+
/** The last cycle was rejected or withdrawn and none is open: between venues. */
|
|
107
|
+
| { readonly kind: "parked"; readonly last: Cycle }
|
|
108
|
+
/** `cycles: []`. */
|
|
109
|
+
| { readonly kind: "none" };
|
|
110
|
+
|
|
111
|
+
/** Why a list of cycles names no current attempt. */
|
|
112
|
+
export type CycleProblem =
|
|
113
|
+
| { readonly kind: "two-open"; readonly ids: readonly string[] }
|
|
114
|
+
| { readonly kind: "open-not-last"; readonly id: string }
|
|
115
|
+
| { readonly kind: "duplicate-id"; readonly id: string };
|
|
116
|
+
|
|
117
|
+
// ── parsing ────────────────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
const KNOWN_KEYS: readonly string[] = [
|
|
120
|
+
"id",
|
|
121
|
+
"venue",
|
|
122
|
+
"kind",
|
|
123
|
+
"opened",
|
|
124
|
+
"phase",
|
|
125
|
+
"deadlines",
|
|
126
|
+
"submission",
|
|
127
|
+
"outcome",
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
const where = (i: number): string => `cycles[${String(i)}]`;
|
|
131
|
+
|
|
132
|
+
const nonEmpty = (v: unknown): v is string => typeof v === "string" && v !== "";
|
|
133
|
+
|
|
134
|
+
const VENUE_KEYS = {
|
|
135
|
+
preset: ["kind", "extends"],
|
|
136
|
+
named: ["kind", "name", "url"],
|
|
137
|
+
} as const;
|
|
138
|
+
|
|
139
|
+
/** The fields of a venue of `kind`, when they are there. */
|
|
140
|
+
function venueFields(
|
|
141
|
+
kind: CycleVenue["kind"],
|
|
142
|
+
v: Readonly<Record<string, unknown>>,
|
|
143
|
+
): CycleVenue | null {
|
|
144
|
+
const { extends: ext, name, url } = v;
|
|
145
|
+
switch (kind) {
|
|
146
|
+
case "preset":
|
|
147
|
+
return nonEmpty(ext) ? { kind, extends: ext } : null;
|
|
148
|
+
case "named":
|
|
149
|
+
return nonEmpty(name) && nonEmpty(url) ? { kind, name, url } : null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The venue's own keys refused when unknown, then its shape; null when the kind is neither. */
|
|
154
|
+
function venueShape(
|
|
155
|
+
v: Readonly<Record<string, unknown>>,
|
|
156
|
+
at: string,
|
|
157
|
+
): Result<CycleVenue, string> | null {
|
|
158
|
+
const kind = v["kind"];
|
|
159
|
+
if (kind !== "preset" && kind !== "named") return null;
|
|
160
|
+
const unknown = unknownKey(
|
|
161
|
+
v,
|
|
162
|
+
VENUE_KEYS[kind],
|
|
163
|
+
`${at}.venue`,
|
|
164
|
+
`a ${kind} venue's`,
|
|
165
|
+
);
|
|
166
|
+
if (unknown !== null) return err(unknown);
|
|
167
|
+
const venue = venueFields(kind, v);
|
|
168
|
+
return venue === null ? null : ok(venue);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseVenue(v: unknown, at: string): Result<CycleVenue, string> {
|
|
172
|
+
const shaped = isRecord(v) ? venueShape(v, at) : null;
|
|
173
|
+
return (
|
|
174
|
+
shaped ??
|
|
175
|
+
err(
|
|
176
|
+
`${at}: "venue" must be { "kind": "preset", "extends": … } or { "kind": "named", "name": …, "url": … }`,
|
|
177
|
+
)
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const isOutcomeKind = (v: unknown): v is Outcome["kind"] =>
|
|
182
|
+
OUTCOME_KINDS.some((k) => k === v);
|
|
183
|
+
|
|
184
|
+
/** A decision's shape by kind: `rejected` also says whether it was a desk rejection. */
|
|
185
|
+
const decisionOf = (
|
|
186
|
+
kind: Exclude<Outcome["kind"], "open">,
|
|
187
|
+
decided: { readonly date: IsoDate; readonly evidence: string },
|
|
188
|
+
desk: unknown,
|
|
189
|
+
at: string,
|
|
190
|
+
): Result<Outcome, string> => {
|
|
191
|
+
switch (kind) {
|
|
192
|
+
case "accepted":
|
|
193
|
+
return ok({ kind, ...decided });
|
|
194
|
+
case "withdrawn":
|
|
195
|
+
return ok({ kind, ...decided });
|
|
196
|
+
case "rejected":
|
|
197
|
+
return typeof desk === "boolean" || desk === undefined
|
|
198
|
+
? ok({ kind, ...decided, desk: desk === true })
|
|
199
|
+
: err(`${at}: "outcome.desk" must be true or false`);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const OUTCOME_KEYS: Readonly<Record<Outcome["kind"], readonly string[]>> = {
|
|
204
|
+
open: ["kind"],
|
|
205
|
+
accepted: ["kind", "date", "evidence"],
|
|
206
|
+
rejected: ["kind", "date", "evidence", "desk"],
|
|
207
|
+
withdrawn: ["kind", "date", "evidence"],
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
function parseOutcome(v: unknown, at: string): Result<Outcome, string> {
|
|
211
|
+
const kind = fieldOf(v, "kind");
|
|
212
|
+
if (!isRecord(v) || !isOutcomeKind(kind))
|
|
213
|
+
return err(
|
|
214
|
+
`${at}: "outcome" kind must be one of ${OUTCOME_KINDS.join(", ")}, got ${JSON.stringify(kind)}`,
|
|
215
|
+
);
|
|
216
|
+
const unknown = unknownKey(
|
|
217
|
+
v,
|
|
218
|
+
OUTCOME_KEYS[kind],
|
|
219
|
+
`${at}.outcome`,
|
|
220
|
+
`${kind === "open" || kind === "accepted" ? "an" : "a"} ${kind} outcome's`,
|
|
221
|
+
);
|
|
222
|
+
if (unknown !== null) return err(unknown);
|
|
223
|
+
if (kind === "open") return ok({ kind });
|
|
224
|
+
const date = v["date"];
|
|
225
|
+
if (!isIsoDate(date))
|
|
226
|
+
return err(`${at}: "outcome" of kind ${kind} needs "date" (YYYY-MM-DD)`);
|
|
227
|
+
const evidence = v["evidence"];
|
|
228
|
+
if (typeof evidence !== "string" || evidence === "")
|
|
229
|
+
return err(
|
|
230
|
+
`${at}: "outcome" of kind ${kind} needs "evidence": a file in the paper folder (the decision mail, the reviews, the note that records the withdrawal)`,
|
|
231
|
+
);
|
|
232
|
+
return decisionOf(kind, { date, evidence }, v["desk"], at);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function parseSubmission(
|
|
236
|
+
v: unknown,
|
|
237
|
+
at: string,
|
|
238
|
+
): Result<{ readonly id: number } | null, string> {
|
|
239
|
+
if (v === undefined || v === null) return ok(null);
|
|
240
|
+
const unknown = isRecord(v)
|
|
241
|
+
? unknownKey(v, ["id"], `${at}.submission`, "a submission's")
|
|
242
|
+
: null;
|
|
243
|
+
if (unknown !== null) return err(unknown);
|
|
244
|
+
const id = fieldOf(v, "id");
|
|
245
|
+
return typeof id === "number" && Number.isInteger(id) && id > 0
|
|
246
|
+
? ok({ id })
|
|
247
|
+
: err(
|
|
248
|
+
`${at}: "submission" must be { "id": <the submission number on the venue's portal> }`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** The fields that say WHICH attempt this is: id, venue, kind, the day it opened, its phase. */
|
|
253
|
+
type Head = Pick<Cycle, "id" | "venue" | "kind" | "opened" | "phase">;
|
|
254
|
+
|
|
255
|
+
const isPhase = (v: unknown): v is Phase => v === "porting" || v === "prepared";
|
|
256
|
+
|
|
257
|
+
/** `kind`: a string, or null when absent — the venue has one kind, or none is chosen yet. */
|
|
258
|
+
const kindOf = (v: unknown): Result<string | null, string> =>
|
|
259
|
+
v === undefined || v === null
|
|
260
|
+
? ok(null)
|
|
261
|
+
: typeof v === "string"
|
|
262
|
+
? ok(v)
|
|
263
|
+
: err('"kind" must be a string or null');
|
|
264
|
+
|
|
265
|
+
function parseHead(
|
|
266
|
+
v: Readonly<Record<string, unknown>>,
|
|
267
|
+
at: string,
|
|
268
|
+
): Result<Head, string> {
|
|
269
|
+
const id = v["id"];
|
|
270
|
+
if (typeof id !== "string" || id === "")
|
|
271
|
+
return err(`${at}: "id" must be a non-empty string (what stages refer to)`);
|
|
272
|
+
const venue = parseVenue(v["venue"], at);
|
|
273
|
+
if (!venue.ok) return venue;
|
|
274
|
+
const kind = kindOf(v["kind"]);
|
|
275
|
+
if (!kind.ok) return err(`${at}: ${kind.error}`);
|
|
276
|
+
const opened = v["opened"];
|
|
277
|
+
if (!isIsoDate(opened))
|
|
278
|
+
return err(
|
|
279
|
+
`${at}: "opened" must be a date YYYY-MM-DD — the day this attempt was decided`,
|
|
280
|
+
);
|
|
281
|
+
const phase = v["phase"] ?? "prepared";
|
|
282
|
+
if (!isPhase(phase))
|
|
283
|
+
return err(`${at}: "phase" must be "porting" or "prepared"`);
|
|
284
|
+
return ok({
|
|
285
|
+
id,
|
|
286
|
+
venue: venue.value,
|
|
287
|
+
kind: kind.value,
|
|
288
|
+
opened,
|
|
289
|
+
phase,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** The fields that say how the attempt is going: deadlines, the submission, the outcome. */
|
|
294
|
+
type Tail = Pick<Cycle, "deadlines" | "submission" | "outcome">;
|
|
295
|
+
|
|
296
|
+
function parseTail(
|
|
297
|
+
v: Readonly<Record<string, unknown>>,
|
|
298
|
+
at: string,
|
|
299
|
+
): Result<Tail, string> {
|
|
300
|
+
const deadlines = parseDeadlines(v["deadlines"], at);
|
|
301
|
+
if (!deadlines.ok) return deadlines;
|
|
302
|
+
const submission = parseSubmission(v["submission"], at);
|
|
303
|
+
if (!submission.ok) return submission;
|
|
304
|
+
const outcome = parseOutcome(v["outcome"] ?? { kind: "open" }, at);
|
|
305
|
+
if (!outcome.ok) return outcome;
|
|
306
|
+
return ok({
|
|
307
|
+
deadlines: deadlines.value,
|
|
308
|
+
submission: submission.value,
|
|
309
|
+
outcome: outcome.value,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function parseCycle(v: unknown, i: number): Result<Cycle, string> {
|
|
314
|
+
const at = where(i);
|
|
315
|
+
if (!isRecord(v)) return err(`${at} must be an object`);
|
|
316
|
+
const unknown = Object.keys(v).find((k) => !KNOWN_KEYS.includes(k));
|
|
317
|
+
if (unknown !== undefined)
|
|
318
|
+
return err(
|
|
319
|
+
`${at}: unknown key "${unknown}" — known keys: ${KNOWN_KEYS.join(", ")}`,
|
|
320
|
+
);
|
|
321
|
+
const head = parseHead(v, at);
|
|
322
|
+
if (!head.ok) return head;
|
|
323
|
+
const tail = parseTail(v, at);
|
|
324
|
+
return tail.ok ? ok({ ...head.value, ...tail.value }) : tail;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** A `cycles` value → the list, null when absent, or one line naming the entry and the key. Pure. */
|
|
328
|
+
export function parseCycles(
|
|
329
|
+
v: unknown,
|
|
330
|
+
): Result<readonly Cycle[] | null, string> {
|
|
331
|
+
if (v === undefined) return ok(null);
|
|
332
|
+
if (!Array.isArray(v))
|
|
333
|
+
return err(
|
|
334
|
+
`"cycles" must be a LIST of attempts, oldest first — a paper can go to several venues`,
|
|
335
|
+
);
|
|
336
|
+
return v.reduce<Result<readonly Cycle[], string>>((acc, c: unknown, i) => {
|
|
337
|
+
if (!acc.ok) return acc;
|
|
338
|
+
const p = parseCycle(c, i);
|
|
339
|
+
return p.ok ? ok([...acc.value, p.value]) : p;
|
|
340
|
+
}, ok([]));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ── deriving ───────────────────────────────────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
const isOpen = (c: Cycle): boolean => c.outcome.kind === "open";
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* The attempt the paper is on. At most one cycle may be open, and it is the last: two open cycles is
|
|
349
|
+
* dual submission or a cycle nobody closed, and both are refused by name. Pure.
|
|
350
|
+
*/
|
|
351
|
+
export function currentCycle(
|
|
352
|
+
cycles: readonly Cycle[],
|
|
353
|
+
): Result<Current, CycleProblem> {
|
|
354
|
+
const dup = cycles.find(
|
|
355
|
+
(c, i) => cycles.findIndex((d) => d.id === c.id) !== i,
|
|
356
|
+
);
|
|
357
|
+
if (dup !== undefined) return err({ kind: "duplicate-id", id: dup.id });
|
|
358
|
+
const open = cycles.filter(isOpen);
|
|
359
|
+
if (open.length > 1)
|
|
360
|
+
return err({ kind: "two-open", ids: open.map((c) => c.id) });
|
|
361
|
+
const last = cycles.at(-1);
|
|
362
|
+
if (last === undefined) return ok({ kind: "none" });
|
|
363
|
+
const [only] = open;
|
|
364
|
+
if (only !== undefined && only !== last)
|
|
365
|
+
return err({ kind: "open-not-last", id: only.id });
|
|
366
|
+
if (isOpen(last)) return ok({ kind: "cycle", cycle: last });
|
|
367
|
+
return ok(
|
|
368
|
+
last.outcome.kind === "accepted"
|
|
369
|
+
? { kind: "accepted", cycle: last }
|
|
370
|
+
: { kind: "parked", last },
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** One line for a problem, naming what to change. Pure. */
|
|
375
|
+
export function cycleProblemText(p: CycleProblem): string {
|
|
376
|
+
switch (p.kind) {
|
|
377
|
+
case "two-open":
|
|
378
|
+
return `cycles ${p.ids.map((id) => `«${id}»`).join(" and ")} are both open — a paper is on one attempt at a time; two open attempts at two venues is dual submission. Close the one that ended: "outcome": { "kind": "rejected" | "withdrawn" | "accepted", "date", "evidence" }`;
|
|
379
|
+
case "open-not-last":
|
|
380
|
+
return `cycle «${p.id}» is open but is not the last entry — cycles are listed oldest first, and the open one is the current attempt`;
|
|
381
|
+
case "duplicate-id":
|
|
382
|
+
return `two cycles carry the id «${p.id}» — a stage's \`cycle:\` could name either; give each attempt its own id`;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** A paper's `cycles`, parsed, with the attempt they name: minted only when the list is consistent. */
|
|
387
|
+
export interface Cycles {
|
|
388
|
+
readonly list: readonly Cycle[];
|
|
389
|
+
readonly current: Current;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** The list with its current attempt, or why the list names none. Pure. */
|
|
393
|
+
export const cyclesOf = (
|
|
394
|
+
list: readonly Cycle[],
|
|
395
|
+
): Result<Cycles, CycleProblem> => {
|
|
396
|
+
const current = currentCycle(list);
|
|
397
|
+
return current.ok ? ok({ list, current: current.value }) : current;
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* The cycle whose venue and kind are the paper's: the open attempt, or the accepted one; null when
|
|
402
|
+
* the paper is between venues or has none.
|
|
403
|
+
*/
|
|
404
|
+
export const venueCycleOf = (c: Current): Cycle | null => {
|
|
405
|
+
switch (c.kind) {
|
|
406
|
+
case "cycle":
|
|
407
|
+
return c.cycle;
|
|
408
|
+
case "accepted":
|
|
409
|
+
return c.cycle;
|
|
410
|
+
case "parked":
|
|
411
|
+
return null;
|
|
412
|
+
case "none":
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
/** The current attempt's own deadline entries; none between venues or before the first attempt. Pure. */
|
|
418
|
+
export const currentDeadlines = (c: Current): readonly Deadline[] =>
|
|
419
|
+
venueCycleOf(c)?.deadlines ?? [];
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Whether the paper is judged as a blind submission: the venue reviews blind, and this attempt has
|
|
423
|
+
* not been accepted — an accepted paper's camera-ready carries its authors. Pure.
|
|
424
|
+
*/
|
|
425
|
+
export const effectiveBlind = (
|
|
426
|
+
venueBlind: boolean,
|
|
427
|
+
current: Current,
|
|
428
|
+
): boolean => venueBlind && current.kind !== "accepted";
|
|
429
|
+
|
|
430
|
+
/** Whether the current attempt declares its port to the venue's template as open work. */
|
|
431
|
+
export const isPorting = (current: Current): boolean =>
|
|
432
|
+
current.kind === "cycle" && current.cycle.phase === "porting";
|