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/submission.ts
CHANGED
|
@@ -17,11 +17,16 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { basename, join, resolve } from "node:path";
|
|
19
19
|
import { absolutePath } from "./domain/paths.ts";
|
|
20
|
+
import { messageOf } from "./domain/text.ts";
|
|
20
21
|
import { err, ok, type Result } from "./domain/result.ts";
|
|
21
22
|
import { sha256Hex, type Sha256 } from "./domain/sha256.ts";
|
|
22
23
|
import {
|
|
23
24
|
supportedPortal,
|
|
25
|
+
withSubmissionId,
|
|
24
26
|
wordCount,
|
|
27
|
+
type Author,
|
|
28
|
+
type CreateOutcome,
|
|
29
|
+
type NewSubmission,
|
|
25
30
|
type PortalFailure,
|
|
26
31
|
type PortalMessage,
|
|
27
32
|
type SubmissionView,
|
|
@@ -29,7 +34,10 @@ import {
|
|
|
29
34
|
type UpdateOutcome,
|
|
30
35
|
} from "./domain/submission.ts";
|
|
31
36
|
import type { Files } from "./ports/files.ts";
|
|
32
|
-
import type {
|
|
37
|
+
import type {
|
|
38
|
+
PortalSite,
|
|
39
|
+
SubmissionPortal,
|
|
40
|
+
} from "./ports/submission-portal.ts";
|
|
33
41
|
import {
|
|
34
42
|
paperPreset,
|
|
35
43
|
unusablePresetLine,
|
|
@@ -40,23 +48,70 @@ import type { PaperSettings } from "./paper-settings.ts";
|
|
|
40
48
|
import { currentDeadlines } from "./domain/cycle.ts";
|
|
41
49
|
import {
|
|
42
50
|
deadlinesInForce,
|
|
51
|
+
readingText,
|
|
43
52
|
type InForce,
|
|
44
|
-
type Observation,
|
|
45
53
|
} from "./domain/deadline.ts";
|
|
46
54
|
import { CONFIG_FILE } from "#lib/paper-config";
|
|
55
|
+
import {
|
|
56
|
+
hostOf,
|
|
57
|
+
lookUpToken,
|
|
58
|
+
type TokenProblem,
|
|
59
|
+
} from "./domain/portal-token.ts";
|
|
60
|
+
import { LINE_BREAK, OPAQUE, runText } from "./domain/tex-document.ts";
|
|
61
|
+
import type { LatexReader } from "./ports/latex.ts";
|
|
62
|
+
import { factsPath, parseFactsText } from "./facts-file.ts";
|
|
63
|
+
import { lineColumn, readPaper } from "./tex-paper.ts";
|
|
64
|
+
import { originOf, type PaperSource } from "./domain/paper-source.ts";
|
|
65
|
+
import type { SourcesCodec } from "./ports/sources-codec.ts";
|
|
66
|
+
import { MAIN } from "./build.ts";
|
|
47
67
|
|
|
48
|
-
/**
|
|
49
|
-
export
|
|
50
|
-
|
|
51
|
-
|
|
68
|
+
/** How to make and set the site's token: the page, the scopes, the variable. Pure. */
|
|
69
|
+
export function tokenSteps(
|
|
70
|
+
portal: SupportedPortal,
|
|
71
|
+
site: PortalSite,
|
|
72
|
+
variable: string,
|
|
73
|
+
): readonly string[] {
|
|
74
|
+
const c = site.credentials;
|
|
75
|
+
return [
|
|
76
|
+
` 1. create a token on this site: ${c.page} (${c.where}), with the scopes ${c.scopes}`,
|
|
77
|
+
` 2. set it in your environment — never paste it into a chat: export ${variable}=<the token>`,
|
|
78
|
+
` tokens are per ${portal.kind} site: ${variable} is read for ${hostOf(portal.url)} alone, so two venues take two variables`,
|
|
79
|
+
];
|
|
80
|
+
}
|
|
52
81
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
82
|
+
/** Why there is no token, as the complete instruction the author follows. Pure. */
|
|
83
|
+
export function tokenProblemLines(
|
|
84
|
+
portal: SupportedPortal,
|
|
85
|
+
site: PortalSite,
|
|
86
|
+
p: TokenProblem,
|
|
87
|
+
): readonly string[] {
|
|
88
|
+
const head =
|
|
89
|
+
p.kind === "missing"
|
|
90
|
+
? `no ${portal.kind} token for ${portal.url}: ${p.variable} is not set`
|
|
91
|
+
: `${p.siteLess} is no longer read: tokens are per ${portal.kind} site. If it was made on ${hostOf(portal.url)}, rename it to ${p.variable}; if it was made on another site, rename it to that site's variable (${site.variablePattern}) and create a token here:`;
|
|
92
|
+
return [head, ...tokenSteps(portal, site, p.variable)];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* What to do when the portal turned the token away: 401 is "this site does not know the token", 403
|
|
97
|
+
* a token without the scope — both answered with the site's token page. Empty for anything else. Pure.
|
|
98
|
+
*/
|
|
99
|
+
export function tokenRefusedLines(
|
|
100
|
+
portal: SupportedPortal,
|
|
101
|
+
site: PortalSite,
|
|
102
|
+
f: PortalFailure,
|
|
103
|
+
): readonly string[] {
|
|
104
|
+
if (f.kind !== "refused" || (f.httpStatus !== 401 && f.httpStatus !== 403))
|
|
105
|
+
return [];
|
|
106
|
+
const variable = site.tokenVariable;
|
|
107
|
+
const why =
|
|
108
|
+
f.httpStatus === 401
|
|
109
|
+
? `${variable} was used; it may belong to another ${portal.kind} site (tokens are per site), or be revoked or expired`
|
|
110
|
+
: `${variable} was used; the site turned the request away — the token may lack a scope this subcommand needs`;
|
|
111
|
+
return [why, ...tokenSteps(portal, site, variable)];
|
|
112
|
+
}
|
|
58
113
|
|
|
59
|
-
export const SUBCOMMANDS = ["show", "update"] as const;
|
|
114
|
+
export const SUBCOMMANDS = ["new", "show", "update"] as const;
|
|
60
115
|
export type Subcommand = (typeof SUBCOMMANDS)[number];
|
|
61
116
|
const isSubcommand = (s: string | undefined): s is Subcommand =>
|
|
62
117
|
SUBCOMMANDS.some((x) => x === s);
|
|
@@ -69,7 +124,10 @@ export interface SubmissionArgs {
|
|
|
69
124
|
readonly extra: readonly string[];
|
|
70
125
|
/** `--pdf <file>`, relative to `cwd`; null: the paper's own PDF. */
|
|
71
126
|
readonly pdf: string | null;
|
|
72
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* `--abstract <file>`, relative to `cwd`; null: `update` leaves the abstract as it is, `new` reads
|
|
129
|
+
* it from the build.
|
|
130
|
+
*/
|
|
73
131
|
readonly abstract: string | null;
|
|
74
132
|
readonly submit: boolean;
|
|
75
133
|
readonly save: boolean;
|
|
@@ -81,7 +139,12 @@ export interface SubmissionDeps {
|
|
|
81
139
|
readonly cwd: string;
|
|
82
140
|
/** The value of an environment variable; the root reads the environment. */
|
|
83
141
|
readonly env: (name: string) => string | undefined;
|
|
84
|
-
|
|
142
|
+
/** The portal site of a supported kind: its token variable and help, and its API once there is a token. */
|
|
143
|
+
readonly site: (p: SupportedPortal) => PortalSite;
|
|
144
|
+
/** Reads the paper's `\title` and its abstract, its includes spliced in (`new`). */
|
|
145
|
+
readonly latex: LatexReader;
|
|
146
|
+
/** Reads the build's record of the files TeX read, for the includes (`new`). */
|
|
147
|
+
readonly codec: SourcesCodec;
|
|
85
148
|
readonly log: (line: string) => void;
|
|
86
149
|
readonly err: (line: string) => void;
|
|
87
150
|
}
|
|
@@ -89,7 +152,14 @@ export interface SubmissionDeps {
|
|
|
89
152
|
/** A paper's portal, its submission there, and the PDF the paper builds. */
|
|
90
153
|
export interface SubmissionTarget {
|
|
91
154
|
readonly portal: SupportedPortal;
|
|
92
|
-
|
|
155
|
+
/** The paper's submission on the portal; null before `new` created one. */
|
|
156
|
+
readonly id: number | null;
|
|
157
|
+
/** The paper's `paperlint.json`, absolute. */
|
|
158
|
+
readonly config: string;
|
|
159
|
+
/** The authors the paper declares for the portal; null when it declares none. */
|
|
160
|
+
readonly authors: readonly Author[] | null;
|
|
161
|
+
/** The cycle attempt is accepted: a new submission belongs to a new cycle. */
|
|
162
|
+
readonly accepted: boolean;
|
|
93
163
|
/** The paper's built PDF, absolute. */
|
|
94
164
|
readonly pdf: string;
|
|
95
165
|
/** The venue's call for papers (the preset's `url`); null when the preset names no venue. */
|
|
@@ -122,11 +192,6 @@ function targetOf(
|
|
|
122
192
|
return err(
|
|
123
193
|
`the venue preset declares no "portal" (read: ${p.preset.chain.join(", ")}) — add "portal": { "kind": "hotcrp", "url": "<the site>" } to the venue's preset, or upload by hand`,
|
|
124
194
|
);
|
|
125
|
-
const sub = p.settings.submission;
|
|
126
|
-
if (sub === null)
|
|
127
|
-
return err(
|
|
128
|
-
`${configOf(paperDir)}: no "submission" — add "submission": { "id": <the submission number on ${declared.url}> }`,
|
|
129
|
-
);
|
|
130
195
|
const portal = supportedPortal(declared);
|
|
131
196
|
if (!portal.ok) return portal;
|
|
132
197
|
const pdf = join(paperDir, p.settings.pdf ?? "paper.pdf");
|
|
@@ -137,7 +202,16 @@ function targetOf(
|
|
|
137
202
|
? []
|
|
138
203
|
: currentDeadlines(p.settings.cycles.current);
|
|
139
204
|
const deadlines = deadlinesInForce(own, p.preset.deadlines);
|
|
140
|
-
return ok({
|
|
205
|
+
return ok({
|
|
206
|
+
portal: portal.value,
|
|
207
|
+
id: p.settings.submission?.id ?? null,
|
|
208
|
+
config: configOf(paperDir),
|
|
209
|
+
authors: p.settings.authors,
|
|
210
|
+
accepted: p.settings.cycles?.current.kind === "accepted",
|
|
211
|
+
pdf,
|
|
212
|
+
call,
|
|
213
|
+
deadlines,
|
|
214
|
+
});
|
|
141
215
|
}
|
|
142
216
|
|
|
143
217
|
/** The paper directory → its portal and submission, or the line naming the file and key to set. */
|
|
@@ -157,20 +231,6 @@ export function submissionTarget(
|
|
|
157
231
|
}
|
|
158
232
|
}
|
|
159
233
|
|
|
160
|
-
/** The token from the environment, or the line naming the variable and where to get one. */
|
|
161
|
-
export function tokenFor(
|
|
162
|
-
portal: SupportedPortal,
|
|
163
|
-
env: (name: string) => string | undefined,
|
|
164
|
-
): Result<string, string> {
|
|
165
|
-
const name = TOKEN_ENV[portal.kind];
|
|
166
|
-
const v = env(name)?.trim();
|
|
167
|
-
return v
|
|
168
|
-
? ok(v)
|
|
169
|
-
: err(
|
|
170
|
-
`${name} is not set — the ${portal.kind} API needs a token: ${TOKEN_HELP[portal.kind]}. Then: export ${name}=…`,
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
234
|
const iso = (seconds: number | null): string =>
|
|
175
235
|
seconds === null ? "—" : new Date(seconds * 1000).toISOString();
|
|
176
236
|
|
|
@@ -207,23 +267,20 @@ const pdfLine = (view: SubmissionView): string =>
|
|
|
207
267
|
? "portal PDF none"
|
|
208
268
|
: `portal PDF ${view.pdf.hash}, ${view.pdf.size === null ? "?" : String(view.pdf.size)} bytes, uploaded ${iso(view.pdf.uploadedAt)}`;
|
|
209
269
|
|
|
210
|
-
const readingText = (o: Observation): string =>
|
|
211
|
-
`${o.source} ${o.at}, read ${o.read}`;
|
|
212
|
-
|
|
213
270
|
/** One deadline in force, what set it, and the other readings of it. Pure. */
|
|
214
271
|
export function deadlineLine(d: InForce): string {
|
|
215
272
|
const by = d.by;
|
|
216
|
-
const
|
|
273
|
+
const set =
|
|
217
274
|
by.kind === "override"
|
|
218
|
-
?
|
|
219
|
-
:
|
|
275
|
+
? `${d.at} — override: ${by.override.reason} (${by.override.evidence})`
|
|
276
|
+
: readingText(by.reading);
|
|
220
277
|
const others = d.readings.filter(
|
|
221
278
|
(r) => by.kind === "override" || r !== by.reading,
|
|
222
279
|
);
|
|
223
280
|
const also = others.length
|
|
224
281
|
? `; also ${others.map(readingText).join("; ")}`
|
|
225
282
|
: "";
|
|
226
|
-
return `deadline ${d.what} ${
|
|
283
|
+
return `deadline ${d.what} ${set}${also}`;
|
|
227
284
|
}
|
|
228
285
|
|
|
229
286
|
/** `show`'s report. Pure. */
|
|
@@ -283,15 +340,37 @@ const shownPath = (cwd: string, p: string): string =>
|
|
|
283
340
|
const bytesAt = (files: Files, p: string): Uint8Array | null =>
|
|
284
341
|
files.readBytes(absolutePath(p));
|
|
285
342
|
|
|
343
|
+
/** The deps a subcommand runs with: the command's, and what to say when the portal turns the token away. */
|
|
344
|
+
type RunDeps = SubmissionDeps & {
|
|
345
|
+
readonly portalSite: PortalSite;
|
|
346
|
+
readonly refused: (f: PortalFailure) => readonly string[];
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
/** A failure printed — the portal's words, then what to do about the token if it was the token — exit 1. */
|
|
350
|
+
function failed(f: PortalFailure, deps: RunDeps): number {
|
|
351
|
+
[failureText(f), ...deps.refused(f)].forEach((l) => {
|
|
352
|
+
deps.err(l);
|
|
353
|
+
});
|
|
354
|
+
return 1;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** The submission's id, or the line naming the key to set — `show` and `update` need one. */
|
|
358
|
+
const idOf = (t: SubmissionTarget): Result<number, string> =>
|
|
359
|
+
t.id === null
|
|
360
|
+
? err(
|
|
361
|
+
`${t.config}: no "submission" — add "submission": { "id": <the submission number on ${t.portal.url}> }, or create the submission with \`paperlint submission new\``,
|
|
362
|
+
)
|
|
363
|
+
: ok(t.id);
|
|
364
|
+
|
|
286
365
|
/** `show`: read the submission and compare its PDF with the local one. */
|
|
287
366
|
async function runShow(
|
|
288
|
-
t: SubmissionTarget,
|
|
367
|
+
t: SubmissionTarget & { readonly id: number },
|
|
289
368
|
portal: SubmissionPortal,
|
|
290
369
|
pdfPath: string,
|
|
291
|
-
deps:
|
|
370
|
+
deps: RunDeps,
|
|
292
371
|
): Promise<number> {
|
|
293
372
|
const view = await portal.show(t.id);
|
|
294
|
-
if (!view.ok) return (
|
|
373
|
+
if (!view.ok) return failed(view.error, deps);
|
|
295
374
|
const bytes = bytesAt(deps.files, pdfPath);
|
|
296
375
|
const local = {
|
|
297
376
|
shown: shownPath(deps.cwd, pdfPath),
|
|
@@ -329,10 +408,10 @@ const changeOf = (
|
|
|
329
408
|
|
|
330
409
|
/** `update`: send the PDF (and the abstract, and "submitted" when asked); a dry run unless saving. */
|
|
331
410
|
async function runUpdate(
|
|
332
|
-
t: SubmissionTarget,
|
|
411
|
+
t: SubmissionTarget & { readonly id: number },
|
|
333
412
|
portal: SubmissionPortal,
|
|
334
413
|
a: SubmissionArgs & { readonly pdfPath: string },
|
|
335
|
-
deps:
|
|
414
|
+
deps: RunDeps,
|
|
336
415
|
): Promise<number> {
|
|
337
416
|
const bytes = bytesAt(deps.files, a.pdfPath);
|
|
338
417
|
const shown = shownPath(deps.cwd, a.pdfPath);
|
|
@@ -347,7 +426,7 @@ async function runUpdate(
|
|
|
347
426
|
const r = await portal.update(t.id, changeOf(a, bytes, abstract.value), {
|
|
348
427
|
save: a.save,
|
|
349
428
|
});
|
|
350
|
-
if (!r.ok) return (
|
|
429
|
+
if (!r.ok) return failed(r.error, deps);
|
|
351
430
|
const local = { shown, sha256: sha256Hex(bytes) };
|
|
352
431
|
updateLines(r.value, local, a.save).forEach((l) => {
|
|
353
432
|
deps.log(l);
|
|
@@ -357,23 +436,366 @@ async function runUpdate(
|
|
|
357
436
|
return fine ? 0 : 1;
|
|
358
437
|
}
|
|
359
438
|
|
|
439
|
+
// ── new ──────────────────────────────────────────────────────────────────────────────
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* The metadata of the PDF that is sent, from the build's facts — the title's fallback; null when
|
|
443
|
+
* there are no facts, or they describe another PDF.
|
|
444
|
+
*/
|
|
445
|
+
function pdfMetadata(
|
|
446
|
+
paperDir: string,
|
|
447
|
+
pdf: Uint8Array,
|
|
448
|
+
deps: SubmissionDeps,
|
|
449
|
+
): Readonly<Record<string, string>> | null {
|
|
450
|
+
const bytes = bytesAt(deps.files, factsPath(paperDir));
|
|
451
|
+
const facts =
|
|
452
|
+
bytes === null ? null : parseFactsText(new TextDecoder().decode(bytes));
|
|
453
|
+
return facts?.ok === true && facts.value.sha === sha256Hex(pdf)
|
|
454
|
+
? facts.value.text.metadata
|
|
455
|
+
: null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Where an offset of the assembled paper stands in its own file: `file:line:column`. */
|
|
459
|
+
function placeIn(paper: PaperSource, offset: number): string {
|
|
460
|
+
const o = originOf(paper, { start: offset, end: offset });
|
|
461
|
+
return o === null ? MAIN : `${o.file}:${lineColumn(o.source, o.span.start)}`;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* The abstract from the paper's source: its `abstract` environment, with the files it includes
|
|
466
|
+
* spliced in as the build recorded them, as plain text — or why it cannot be read without guessing.
|
|
467
|
+
*/
|
|
468
|
+
function sourceAbstract(
|
|
469
|
+
paper: PaperSource | null,
|
|
470
|
+
deps: SubmissionDeps,
|
|
471
|
+
): Result<string, string> {
|
|
472
|
+
const instead = "give the abstract with --abstract <file>, a plain-text file";
|
|
473
|
+
if (paper === null)
|
|
474
|
+
return err(`no ${MAIN} to read the abstract from — ${instead}`);
|
|
475
|
+
const r = deps.latex.abstractText(paper.text);
|
|
476
|
+
if (r.ok) return r;
|
|
477
|
+
const guess = "paperlint sends no text it would have to guess";
|
|
478
|
+
switch (r.error.kind) {
|
|
479
|
+
case "none":
|
|
480
|
+
return err(
|
|
481
|
+
`${MAIN} has no \\begin{abstract}…\\end{abstract} — ${instead}`,
|
|
482
|
+
);
|
|
483
|
+
case "unreadable":
|
|
484
|
+
return err(
|
|
485
|
+
`the abstract, ${placeIn(paper, r.error.at)}: ${r.error.what} — ${guess}; ${instead}`,
|
|
486
|
+
);
|
|
487
|
+
case "redefined":
|
|
488
|
+
return err(
|
|
489
|
+
`the abstract uses \\${r.error.name}, which the paper defines ${String(r.error.at.length)} times (${r.error.at.map((o) => placeIn(paper, o)).join(", ")}) — which one TeX uses there is not guessed; ${instead}`,
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* The title as the paper sets it: its `\title`, as text — or, when the title holds a command
|
|
496
|
+
* paperlint cannot read (a macro of the author's own), the PDF's own Title (`pdftitle`). Pure.
|
|
497
|
+
*/
|
|
498
|
+
export function titleOf(
|
|
499
|
+
source: {
|
|
500
|
+
readonly text: string;
|
|
501
|
+
/** Where an offset of `text` stands, as a message names it (file:line:column). */
|
|
502
|
+
readonly where: (offset: number) => string;
|
|
503
|
+
} | null,
|
|
504
|
+
latex: Pick<LatexReader, "headings">,
|
|
505
|
+
metadata: Readonly<Record<string, string>> | null,
|
|
506
|
+
): Result<string, string> {
|
|
507
|
+
const titles = (source === null ? [] : latex.headings(source.text)).filter(
|
|
508
|
+
(h) => h.command.kind === "title" && h.argument === "title",
|
|
509
|
+
);
|
|
510
|
+
// One \title is read; several are not chosen between — the PDF's own Title stands in, or the fix.
|
|
511
|
+
const [heading] = titles.length === 1 ? titles : [];
|
|
512
|
+
const text =
|
|
513
|
+
heading === undefined
|
|
514
|
+
? ""
|
|
515
|
+
: runText(heading.title)
|
|
516
|
+
.replaceAll(LINE_BREAK, " ")
|
|
517
|
+
.replace(/\s+/g, " ")
|
|
518
|
+
.trim();
|
|
519
|
+
if (text !== "" && !text.includes(OPAQUE)) return ok(text);
|
|
520
|
+
const pdfTitle = pdfTitleOf(metadata);
|
|
521
|
+
if (pdfTitle !== "") return ok(pdfTitle);
|
|
522
|
+
return err(
|
|
523
|
+
titles.length > 1
|
|
524
|
+
? `the paper sets \\title ${String(titles.length)} times (${titles.map((h) => source?.where(h.title.segments[0].at)).join(", ")}), and the PDF names no Title — which one to send is not guessed: keep one \\title, or set \\hypersetup{pdftitle={<the title as plain text>}} and rebuild`
|
|
525
|
+
: noReadableTitle(text),
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** The Info dictionary's Title, else the XMP packet's dc:title (pdf-content.ts keeps both); "" for none. */
|
|
530
|
+
const pdfTitleOf = (
|
|
531
|
+
metadata: Readonly<Record<string, string>> | null,
|
|
532
|
+
): string =>
|
|
533
|
+
[metadata?.["Title"], metadata?.["dc:title"]]
|
|
534
|
+
.map((t) => t?.trim() ?? "")
|
|
535
|
+
.find((t) => t !== "") ?? "";
|
|
536
|
+
|
|
537
|
+
/** The fix when the one \\title is missing (`text` "") or holds a command paperlint does not read. */
|
|
538
|
+
const noReadableTitle = (text: string): string =>
|
|
539
|
+
text === ""
|
|
540
|
+
? `no \\title in ${MAIN}, and the PDF names no Title — add \\title{…}, or \\hypersetup{pdftitle={…}}, and rebuild`
|
|
541
|
+
: `the \\title in ${MAIN} uses a command paperlint does not read («${text.replaceAll(OPAQUE, "…")}»), and the PDF names no Title — set \\hypersetup{pdftitle={<the title as plain text>}} and rebuild`;
|
|
542
|
+
|
|
543
|
+
/** What `new` will send, as the author reads it before saying yes. Pure. */
|
|
544
|
+
export function newLines(
|
|
545
|
+
title: string,
|
|
546
|
+
abstract: string,
|
|
547
|
+
authors: readonly Author[],
|
|
548
|
+
): readonly string[] {
|
|
549
|
+
return [
|
|
550
|
+
`title ${title}`,
|
|
551
|
+
...authors.map(
|
|
552
|
+
(a, i) =>
|
|
553
|
+
`author ${String(i + 1).padEnd(6)} ${a.name} <${a.email}>${a.affiliation === null ? "" : `, ${a.affiliation}`}`,
|
|
554
|
+
),
|
|
555
|
+
`abstract ${String(wordCount(abstract))} words:`,
|
|
556
|
+
` ${abstract}`,
|
|
557
|
+
];
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** What the author does on the site after `new`: what paperlint does not send. Pure. */
|
|
561
|
+
export function onTheSiteLines(
|
|
562
|
+
site: PortalSite,
|
|
563
|
+
id: number | null,
|
|
564
|
+
paperArg: string,
|
|
565
|
+
): readonly string[] {
|
|
566
|
+
return [
|
|
567
|
+
`do on the site, ${site.submissionPage(id)} — paperlint sends only the title, the abstract, the authors and the PDF:`,
|
|
568
|
+
...site.siteOnly.map((f) => ` - ${f}`),
|
|
569
|
+
` - then mark it submitted: npx paperlint submission update ${paperArg} --submit (a dry run; --save after your yes)`,
|
|
570
|
+
];
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** `new`'s report of the portal's answer. Pure. */
|
|
574
|
+
export function createLines(
|
|
575
|
+
o: CreateOutcome,
|
|
576
|
+
save: boolean,
|
|
577
|
+
): readonly string[] {
|
|
578
|
+
const mode = save
|
|
579
|
+
? o.id === null
|
|
580
|
+
? "NOTHING CREATED — the portal did not save it"
|
|
581
|
+
: `created submission ${String(o.id)}`
|
|
582
|
+
: "dry run — the portal checked the new submission and kept nothing; --save creates it for real";
|
|
583
|
+
return [
|
|
584
|
+
mode,
|
|
585
|
+
`HTTP ${String(o.httpStatus)}`,
|
|
586
|
+
`valid ${o.valid ? "yes" : "NO"}`,
|
|
587
|
+
`changes ${o.changes.length ? o.changes.join(", ") : "—"}`,
|
|
588
|
+
...o.messages.map(messageLine),
|
|
589
|
+
];
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** A file's JSON, or why there is none: gone, or not JSON. */
|
|
593
|
+
function jsonAt(files: Files, path: string): Result<unknown, string> {
|
|
594
|
+
const bytes = bytesAt(files, path);
|
|
595
|
+
if (bytes === null) return err("the file is gone");
|
|
596
|
+
try {
|
|
597
|
+
return ok(JSON.parse(new TextDecoder().decode(bytes)));
|
|
598
|
+
} catch (e) {
|
|
599
|
+
return err(`not JSON: ${messageOf(e)}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** `paperlint.json` with the new id recorded in it, written; or why it was not. */
|
|
604
|
+
function recordId(
|
|
605
|
+
config: string,
|
|
606
|
+
id: number,
|
|
607
|
+
deps: SubmissionDeps,
|
|
608
|
+
): Result<null, string> {
|
|
609
|
+
const json = jsonAt(deps.files, config);
|
|
610
|
+
if (!json.ok) return json;
|
|
611
|
+
const next = withSubmissionId(json.value, id);
|
|
612
|
+
if (!next.ok) return next;
|
|
613
|
+
// A write that fails (read-only, disk full) must not lose the id the portal just gave.
|
|
614
|
+
try {
|
|
615
|
+
deps.files.writeAtomic(
|
|
616
|
+
absolutePath(config),
|
|
617
|
+
new TextEncoder().encode(`${JSON.stringify(next.value, null, 2)}\n`),
|
|
618
|
+
);
|
|
619
|
+
return ok(null);
|
|
620
|
+
} catch (e) {
|
|
621
|
+
return err(messageOf(e));
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
//** Why `new` cannot run for this paper, or the authors it sends: checked before anything is read. */
|
|
626
|
+
function readyForNew(t: SubmissionTarget): Result<readonly Author[], string> {
|
|
627
|
+
if (t.id !== null)
|
|
628
|
+
return err(
|
|
629
|
+
`${t.config}: the paper already has submission ${String(t.id)} on ${t.portal.url} — \`paperlint submission show\` reads it, \`paperlint submission update\` changes it`,
|
|
630
|
+
);
|
|
631
|
+
if (t.accepted)
|
|
632
|
+
return err(
|
|
633
|
+
`${t.config}: the current attempt is accepted — a new submission belongs to a new cycle`,
|
|
634
|
+
);
|
|
635
|
+
return t.authors === null
|
|
636
|
+
? err(
|
|
637
|
+
`${t.config}: no "authors" — add "authors": [{ "name": "…", "email": "…", "affiliation": "…" }], every author in the paper's order, as ${hostOf(t.portal.url)} should record them; paperlint never guesses them`,
|
|
638
|
+
)
|
|
639
|
+
: ok(t.authors);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** The paper as the build read it: paper.tex with the files it includes spliced in; null without one. */
|
|
643
|
+
function assembledPaper(
|
|
644
|
+
paperDir: string,
|
|
645
|
+
deps: SubmissionDeps,
|
|
646
|
+
): PaperSource | null {
|
|
647
|
+
const bytes = bytesAt(deps.files, join(paperDir, MAIN));
|
|
648
|
+
return bytes === null
|
|
649
|
+
? null
|
|
650
|
+
: readPaper(join(paperDir, MAIN), new TextDecoder().decode(bytes), deps);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/** The title and the abstract: the paper's `\title`, and `--abstract`'s file or the source's abstract. */
|
|
654
|
+
function titleAndAbstract(
|
|
655
|
+
a: SubmissionArgs & { readonly paperDir: string },
|
|
656
|
+
pdf: Uint8Array,
|
|
657
|
+
deps: SubmissionDeps,
|
|
658
|
+
): Result<{ readonly title: string; readonly abstract: string }, string> {
|
|
659
|
+
const own = abstractOf(a, deps);
|
|
660
|
+
if (!own.ok) return own;
|
|
661
|
+
const paper = assembledPaper(a.paperDir, deps);
|
|
662
|
+
const metadata = pdfMetadata(a.paperDir, pdf, deps);
|
|
663
|
+
const title = titleOf(
|
|
664
|
+
paper === null
|
|
665
|
+
? null
|
|
666
|
+
: { text: paper.text, where: (o: number) => placeIn(paper, o) },
|
|
667
|
+
deps.latex,
|
|
668
|
+
metadata,
|
|
669
|
+
);
|
|
670
|
+
if (!title.ok) return title;
|
|
671
|
+
const abstract =
|
|
672
|
+
own.value === null
|
|
673
|
+
? sourceAbstract(paper, deps)
|
|
674
|
+
: ok(own.value.replace(/\s+/g, " "));
|
|
675
|
+
return abstract.ok
|
|
676
|
+
? ok({ title: title.value, abstract: abstract.value })
|
|
677
|
+
: abstract;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/** What `new` sends, or the first line saying what is missing. */
|
|
681
|
+
function newSubmissionOf(
|
|
682
|
+
t: SubmissionTarget,
|
|
683
|
+
a: SubmissionArgs & { readonly pdfPath: string; readonly paperDir: string },
|
|
684
|
+
deps: SubmissionDeps,
|
|
685
|
+
): Result<{ readonly paper: NewSubmission; readonly shown: string }, string> {
|
|
686
|
+
const authors = readyForNew(t);
|
|
687
|
+
if (!authors.ok) return authors;
|
|
688
|
+
const shown = shownPath(deps.cwd, a.pdfPath);
|
|
689
|
+
const pdf = bytesAt(deps.files, a.pdfPath);
|
|
690
|
+
if (pdf === null)
|
|
691
|
+
return err(`no PDF at ${shown} — build the paper, or name one with --pdf`);
|
|
692
|
+
const text = titleAndAbstract(a, pdf, deps);
|
|
693
|
+
if (!text.ok) return text;
|
|
694
|
+
const paper = {
|
|
695
|
+
...text.value,
|
|
696
|
+
authors: authors.value,
|
|
697
|
+
pdf: { name: basename(a.pdfPath), bytes: pdf },
|
|
698
|
+
};
|
|
699
|
+
return ok({ paper, shown });
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
//** After a save that created one: the id written into `paperlint.json`, and the next command. */
|
|
703
|
+
function recordCreated(
|
|
704
|
+
t: SubmissionTarget,
|
|
705
|
+
id: number,
|
|
706
|
+
paperArg: string,
|
|
707
|
+
deps: RunDeps,
|
|
708
|
+
): number {
|
|
709
|
+
const config = shownPath(deps.cwd, t.config);
|
|
710
|
+
const recorded = recordId(t.config, id, deps);
|
|
711
|
+
if (!recorded.ok) {
|
|
712
|
+
deps.err(
|
|
713
|
+
`submission ${String(id)} was created (${deps.portalSite.submissionPage(id)}), but ${config} was not changed (${recorded.error}) — add "submission": { "id": ${String(id)} } to the current attempt by hand`,
|
|
714
|
+
);
|
|
715
|
+
return 1;
|
|
716
|
+
}
|
|
717
|
+
deps.log(`recorded "submission": { "id": ${String(id)} } in ${config}`);
|
|
718
|
+
deps.log(
|
|
719
|
+
`next npx paperlint submission show ${paperArg} — it should say "match yes"`,
|
|
720
|
+
);
|
|
721
|
+
return 0;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const logAll = (lines: readonly string[], deps: RunDeps): void => {
|
|
725
|
+
lines.forEach((l) => {
|
|
726
|
+
deps.log(l);
|
|
727
|
+
});
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* `new`: create the paper's submission — title and abstract from the build, the authors the paper
|
|
732
|
+
* declares, the PDF. A dry run unless `--save`; after a save, the id goes into `paperlint.json`.
|
|
733
|
+
*/
|
|
734
|
+
async function runNew(
|
|
735
|
+
t: SubmissionTarget,
|
|
736
|
+
portal: SubmissionPortal,
|
|
737
|
+
a: SubmissionArgs & { readonly pdfPath: string; readonly paperDir: string },
|
|
738
|
+
deps: RunDeps,
|
|
739
|
+
): Promise<number> {
|
|
740
|
+
const n = newSubmissionOf(t, a, deps);
|
|
741
|
+
if (!n.ok) return (deps.err(n.error), 2);
|
|
742
|
+
const { paper, shown } = n.value;
|
|
743
|
+
deps.log(checkPdfLine(shown, t.call));
|
|
744
|
+
logAll(newLines(paper.title, paper.abstract, paper.authors), deps);
|
|
745
|
+
const r = await portal.create(paper, { save: a.save });
|
|
746
|
+
if (!r.ok) return failed(r.error, deps);
|
|
747
|
+
logAll(createLines(r.value, a.save), deps);
|
|
748
|
+
const fine =
|
|
749
|
+
r.value.valid && r.value.httpStatus >= 200 && r.value.httpStatus < 300;
|
|
750
|
+
if (!fine || (a.save && r.value.id === null)) return 1;
|
|
751
|
+
const paperArg =
|
|
752
|
+
a.paperDir === deps.cwd ? "." : shownPath(deps.cwd, a.paperDir);
|
|
753
|
+
logAll(onTheSiteLines(deps.portalSite, r.value.id, paperArg), deps);
|
|
754
|
+
return r.value.id === null ? 0 : recordCreated(t, r.value.id, paperArg, deps);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
//** The flags that change something, as given. */
|
|
758
|
+
const writingFlags = (a: SubmissionArgs): readonly string[] =>
|
|
759
|
+
[
|
|
760
|
+
a.abstract === null ? null : "--abstract",
|
|
761
|
+
a.submit ? "--submit" : null,
|
|
762
|
+
a.save ? "--save" : null,
|
|
763
|
+
].filter((f) => f !== null);
|
|
764
|
+
|
|
360
765
|
/** Why the command line cannot be run as asked; null when it can. Pure. */
|
|
361
766
|
export function usageProblem(a: SubmissionArgs): string | null {
|
|
362
767
|
if (!isSubcommand(a.sub))
|
|
363
768
|
return `\`submission\` needs a subcommand: ${SUBCOMMANDS.join(" or ")} — got \`${a.sub ?? ""}\``;
|
|
364
769
|
if (a.extra.length > 0)
|
|
365
770
|
return `\`submission ${a.sub}\` takes one paper folder — got also \`${a.extra.join(" ")}\``;
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
return a.sub === "show" && updateOnly.length > 0
|
|
372
|
-
? `${updateOnly.join(", ")}: only \`submission update\` changes anything; \`show\` reads`
|
|
771
|
+
const writing = writingFlags(a);
|
|
772
|
+
if (a.sub === "show" && writing.length > 0)
|
|
773
|
+
return `${writing.join(", ")}: only \`submission new\` and \`submission update\` change anything; \`show\` reads`;
|
|
774
|
+
return a.sub === "new" && a.submit
|
|
775
|
+
? "--submit: `submission new` creates the submission; mark it submitted with `submission update --submit` once `show` says the portal holds this build"
|
|
373
776
|
: null;
|
|
374
777
|
}
|
|
375
778
|
|
|
376
|
-
/**
|
|
779
|
+
/** The variables a site's token lookup reads. */
|
|
780
|
+
const tokenNames = (site: PortalSite) => ({
|
|
781
|
+
variable: site.tokenVariable,
|
|
782
|
+
siteLess: site.siteLessVariable,
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
/** No token to use: the complete instruction, exit 2 — nothing is sent. */
|
|
786
|
+
function tokenMissing(
|
|
787
|
+
portal: SupportedPortal,
|
|
788
|
+
site: PortalSite,
|
|
789
|
+
p: TokenProblem,
|
|
790
|
+
deps: SubmissionDeps,
|
|
791
|
+
): number {
|
|
792
|
+
tokenProblemLines(portal, site, p).forEach((l) => {
|
|
793
|
+
deps.err(l);
|
|
794
|
+
});
|
|
795
|
+
return 2;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/** `paperlint submission <new|show|update> [paper-dir]` — exit 0, 1 (the portal said no), or 2 (usage). */
|
|
377
799
|
export async function runSubmission(
|
|
378
800
|
a: SubmissionArgs,
|
|
379
801
|
deps: SubmissionDeps,
|
|
@@ -383,11 +805,22 @@ export async function runSubmission(
|
|
|
383
805
|
const paperDir = resolve(deps.cwd, a.paperDir);
|
|
384
806
|
const t = submissionTarget(paperDir, deps.presets);
|
|
385
807
|
if (!t.ok) return (deps.err(t.error), 2);
|
|
386
|
-
const
|
|
387
|
-
if (!
|
|
388
|
-
const
|
|
808
|
+
const id = a.sub === "new" ? ok(null) : idOf(t.value);
|
|
809
|
+
if (!id.ok) return (deps.err(id.error), 2);
|
|
810
|
+
const site = deps.site(t.value.portal);
|
|
811
|
+
const found = lookUpToken(tokenNames(site), deps.env);
|
|
812
|
+
if (!found.ok) return tokenMissing(t.value.portal, site, found.error, deps);
|
|
813
|
+
const portal = site.connect(found.value.token);
|
|
389
814
|
const pdfPath = a.pdf === null ? t.value.pdf : resolve(deps.cwd, a.pdf);
|
|
815
|
+
const withToken: RunDeps = {
|
|
816
|
+
...deps,
|
|
817
|
+
portalSite: site,
|
|
818
|
+
refused: (f) => tokenRefusedLines(t.value.portal, site, f),
|
|
819
|
+
};
|
|
820
|
+
if (id.value === null)
|
|
821
|
+
return runNew(t.value, portal, { ...a, pdfPath, paperDir }, withToken);
|
|
822
|
+
const target = { ...t.value, id: id.value };
|
|
390
823
|
return a.sub === "show"
|
|
391
|
-
? runShow(
|
|
392
|
-
: runUpdate(
|
|
824
|
+
? runShow(target, portal, pdfPath, withToken)
|
|
825
|
+
: runUpdate(target, portal, { ...a, pdfPath }, withToken);
|
|
393
826
|
}
|