paperlint 10.4.0 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/deadlines.yml +27 -7
- package/README.md +16 -15
- package/dist/CLAUDE.md.spec.d.ts.map +1 -1
- package/dist/CLAUDE.md.spec.js +6 -3
- package/dist/CLAUDE.md.spec.js.map +1 -1
- package/dist/adapters/hotcrp/index.d.ts +6 -4
- package/dist/adapters/hotcrp/index.d.ts.map +1 -1
- package/dist/adapters/hotcrp/index.js +6 -4
- package/dist/adapters/hotcrp/index.js.map +1 -1
- package/dist/adapters/hotcrp/portal.io.d.ts +3 -1
- package/dist/adapters/hotcrp/portal.io.d.ts.map +1 -1
- package/dist/adapters/hotcrp/portal.io.js +36 -11
- package/dist/adapters/hotcrp/portal.io.js.map +1 -1
- package/dist/adapters/hotcrp/response.d.ts +6 -1
- package/dist/adapters/hotcrp/response.d.ts.map +1 -1
- package/dist/adapters/hotcrp/response.js +17 -0
- package/dist/adapters/hotcrp/response.js.map +1 -1
- package/dist/adapters/hotcrp/site.d.ts +9 -0
- package/dist/adapters/hotcrp/site.d.ts.map +1 -0
- package/dist/adapters/hotcrp/site.js +22 -0
- package/dist/adapters/hotcrp/site.js.map +1 -0
- package/dist/adapters/hotcrp/token.d.ts +13 -0
- package/dist/adapters/hotcrp/token.d.ts.map +1 -0
- package/dist/adapters/hotcrp/token.js +48 -0
- package/dist/adapters/hotcrp/token.js.map +1 -0
- package/dist/adapters/latex/abstract.d.ts +22 -0
- package/dist/adapters/latex/abstract.d.ts.map +1 -0
- package/dist/adapters/latex/abstract.js +187 -0
- package/dist/adapters/latex/abstract.js.map +1 -0
- package/dist/adapters/latex/captions.d.ts +9 -0
- package/dist/adapters/latex/captions.d.ts.map +1 -0
- package/dist/adapters/latex/captions.js +40 -0
- package/dist/adapters/latex/captions.js.map +1 -0
- package/dist/adapters/latex/headings.d.ts +6 -0
- package/dist/adapters/latex/headings.d.ts.map +1 -1
- package/dist/adapters/latex/headings.js +5 -4
- package/dist/adapters/latex/headings.js.map +1 -1
- package/dist/adapters/latex/index.d.ts +1 -0
- package/dist/adapters/latex/index.d.ts.map +1 -1
- package/dist/adapters/latex/index.js +3 -0
- package/dist/adapters/latex/index.js.map +1 -1
- package/dist/adapters/latex/parse.d.ts +15 -0
- package/dist/adapters/latex/parse.d.ts.map +1 -1
- package/dist/adapters/latex/parse.js +64 -1
- package/dist/adapters/latex/parse.js.map +1 -1
- package/dist/adapters/node/list-entries.io.d.ts.map +1 -1
- package/dist/adapters/node/list-entries.io.js +3 -1
- package/dist/adapters/node/list-entries.io.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +20 -8
- package/dist/cli.js.map +1 -1
- package/dist/cycle-rules.d.ts.map +1 -1
- package/dist/cycle-rules.js +3 -4
- package/dist/cycle-rules.js.map +1 -1
- package/dist/domain/deadline.d.ts +8 -1
- package/dist/domain/deadline.d.ts.map +1 -1
- package/dist/domain/deadline.js +26 -12
- package/dist/domain/deadline.js.map +1 -1
- package/dist/domain/decide-first.d.ts +11 -1
- package/dist/domain/decide-first.d.ts.map +1 -1
- package/dist/domain/decide-first.js +6 -0
- package/dist/domain/decide-first.js.map +1 -1
- package/dist/domain/index-columns.d.ts.map +1 -1
- package/dist/domain/index-columns.js +2 -0
- package/dist/domain/index-columns.js.map +1 -1
- package/dist/domain/portal-token.d.ts +44 -0
- package/dist/domain/portal-token.d.ts.map +1 -0
- package/dist/domain/portal-token.js +32 -0
- package/dist/domain/portal-token.js.map +1 -0
- package/dist/domain/study-registry.d.ts +65 -8
- package/dist/domain/study-registry.d.ts.map +1 -1
- package/dist/domain/study-registry.js +63 -19
- package/dist/domain/study-registry.js.map +1 -1
- package/dist/domain/submission.d.ts +30 -0
- package/dist/domain/submission.d.ts.map +1 -1
- package/dist/domain/submission.js +68 -0
- package/dist/domain/submission.js.map +1 -1
- package/dist/lib/paper-config.d.ts +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +6 -4
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/outputs-before-decision-rule.d.ts +1 -1
- package/dist/outputs-before-decision-rule.d.ts.map +1 -1
- package/dist/outputs-before-decision-rule.js +21 -17
- package/dist/outputs-before-decision-rule.js.map +1 -1
- package/dist/paper-settings.d.ts +3 -1
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +11 -5
- package/dist/paper-settings.js.map +1 -1
- package/dist/ports/latex.d.ts +23 -0
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/ports/list-entries.d.ts +7 -4
- package/dist/ports/list-entries.d.ts.map +1 -1
- package/dist/ports/study-deps.d.ts +14 -0
- package/dist/ports/study-deps.d.ts.map +1 -0
- package/dist/ports/study-deps.js +2 -0
- package/dist/ports/study-deps.js.map +1 -0
- package/dist/ports/study-folder.d.ts +43 -0
- package/dist/ports/study-folder.d.ts.map +1 -0
- package/dist/ports/study-folder.js +2 -0
- package/dist/ports/study-folder.js.map +1 -0
- package/dist/ports/submission-portal.d.ts +39 -2
- package/dist/ports/submission-portal.d.ts.map +1 -1
- package/dist/ports/venue-deadlines.d.ts +1 -1
- package/dist/presets.d.ts +1 -2
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +1 -2
- package/dist/presets.js.map +1 -1
- package/dist/studies.d.ts.map +1 -1
- package/dist/studies.js +10 -7
- package/dist/studies.js.map +1 -1
- package/dist/study-folder.d.ts +14 -0
- package/dist/study-folder.d.ts.map +1 -0
- package/dist/study-folder.js +123 -0
- package/dist/study-folder.js.map +1 -0
- package/dist/study-gate.d.ts +1 -1
- package/dist/study-gate.d.ts.map +1 -1
- package/dist/study-gate.js +65 -28
- package/dist/study-gate.js.map +1 -1
- package/dist/study-index-input.d.ts +1 -1
- package/dist/study-index-input.d.ts.map +1 -1
- package/dist/study-index-input.js +8 -3
- package/dist/study-index-input.js.map +1 -1
- package/dist/study-modes.d.ts +38 -27
- package/dist/study-modes.d.ts.map +1 -1
- package/dist/study-modes.js +42 -53
- package/dist/study-modes.js.map +1 -1
- package/dist/study-registry-rule.d.ts +2 -6
- package/dist/study-registry-rule.d.ts.map +1 -1
- package/dist/study-registry-rule.js +32 -41
- package/dist/study-registry-rule.js.map +1 -1
- package/dist/submission.d.ts +49 -11
- package/dist/submission.d.ts.map +1 -1
- package/dist/submission.js +332 -44
- package/dist/submission.js.map +1 -1
- package/docs/configuration.md +2 -1
- package/docs/design/study-modes.md +46 -28
- package/docs/rules/cycle/record.md +9 -6
- package/docs/rules/repro/outputs-before-decision.md +3 -0
- package/docs/rules/repro/study-registered.md +3 -2
- package/docs/rules/tex/heading-case.md +1 -1
- package/docs/submission.md +47 -48
- package/eslint.config.mjs +39 -24
- package/lib/paper-config.ts +7 -4
- package/package.json +6 -1
- package/presets/msr.jsonc +9 -7
- package/presets/msr.md +14 -10
- package/scripts/check.ts +1 -1
- package/scripts/refresh-deadlines/compare.test.ts +75 -0
- package/scripts/refresh-deadlines/compare.ts +101 -0
- package/scripts/refresh-deadlines/main.test.ts +140 -0
- package/scripts/refresh-deadlines/main.ts +56 -0
- package/scripts/refresh-deadlines/refresh.test.ts +287 -0
- package/scripts/refresh-deadlines/refresh.ts +183 -0
- package/skills/grade-paper-writing/SKILL.md +4 -2
- package/skills/grade-paper-writing/SKILL.md.spec.ts +3 -1
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.ts.snap +16 -3
- package/skills/grade-paper-writing/prose-lint.mjs +127 -95
- package/skills/grade-paper-writing/prose-lint.test.ts +119 -10
- package/skills/submission-portal/SKILL.md +60 -44
- package/skills/submission-portal/SKILL.md.spec.ts +62 -43
- package/skills/submission-portal/submission-portal.test.ts +4 -73
- package/src/CLAUDE.md +7 -4
- package/src/CLAUDE.md.spec.ts +6 -3
- package/src/adapters/hotcrp/cassette/README.md +4 -4
- package/src/adapters/hotcrp/cassette/deadlines-msr2027.html +9 -9
- package/src/adapters/hotcrp/deadlines.io.test.ts +2 -2
- package/src/adapters/hotcrp/deadlines.test.ts +3 -3
- package/src/adapters/hotcrp/index.ts +15 -3
- package/src/adapters/hotcrp/portal.io.ts +61 -17
- package/src/adapters/hotcrp/portal.test.ts +105 -1
- package/src/adapters/hotcrp/response.ts +21 -0
- package/src/adapters/hotcrp/site.ts +34 -0
- package/src/adapters/hotcrp/token.test.ts +69 -0
- package/src/adapters/hotcrp/token.ts +51 -0
- package/src/adapters/latex/abstract.test.ts +280 -0
- package/src/adapters/latex/abstract.ts +272 -0
- package/src/adapters/latex/captions.test.ts +57 -0
- package/src/adapters/latex/captions.ts +56 -0
- package/src/adapters/latex/headings.ts +5 -4
- package/src/adapters/latex/index.test.ts +31 -1
- package/src/adapters/latex/index.ts +3 -0
- package/src/adapters/latex/parse.ts +71 -3
- package/src/adapters/node/list-entries.io.ts +2 -1
- package/src/adapters/node/list-entries.test.ts +18 -0
- package/src/cli.test.ts +20 -8
- package/src/cli.ts +22 -13
- package/src/cycle-rules.test.ts +6 -6
- package/src/cycle-rules.ts +7 -4
- package/src/domain/deadline.test.ts +31 -2
- package/src/domain/deadline.ts +42 -16
- package/src/domain/decide-first.test.ts +33 -0
- package/src/domain/decide-first.ts +16 -1
- package/src/domain/index-columns.test.ts +49 -4
- package/src/domain/index-columns.ts +2 -0
- package/src/domain/portal-token.test.ts +55 -0
- package/src/domain/portal-token.ts +63 -0
- package/src/domain/study-measurements.test.ts +7 -3
- package/src/domain/study-registry.test.ts +34 -6
- package/src/domain/study-registry.ts +123 -31
- package/src/domain/submission.test.ts +88 -0
- package/src/domain/submission.ts +99 -0
- package/src/new-paper.test.ts +1 -0
- package/src/outputs-before-decision-rule.test.ts +47 -2
- package/src/outputs-before-decision-rule.ts +31 -22
- package/src/paper-settings.test.ts +20 -0
- package/src/paper-settings.ts +17 -5
- package/src/ports/latex.ts +25 -0
- package/src/ports/list-entries.ts +7 -4
- package/src/ports/study-deps.ts +14 -0
- package/src/ports/study-folder.ts +38 -0
- package/src/ports/submission-portal.ts +43 -1
- package/src/ports/venue-deadlines.ts +1 -1
- package/src/presets.test.ts +8 -5
- package/src/presets.ts +1 -2
- package/src/studies-index-modes.test.ts +2 -2
- package/src/studies.ts +19 -10
- package/src/study-folder.test.ts +237 -0
- package/src/study-folder.ts +176 -0
- package/src/study-gate.test.ts +158 -7
- package/src/study-gate.ts +127 -54
- package/src/study-index-input.ts +12 -4
- package/src/study-modes.ts +67 -80
- package/src/study-registry-rule.test.ts +87 -0
- package/src/study-registry-rule.ts +58 -66
- package/src/submission-new.test.ts +569 -0
- package/src/submission-show.test.ts +210 -0
- package/src/submission-update.test.ts +160 -0
- package/src/submission.test.ts +140 -518
- package/src/submission.ts +494 -61
- package/scripts/refresh-deadlines.test.ts +0 -431
- package/scripts/refresh-deadlines.ts +0 -387
package/src/cli.ts
CHANGED
|
@@ -100,10 +100,10 @@ import { texOutput } from "./adapters/tex-output/index.ts";
|
|
|
100
100
|
import { BIB_RULE_LEVELS, bibRules } from "./bib-rules.ts";
|
|
101
101
|
import { bibliographyReader } from "./references.ts";
|
|
102
102
|
import { onlineReferences } from "./adapters/references/index.ts";
|
|
103
|
-
import {
|
|
103
|
+
import { hotcrpSite } from "./adapters/hotcrp/index.ts";
|
|
104
104
|
import { runSubmission } from "./submission.ts";
|
|
105
105
|
import type { PortalKind, SupportedPortal } from "./domain/submission.ts";
|
|
106
|
-
import type {
|
|
106
|
+
import type { PortalSite } from "./ports/submission-portal.ts";
|
|
107
107
|
import type { CheckReferences } from "./ports/check-references.ts";
|
|
108
108
|
import {
|
|
109
109
|
narrowToOwners,
|
|
@@ -234,6 +234,11 @@ const USAGE = `paperlint — machine-checkable gates for a paper kept in git
|
|
|
234
234
|
into ~/.cache/paperlint/texlive (PAPERLINT_TEXLIVE_DIR overrides); a second
|
|
235
235
|
run does nothing. --check: report what is missing, change nothing
|
|
236
236
|
npx paperlint doctor say what is actually wired — and what only LOOKS wired
|
|
237
|
+
npx paperlint submission new [paper] [--abstract <file>] [--save]
|
|
238
|
+
create the paper's submission on the venue's portal: the title
|
|
239
|
+
and abstract from the build, the authors from paperlint.json,
|
|
240
|
+
the PDF. A DRY RUN unless --save; a save records the new id in
|
|
241
|
+
paperlint.json
|
|
237
242
|
npx paperlint submission show [paper]
|
|
238
243
|
the paper's submission on the venue's portal: status, title,
|
|
239
244
|
type, topics, abstract length, the PDF the portal holds, and
|
|
@@ -285,13 +290,18 @@ studies:
|
|
|
285
290
|
|
|
286
291
|
submission:
|
|
287
292
|
The venue preset declares the portal — "portal": { "kind": "hotcrp", "url": "<site>" } — and the
|
|
288
|
-
paper's paperlint.json its submission there — "submission": { "id": <number> }
|
|
289
|
-
|
|
293
|
+
paper's paperlint.json its submission there — "submission": { "id": <number> }, written by new
|
|
294
|
+
— and, for new, its "authors": [{ "name", "email", "affiliation"? }]. The token comes only from
|
|
295
|
+
the environment, one variable per site: HOTCRP_TOKEN_<subdomain> on *.hotcrp.com, else
|
|
296
|
+
HOTCRP_TOKEN_<host and path> — e.g. HOTCRP_TOKEN_MSR2027 for msr2027.hotcrp.com,
|
|
297
|
+
HOTCRP_TOKEN_AIDC_SUBMIT_ACSAC_ORG for aidc.submit.acsac.org (HotCRP: Account settings →
|
|
298
|
+
Developer). A plain HOTCRP_TOKEN is not read.
|
|
290
299
|
[paper] the paper's folder; default the current directory
|
|
291
300
|
--pdf <file> the PDF to compare (show) or send (update) instead of the paper's own
|
|
292
|
-
--abstract <file> update: a plain-text file whose text
|
|
301
|
+
--abstract <file> new, update: a plain-text file whose text is sent as the abstract
|
|
293
302
|
--submit update: mark the submission submitted
|
|
294
|
-
--save update: really change the submission. Without it, nothing
|
|
303
|
+
--save new, update: really create or change the submission. Without it, nothing
|
|
304
|
+
is saved
|
|
295
305
|
|
|
296
306
|
lint:
|
|
297
307
|
npx paperlint lint [paths…] [--fix] [--config <file.json>] [--json]
|
|
@@ -1737,7 +1747,9 @@ const SIMPLE: Readonly<
|
|
|
1737
1747
|
presets: PRESET_DEPS,
|
|
1738
1748
|
cwd,
|
|
1739
1749
|
env: (name) => process.env[name],
|
|
1740
|
-
|
|
1750
|
+
site: portalSite,
|
|
1751
|
+
latex: latexReader,
|
|
1752
|
+
codec: sourcesCodec,
|
|
1741
1753
|
log,
|
|
1742
1754
|
err,
|
|
1743
1755
|
},
|
|
@@ -1745,13 +1757,10 @@ const SIMPLE: Readonly<
|
|
|
1745
1757
|
};
|
|
1746
1758
|
|
|
1747
1759
|
/** The adapter of each portal kind — one per kind, chosen here at the root. */
|
|
1748
|
-
const PORTALS: Readonly<
|
|
1749
|
-
|
|
1750
|
-
> = {
|
|
1751
|
-
hotcrp: (url, token) => hotcrpPortal({ url, token }),
|
|
1760
|
+
const PORTALS: Readonly<Record<PortalKind, (url: string) => PortalSite>> = {
|
|
1761
|
+
hotcrp: hotcrpSite,
|
|
1752
1762
|
};
|
|
1753
|
-
const
|
|
1754
|
-
PORTALS[p.kind](p.url, token);
|
|
1763
|
+
const portalSite = (p: SupportedPortal): PortalSite => PORTALS[p.kind](p.url);
|
|
1755
1764
|
|
|
1756
1765
|
/** banal's installer, wired from this process's environment: the composition root's work. */
|
|
1757
1766
|
function hostBanalInstaller(): ToolInstaller {
|
package/src/cycle-rules.test.ts
CHANGED
|
@@ -155,8 +155,8 @@ describe("cycle/record", () => {
|
|
|
155
155
|
|
|
156
156
|
describe("cycle/record — the deadlines, over the venue preset's readings", () => {
|
|
157
157
|
it("MSR 2027 as a newcomer records the call: abstract → registration, paper → submission; the preset's portal readings are derived — silent", () => {
|
|
158
|
-
// The portal's submission (2026-10-
|
|
159
|
-
// registration (
|
|
158
|
+
// The portal's submission (2026-10-21T11:59:59Z, from the preset) is the end of the call's
|
|
159
|
+
// registration day (2026-10-20 AoE), before the call's submission: no contradiction.
|
|
160
160
|
const fs = lint({
|
|
161
161
|
cycles: [
|
|
162
162
|
msr([
|
|
@@ -185,7 +185,7 @@ describe("cycle/record — the deadlines, over the venue preset's readings", ()
|
|
|
185
185
|
});
|
|
186
186
|
expect(fs.map((f) => f.messageId)).toEqual(["order"]);
|
|
187
187
|
expect(fs[0]?.message).toBe(
|
|
188
|
-
'paperlint.json, cycle «msr-2027»: the call\'s "registration" (2026-10-26T11:59:59Z) is after its "submission" (2026-10-24T11:59:59Z) — a date typed wrong, or an AoE day miscounted (write a call\'s date as "YYYY-MM-DD AoE" and paperlint converts it)',
|
|
188
|
+
'paperlint.json, cycle «msr-2027»: the call\'s "registration" (2026-10-26T11:59:59Z, the call\'s reading on 2026-10-05 from https://2027.msrconf.org/track/msr-2027-technical-papers) is after its "submission" (2026-10-24T11:59:59Z, the call\'s reading on 2026-10-05 from https://2027.msrconf.org/track/msr-2027-technical-papers) — a date typed wrong, or an AoE day miscounted (write a call\'s date as "YYYY-MM-DD AoE" and paperlint converts it)',
|
|
189
189
|
);
|
|
190
190
|
});
|
|
191
191
|
});
|
|
@@ -199,10 +199,10 @@ describe("cycle/record — a portal reading beside the preset's", () => {
|
|
|
199
199
|
what: "submission",
|
|
200
200
|
observed: [
|
|
201
201
|
{
|
|
202
|
-
at: "2026-10-
|
|
202
|
+
at: "2026-10-21T11:59:59Z",
|
|
203
203
|
source: "portal",
|
|
204
204
|
url: "https://msr2027.hotcrp.com/deadlines",
|
|
205
|
-
read: "2026-10-
|
|
205
|
+
read: "2026-10-19",
|
|
206
206
|
},
|
|
207
207
|
],
|
|
208
208
|
},
|
|
@@ -210,7 +210,7 @@ describe("cycle/record — a portal reading beside the preset's", () => {
|
|
|
210
210
|
],
|
|
211
211
|
});
|
|
212
212
|
expect(fs.map((f) => f.message)).toEqual([
|
|
213
|
-
"paperlint.json, cycle «msr-2027»: the \"submission\" deadline records a portal reading, and the preset paperlint:msr carries the portal's reading
|
|
213
|
+
"paperlint.json, cycle «msr-2027»: the \"submission\" deadline records a portal reading, and the preset paperlint:msr carries it (2026-10-21T11:59:59Z, the portal's reading on 2026-10-19 from https://msr2027.hotcrp.com/deadlines — venues move dates: re-check there before relying on it) — a second copy drifts when paperlint updates the preset: delete this one (the call's reading and an override stay)",
|
|
214
214
|
]);
|
|
215
215
|
});
|
|
216
216
|
|
package/src/cycle-rules.ts
CHANGED
|
@@ -41,7 +41,11 @@ import {
|
|
|
41
41
|
type Cycle,
|
|
42
42
|
type Cycles,
|
|
43
43
|
} from "./domain/cycle.ts";
|
|
44
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
deadlineOrderProblems,
|
|
46
|
+
readingText,
|
|
47
|
+
type Reading,
|
|
48
|
+
} from "./domain/deadline.ts";
|
|
45
49
|
import { resolvePreset } from "./presets.ts";
|
|
46
50
|
import { fieldOf } from "./domain/record.ts";
|
|
47
51
|
import { err, ok, type Result } from "./domain/result.ts";
|
|
@@ -104,8 +108,7 @@ function presetCopies(c: Cycle, preset: PresetOfCycle): readonly Finding[] {
|
|
|
104
108
|
id: c.id,
|
|
105
109
|
what: d.what,
|
|
106
110
|
preset: preset.spec,
|
|
107
|
-
|
|
108
|
-
read: theirs.read,
|
|
111
|
+
reading: readingText(theirs),
|
|
109
112
|
},
|
|
110
113
|
},
|
|
111
114
|
]
|
|
@@ -190,7 +193,7 @@ const META: Readonly<Record<CycleRuleName, FolderRuleModule["meta"]>> = {
|
|
|
190
193
|
messages: {
|
|
191
194
|
broken: `${CONFIG_FILE}: {{why}}`,
|
|
192
195
|
order: `${CONFIG_FILE}, cycle «{{id}}»: {{why}} — a date typed wrong, or an AoE day miscounted (write a call's date as "YYYY-MM-DD AoE" and paperlint converts it)`,
|
|
193
|
-
presetCopy: `${CONFIG_FILE}, cycle «{{id}}»: the "{{what}}" deadline records a portal reading, and the preset {{preset}} carries
|
|
196
|
+
presetCopy: `${CONFIG_FILE}, cycle «{{id}}»: the "{{what}}" deadline records a portal reading, and the preset {{preset}} carries it ({{reading}}) — a second copy drifts when paperlint updates the preset: delete this one (the call's reading and an override stay)`,
|
|
194
197
|
},
|
|
195
198
|
},
|
|
196
199
|
evidence: {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { describe, expect, it } from "vitest";
|
|
6
6
|
import {
|
|
7
|
+
readingText,
|
|
7
8
|
deadlineOrderProblems,
|
|
8
9
|
deadlinesInForce,
|
|
9
10
|
instantOf,
|
|
@@ -325,8 +326,8 @@ describe("deadlineOrderProblems — within ONE source; across sources is the dis
|
|
|
325
326
|
reading("notification", "2027-02-01T00:00:00Z"),
|
|
326
327
|
];
|
|
327
328
|
expect(deadlineOrderProblems(own, preset)).toEqual([
|
|
328
|
-
'the portal\'s "notification" (2027-02-01T00:00:00Z) is after its "camera-ready" (2027-01-01T00:00:00Z)',
|
|
329
|
-
'the call\'s "registration" (2026-10-26T11:59:59Z) is after its "submission" (2026-10-24T11:59:59Z)',
|
|
329
|
+
'the portal\'s "notification" (2027-02-01T00:00:00Z, the portal\'s reading on 2026-10-05 from https://example.org/portal — venues move dates: re-check there before relying on it) is after its "camera-ready" (2027-01-01T00:00:00Z, the portal\'s reading on 2026-10-05 from https://example.org/portal — venues move dates: re-check there before relying on it)',
|
|
330
|
+
'the call\'s "registration" (2026-10-26T11:59:59Z, the call\'s reading on 2026-10-05 from https://beta2027.example.org/call) is after its "submission" (2026-10-24T11:59:59Z, the call\'s reading on 2026-10-05 from https://beta2027.example.org/call)',
|
|
330
331
|
]);
|
|
331
332
|
});
|
|
332
333
|
|
|
@@ -384,3 +385,31 @@ describe("instantOf", () => {
|
|
|
384
385
|
}
|
|
385
386
|
});
|
|
386
387
|
});
|
|
388
|
+
|
|
389
|
+
describe("readingText — the one way a reading is printed", () => {
|
|
390
|
+
it("🔴 a portal's reading says when and where it was read, that venues move dates, and to re-check", () => {
|
|
391
|
+
expect(
|
|
392
|
+
readingText({
|
|
393
|
+
at: "2026-10-21T11:59:59Z",
|
|
394
|
+
source: "portal",
|
|
395
|
+
url: "https://msr2027.hotcrp.com/deadlines",
|
|
396
|
+
read: "2026-10-19",
|
|
397
|
+
}),
|
|
398
|
+
).toBe(
|
|
399
|
+
"2026-10-21T11:59:59Z, the portal's reading on 2026-10-19 from https://msr2027.hotcrp.com/deadlines — venues move dates: re-check there before relying on it",
|
|
400
|
+
);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("a call's reading: when and where it was read", () => {
|
|
404
|
+
expect(
|
|
405
|
+
readingText({
|
|
406
|
+
at: "2026-10-24T11:59:59Z",
|
|
407
|
+
source: "call",
|
|
408
|
+
url: "https://2027.msrconf.org/cfp",
|
|
409
|
+
read: "2026-10-05",
|
|
410
|
+
}),
|
|
411
|
+
).toBe(
|
|
412
|
+
"2026-10-24T11:59:59Z, the call's reading on 2026-10-05 from https://2027.msrconf.org/cfp",
|
|
413
|
+
);
|
|
414
|
+
});
|
|
415
|
+
});
|
package/src/domain/deadline.ts
CHANGED
|
@@ -75,6 +75,17 @@ export interface Observation {
|
|
|
75
75
|
readonly read: IsoDate;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* THE ONE WAY a deadline reading is printed: the instant, and where and when it was read. A portal's
|
|
80
|
+
* reading adds that venues move dates and to re-check the page before relying on it — MSR moved both
|
|
81
|
+
* its deadlines after the preset read them, and the preset kept the old ones for two weeks (#235).
|
|
82
|
+
* Used by `submission show` and the `cycle/record` messages. Pure.
|
|
83
|
+
*/
|
|
84
|
+
export const readingText = (o: Observation): string =>
|
|
85
|
+
o.source === "portal"
|
|
86
|
+
? `${o.at}, the portal's reading on ${o.read} from ${o.url} — venues move dates: re-check there before relying on it`
|
|
87
|
+
: `${o.at}, the ${o.source}'s reading on ${o.read} from ${o.url}`;
|
|
88
|
+
|
|
78
89
|
/** A human decision that another instant binds, with why and the file it rests on. */
|
|
79
90
|
export interface Override {
|
|
80
91
|
readonly at: Instant;
|
|
@@ -110,7 +121,7 @@ export interface InForce {
|
|
|
110
121
|
|
|
111
122
|
/**
|
|
112
123
|
* What a venue's portal says about its deadlines, in paperlint's words — what the scheduled refresh
|
|
113
|
-
* of the shipped presets reads (`scripts/refresh-deadlines
|
|
124
|
+
* of the shipped presets reads (`scripts/refresh-deadlines/`). One adapter per portal kind.
|
|
114
125
|
*/
|
|
115
126
|
export interface PortalDeadlines {
|
|
116
127
|
/** The portal's clock when it answered, Unix seconds. */
|
|
@@ -406,40 +417,51 @@ export function deadlinesInForce(
|
|
|
406
417
|
});
|
|
407
418
|
}
|
|
408
419
|
|
|
409
|
-
/** One instant per kind
|
|
410
|
-
type
|
|
420
|
+
/** One instant per kind — and the reading it came from, an override's none. */
|
|
421
|
+
type Entry = {
|
|
422
|
+
readonly what: DeadlineWhat;
|
|
423
|
+
readonly at: Instant;
|
|
424
|
+
readonly reading: Observation | null;
|
|
425
|
+
};
|
|
426
|
+
/** One entry per kind, the earliest a layer holds for it, in the order of the kinds. */
|
|
427
|
+
type Layer = readonly Entry[];
|
|
411
428
|
|
|
412
429
|
const layerOf = (entries: Layer): Layer =>
|
|
413
430
|
DEADLINE_WHATS.flatMap((what) => {
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
431
|
+
const first = entries
|
|
432
|
+
.filter((e) => e.what === what)
|
|
433
|
+
.toSorted((a, b) => a.at.localeCompare(b.at))[0];
|
|
434
|
+
return first === undefined ? [] : [first];
|
|
417
435
|
});
|
|
418
436
|
|
|
419
437
|
/** The pairs of a layer out of the order registration ≤ submission ≤ resubmission < notification < camera-ready. */
|
|
420
438
|
const inversions = (
|
|
421
439
|
layer: Layer,
|
|
422
|
-
says: (
|
|
440
|
+
says: (e: Entry, later: boolean) => string,
|
|
423
441
|
): readonly string[] =>
|
|
424
442
|
layer.flatMap((d, i) => {
|
|
425
443
|
const before = layer[i - 1];
|
|
426
444
|
return before !== undefined && before.at > d.at
|
|
427
|
-
? [
|
|
428
|
-
`${says(before.what, before.at, false)} is after ${says(d.what, d.at, true)}`,
|
|
429
|
-
]
|
|
445
|
+
? [`${says(before, false)} is after ${says(d, true)}`]
|
|
430
446
|
: [];
|
|
431
447
|
});
|
|
432
448
|
|
|
449
|
+
/** An entry's instant, with where and when it was read when it is a reading. */
|
|
450
|
+
const entryText = (e: Entry): string =>
|
|
451
|
+
e.reading === null ? e.at : readingText(e.reading);
|
|
452
|
+
|
|
433
453
|
const readingsOf = (
|
|
434
454
|
own: readonly Deadline[],
|
|
435
455
|
preset: readonly Reading[],
|
|
436
456
|
source: DeadlineSource,
|
|
437
457
|
): Layer => [
|
|
438
|
-
...preset
|
|
458
|
+
...preset
|
|
459
|
+
.filter((r) => r.source === source)
|
|
460
|
+
.map((r) => ({ what: r.what, at: r.at, reading: r })),
|
|
439
461
|
...own.flatMap((d) =>
|
|
440
462
|
d.observed
|
|
441
463
|
.filter((o) => o.source === source)
|
|
442
|
-
.map((o) => ({ what: d.what, at: o.at })),
|
|
464
|
+
.map((o) => ({ what: d.what, at: o.at, reading: o })),
|
|
443
465
|
),
|
|
444
466
|
];
|
|
445
467
|
|
|
@@ -453,20 +475,24 @@ export function deadlineOrderProblems(
|
|
|
453
475
|
preset: readonly Reading[],
|
|
454
476
|
): readonly string[] {
|
|
455
477
|
const overrides = own.flatMap((d) =>
|
|
456
|
-
d.override === null
|
|
478
|
+
d.override === null
|
|
479
|
+
? []
|
|
480
|
+
: [{ what: d.what, at: d.override.at, reading: null }],
|
|
457
481
|
);
|
|
458
482
|
return [
|
|
459
483
|
...inversions(
|
|
460
484
|
layerOf(readingsOf(own, preset, "portal")),
|
|
461
|
-
(
|
|
485
|
+
(e, later) =>
|
|
486
|
+
`${later ? "its" : "the portal's"} "${e.what}" (${entryText(e)})`,
|
|
462
487
|
),
|
|
463
488
|
...inversions(
|
|
464
489
|
layerOf(readingsOf(own, preset, "call")),
|
|
465
|
-
(
|
|
490
|
+
(e, later) =>
|
|
491
|
+
`${later ? "its" : "the call's"} "${e.what}" (${entryText(e)})`,
|
|
466
492
|
),
|
|
467
493
|
...inversions(
|
|
468
494
|
layerOf(overrides),
|
|
469
|
-
(
|
|
495
|
+
(e) => `the override of "${e.what}" (${entryText(e)})`,
|
|
470
496
|
),
|
|
471
497
|
];
|
|
472
498
|
}
|
|
@@ -180,6 +180,23 @@ it("a decision that names a directory is reported, and no history is judged", ()
|
|
|
180
180
|
]);
|
|
181
181
|
});
|
|
182
182
|
|
|
183
|
+
it("a decision behind a linked directory is reported with the link, and no history is judged", () => {
|
|
184
|
+
// Guards: git records a path through a link as what lies behind the link.
|
|
185
|
+
expect(
|
|
186
|
+
judgeOrder({
|
|
187
|
+
...STUDY,
|
|
188
|
+
facts: { kind: "linked", link: "repro/P1-S01-x/sub" },
|
|
189
|
+
}),
|
|
190
|
+
).toEqual([
|
|
191
|
+
{
|
|
192
|
+
problem: "decision-linked",
|
|
193
|
+
id: "P1-S01",
|
|
194
|
+
file: "repro/P1-S01-x/decision.md",
|
|
195
|
+
link: "repro/P1-S01-x/sub",
|
|
196
|
+
},
|
|
197
|
+
]);
|
|
198
|
+
});
|
|
199
|
+
|
|
183
200
|
const PAGE =
|
|
184
201
|
"\nRule repro/outputs-before-decision: https://github.com/zernie/paperlint/blob/main/docs/rules/repro/outputs-before-decision.md";
|
|
185
202
|
|
|
@@ -192,6 +209,12 @@ const ALL: readonly OrderFinding[] = [
|
|
|
192
209
|
glob: "**/*.md",
|
|
193
210
|
},
|
|
194
211
|
{ problem: "decision-deleted", id: "P1-S01", file: "repro/d.md" },
|
|
212
|
+
{
|
|
213
|
+
problem: "decision-not-file",
|
|
214
|
+
id: "P1-S01",
|
|
215
|
+
file: "repro/p",
|
|
216
|
+
what: "special file",
|
|
217
|
+
},
|
|
195
218
|
{
|
|
196
219
|
problem: "decision-not-file",
|
|
197
220
|
id: "P1-S01",
|
|
@@ -204,6 +227,12 @@ const ALL: readonly OrderFinding[] = [
|
|
|
204
227
|
file: "repro/l.md",
|
|
205
228
|
what: "symbolic link",
|
|
206
229
|
},
|
|
230
|
+
{
|
|
231
|
+
problem: "decision-linked",
|
|
232
|
+
id: "P1-S01",
|
|
233
|
+
file: "repro/sub/d.md",
|
|
234
|
+
link: "repro/sub",
|
|
235
|
+
},
|
|
207
236
|
{ problem: "no-history", id: "P1-S01", why: "git is not installed" },
|
|
208
237
|
{ problem: "shallow", id: "P1-S01" },
|
|
209
238
|
{
|
|
@@ -240,10 +269,14 @@ it("each finding's message: what is wrong, what to do, the rule's page", () => {
|
|
|
240
269
|
PAGE,
|
|
241
270
|
"The decision of `P1-S01`, `repro/d.md`, was committed and then deleted: it is not in HEAD, so the study has no decision its outputs could follow. What to do: restore the decision file as it was decided and commit it — never write a new one after the outputs; if the decision no longer holds, declare the study `exploratory` with the reason." +
|
|
242
271
|
PAGE,
|
|
272
|
+
"The decision of `P1-S01`, `repro/p`, is a special file, not a file: git tracks only regular files, so no order can be read from it. What to do: name the decision file itself in `mode.decision.file`." +
|
|
273
|
+
PAGE,
|
|
243
274
|
"The decision of `P1-S01`, `repro/d`, is a directory, not a file: git answers for a directory with the commits of anything under it, so no order can be read from it. What to do: name the decision file itself in `mode.decision.file`." +
|
|
244
275
|
PAGE,
|
|
245
276
|
"The decision of `P1-S01`, `repro/l.md`, is a symbolic link, not a file: git records the link, not the file it points to, so no order can be read from it. What to do: name the decision file itself in `mode.decision.file`." +
|
|
246
277
|
PAGE,
|
|
278
|
+
"The decision of `P1-S01`, `repro/sub/d.md`, passes through the symbolic link `repro/sub`: git records the link, not the files behind it, so no order can be read from it. What to do: name a decision file that lies in real directories in `mode.decision.file`." +
|
|
279
|
+
PAGE,
|
|
247
280
|
"`P1-S01` is decided before its run, and git's history is the proof of that order, but git could not answer: git is not installed. What to do: lint the paper inside its git repository, with git installed." +
|
|
248
281
|
PAGE,
|
|
249
282
|
"`P1-S01` is decided before its run, and this clone is shallow: every file's first commit is the clone's first, so the order of decision and outputs cannot be read. What to do: fetch the whole history (`git fetch --unshallow`; in GitHub Actions, `actions/checkout` with `fetch-depth: 0`)." +
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
} from "./study-mode.ts";
|
|
24
24
|
|
|
25
25
|
/** What a decision path names on disk when it is no file git can vouch for. */
|
|
26
|
-
export type NotAFile = "directory" | "symbolic link";
|
|
26
|
+
export type NotAFile = "directory" | "symbolic link" | "special file";
|
|
27
27
|
|
|
28
28
|
/** A commit, and its committer date (ISO 8601 with its offset). */
|
|
29
29
|
export interface CommitAt {
|
|
@@ -54,6 +54,8 @@ export interface FirstCommit extends CommitAt {
|
|
|
54
54
|
*/
|
|
55
55
|
export type OrderFacts =
|
|
56
56
|
| { readonly kind: "not-a-file"; readonly what: NotAFile }
|
|
57
|
+
/** The decision's path passes through a symbolic link above the file (`link`: the first, relative to the paper). */
|
|
58
|
+
| { readonly kind: "linked"; readonly link: string }
|
|
57
59
|
| { readonly kind: "unknown"; readonly why: string }
|
|
58
60
|
/** A shallow clone: every file's first commit is the clone's first, so no order can be read. */
|
|
59
61
|
| { readonly kind: "shallow" }
|
|
@@ -97,6 +99,12 @@ export type OrderFinding =
|
|
|
97
99
|
readonly file: string;
|
|
98
100
|
readonly what: NotAFile;
|
|
99
101
|
}
|
|
102
|
+
| {
|
|
103
|
+
readonly problem: "decision-linked";
|
|
104
|
+
readonly id: string;
|
|
105
|
+
readonly file: string;
|
|
106
|
+
readonly link: string;
|
|
107
|
+
}
|
|
100
108
|
| {
|
|
101
109
|
readonly problem: "no-history";
|
|
102
110
|
readonly id: string;
|
|
@@ -188,6 +196,10 @@ const historyFindings = (s: DecideFirstStudy): readonly OrderFinding[] => {
|
|
|
188
196
|
return [
|
|
189
197
|
{ problem: "decision-not-file", id: s.id, file, what: s.facts.what },
|
|
190
198
|
];
|
|
199
|
+
case "linked":
|
|
200
|
+
return [
|
|
201
|
+
{ problem: "decision-linked", id: s.id, file, link: s.facts.link },
|
|
202
|
+
];
|
|
191
203
|
case "unknown":
|
|
192
204
|
return [{ problem: "no-history", id: s.id, why: s.facts.why }];
|
|
193
205
|
case "shallow":
|
|
@@ -212,6 +224,7 @@ const NOT_A_FILE: Readonly<Record<NotAFile, string>> = {
|
|
|
212
224
|
directory:
|
|
213
225
|
"git answers for a directory with the commits of anything under it",
|
|
214
226
|
"symbolic link": "git records the link, not the file it points to",
|
|
227
|
+
"special file": "git tracks only regular files",
|
|
215
228
|
};
|
|
216
229
|
|
|
217
230
|
const short = (c: CommitAt): string => `${c.commit.slice(0, 12)} (${c.date})`;
|
|
@@ -225,6 +238,8 @@ const HEADS: {
|
|
|
225
238
|
`The decision of \`${f.id}\`, \`${f.file}\`, was committed and then deleted: it is not in HEAD, so the study has no decision its outputs could follow. What to do: restore the decision file as it was decided and commit it — never write a new one after the outputs; if the decision no longer holds, declare the study \`exploratory\` with the reason.`,
|
|
226
239
|
"decision-not-file": (f) =>
|
|
227
240
|
`The decision of \`${f.id}\`, \`${f.file}\`, is a ${f.what}, not a file: ${NOT_A_FILE[f.what]}, so no order can be read from it. What to do: name the decision file itself in \`mode.decision.file\`.`,
|
|
241
|
+
"decision-linked": (f) =>
|
|
242
|
+
`The decision of \`${f.id}\`, \`${f.file}\`, passes through the symbolic link \`${f.link}\`: git records the link, not the files behind it, so no order can be read from it. What to do: name a decision file that lies in real directories in \`mode.decision.file\`.`,
|
|
228
243
|
"no-history": (f) =>
|
|
229
244
|
`\`${f.id}\` is decided before its run, and git's history is the proof of that order, but git could not answer: ${f.why}. What to do: lint the paper inside its git repository, with git installed.`,
|
|
230
245
|
shallow: (f) =>
|
|
@@ -82,11 +82,11 @@ const NINE_AT: IndexInput = {
|
|
|
82
82
|
]),
|
|
83
83
|
history: new Map([
|
|
84
84
|
[
|
|
85
|
-
"P1-S01",
|
|
85
|
+
"P1-S01-hint/plan.json",
|
|
86
86
|
{ kind: "read", decision: D, decisionDeleted: false, firstOutput: A },
|
|
87
87
|
],
|
|
88
88
|
[
|
|
89
|
-
"P1-S02",
|
|
89
|
+
"P1-S02-s/plan.json",
|
|
90
90
|
{
|
|
91
91
|
kind: "read",
|
|
92
92
|
decision: null,
|
|
@@ -94,7 +94,7 @@ const NINE_AT: IndexInput = {
|
|
|
94
94
|
firstOutput: null,
|
|
95
95
|
},
|
|
96
96
|
],
|
|
97
|
-
["P1-S04", { kind: "unread", why: "shallow clone" }],
|
|
97
|
+
["P1-S04-s/plan.json", { kind: "unread", why: "shallow clone" }],
|
|
98
98
|
]),
|
|
99
99
|
};
|
|
100
100
|
|
|
@@ -124,7 +124,7 @@ describe("the mode columns of the index", () => {
|
|
|
124
124
|
plans,
|
|
125
125
|
history: new Map([
|
|
126
126
|
[
|
|
127
|
-
"P1-S01",
|
|
127
|
+
"P1-S01-s/plan.json",
|
|
128
128
|
{
|
|
129
129
|
kind: "read",
|
|
130
130
|
decision: D,
|
|
@@ -151,6 +151,49 @@ describe("the mode columns of the index", () => {
|
|
|
151
151
|
});
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
+
it("two rows of one id each show their own history, keyed by folder and plan", () => {
|
|
155
|
+
const r: StudyRegistry = {
|
|
156
|
+
prefix: "P1",
|
|
157
|
+
studies: [row(1, { folder: "P1-S01-a" }), row(1, { folder: "P1-S01-b" })],
|
|
158
|
+
};
|
|
159
|
+
const at: IndexInput = {
|
|
160
|
+
plans: new Map([
|
|
161
|
+
["P1-S01-a/plan.json", plan(DECIDED)],
|
|
162
|
+
["P1-S01-b/plan.json", plan(DECIDED)],
|
|
163
|
+
]),
|
|
164
|
+
history: new Map([
|
|
165
|
+
[
|
|
166
|
+
"P1-S01-a/plan.json",
|
|
167
|
+
{ kind: "read", decision: D, decisionDeleted: false, firstOutput: A },
|
|
168
|
+
],
|
|
169
|
+
[
|
|
170
|
+
"P1-S01-b/plan.json",
|
|
171
|
+
{
|
|
172
|
+
kind: "read",
|
|
173
|
+
decision: null,
|
|
174
|
+
decisionDeleted: false,
|
|
175
|
+
firstOutput: null,
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
]),
|
|
179
|
+
};
|
|
180
|
+
// Guards: keyed by id, the second row's history was shown on both rows (the later one wins).
|
|
181
|
+
expect(
|
|
182
|
+
studyIndex(r, at)
|
|
183
|
+
.split("\n")
|
|
184
|
+
.slice(2)
|
|
185
|
+
.map((l) =>
|
|
186
|
+
l
|
|
187
|
+
.split("|")
|
|
188
|
+
.slice(6, 9)
|
|
189
|
+
.map((c) => c.trim()),
|
|
190
|
+
),
|
|
191
|
+
).toEqual([
|
|
192
|
+
["decide_before_run", "dddddddddddd 2026-10-14", "aaaaaaaaaaaa 2026-10-16"],
|
|
193
|
+
["decide_before_run", "not committed", "none"],
|
|
194
|
+
]);
|
|
195
|
+
});
|
|
196
|
+
|
|
154
197
|
it("a decision committed and since deleted shows its commit, and that it is gone", () => {
|
|
155
198
|
// Guards: the Methods sentence must not cite a decision that no longer stands.
|
|
156
199
|
expect(
|
|
@@ -181,6 +224,7 @@ describe("indexHistory", () => {
|
|
|
181
224
|
indexHistory(study({ kind: "unknown", why: "not a repository" })),
|
|
182
225
|
indexHistory(study({ kind: "shallow" })),
|
|
183
226
|
indexHistory(study({ kind: "not-a-file", what: "directory" })),
|
|
227
|
+
indexHistory(study({ kind: "linked", link: "repro/sub" })),
|
|
184
228
|
indexHistory(
|
|
185
229
|
study({
|
|
186
230
|
kind: "read",
|
|
@@ -204,6 +248,7 @@ describe("indexHistory", () => {
|
|
|
204
248
|
{ kind: "unread", why: "git could not answer" },
|
|
205
249
|
{ kind: "unread", why: "shallow clone" },
|
|
206
250
|
{ kind: "unread", why: "the decision is a directory" },
|
|
251
|
+
{ kind: "unread", why: "the decision is behind a symbolic link" },
|
|
207
252
|
{ kind: "read", decision: D, decisionDeleted: false, firstOutput: A },
|
|
208
253
|
{
|
|
209
254
|
kind: "read",
|
|
@@ -85,6 +85,8 @@ export const indexHistory = (s: DecideFirstStudy): IndexHistory => {
|
|
|
85
85
|
switch (s.facts.kind) {
|
|
86
86
|
case "not-a-file":
|
|
87
87
|
return { kind: "unread", why: `the decision is a ${s.facts.what}` };
|
|
88
|
+
case "linked":
|
|
89
|
+
return { kind: "unread", why: "the decision is behind a symbolic link" };
|
|
88
90
|
case "unknown":
|
|
89
91
|
return { kind: "unread", why: "git could not answer" };
|
|
90
92
|
case "shallow":
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Which token a portal command uses: the site's variable only, a lone site-less one refused by name. */
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { test } from "vitest";
|
|
4
|
+
import { hostOf, lookUpToken } from "./portal-token.ts";
|
|
5
|
+
|
|
6
|
+
const NAMES = { variable: "HOTCRP_TOKEN_MSR2027", siteLess: "HOTCRP_TOKEN" };
|
|
7
|
+
|
|
8
|
+
test("hostOf: the URL's host; a text that is no URL stands for itself", () => {
|
|
9
|
+
assert.equal(
|
|
10
|
+
hostOf("https://msr2027.hotcrp.com/deadlines"),
|
|
11
|
+
"msr2027.hotcrp.com",
|
|
12
|
+
);
|
|
13
|
+
assert.equal(hostOf("msr2027.hotcrp.com"), "msr2027.hotcrp.com");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("lookUpToken: the site's variable set → its token, trimmed, and its name", () => {
|
|
17
|
+
const env: Record<string, string> = {
|
|
18
|
+
HOTCRP_TOKEN_MSR2027: " hct_site \n",
|
|
19
|
+
HOTCRP_TOKEN: "hct_other",
|
|
20
|
+
};
|
|
21
|
+
assert.deepEqual(
|
|
22
|
+
lookUpToken(NAMES, (n) => env[n]),
|
|
23
|
+
{
|
|
24
|
+
ok: true,
|
|
25
|
+
value: { variable: "HOTCRP_TOKEN_MSR2027", token: "hct_site" },
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("🔴 lookUpToken: only the site-less variable set → refused by name, never used", () => {
|
|
31
|
+
const env: Record<string, string> = { HOTCRP_TOKEN: "hct_other" };
|
|
32
|
+
assert.deepEqual(
|
|
33
|
+
lookUpToken(NAMES, (n) => env[n]),
|
|
34
|
+
{
|
|
35
|
+
ok: false,
|
|
36
|
+
error: {
|
|
37
|
+
kind: "site-less",
|
|
38
|
+
variable: "HOTCRP_TOKEN_MSR2027",
|
|
39
|
+
siteLess: "HOTCRP_TOKEN",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("lookUpToken: neither set, or only blanks → missing, naming the site's variable", () => {
|
|
46
|
+
const blank: Record<string, string> = {
|
|
47
|
+
HOTCRP_TOKEN_MSR2027: " ",
|
|
48
|
+
HOTCRP_TOKEN: "",
|
|
49
|
+
};
|
|
50
|
+
for (const read of [() => undefined, (n: string) => blank[n]])
|
|
51
|
+
assert.deepEqual(lookUpToken(NAMES, read), {
|
|
52
|
+
ok: false,
|
|
53
|
+
error: { kind: "missing", variable: "HOTCRP_TOKEN_MSR2027" },
|
|
54
|
+
});
|
|
55
|
+
});
|