paperlint 6.2.2 → 6.3.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/README.md +8 -1
- package/dist/adapters/hotcrp/index.d.ts +7 -0
- package/dist/adapters/hotcrp/index.d.ts.map +1 -0
- package/dist/adapters/hotcrp/index.js +7 -0
- package/dist/adapters/hotcrp/index.js.map +1 -0
- package/dist/adapters/hotcrp/portal.io.d.ts +20 -0
- package/dist/adapters/hotcrp/portal.io.d.ts.map +1 -0
- package/dist/adapters/hotcrp/portal.io.js +90 -0
- package/dist/adapters/hotcrp/portal.io.js.map +1 -0
- package/dist/adapters/hotcrp/response.d.ts +13 -0
- package/dist/adapters/hotcrp/response.d.ts.map +1 -0
- package/dist/adapters/hotcrp/response.js +138 -0
- package/dist/adapters/hotcrp/response.js.map +1 -0
- package/dist/cli.d.ts +3 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +109 -34
- package/dist/cli.js.map +1 -1
- package/dist/domain/result.d.ts +4 -0
- package/dist/domain/result.d.ts.map +1 -1
- package/dist/domain/result.js +2 -0
- package/dist/domain/result.js.map +1 -1
- package/dist/domain/submission.d.ts +106 -0
- package/dist/domain/submission.d.ts.map +1 -0
- package/dist/domain/submission.js +39 -0
- package/dist/domain/submission.js.map +1 -0
- package/dist/domain/venue-name.d.ts +38 -0
- package/dist/domain/venue-name.d.ts.map +1 -0
- package/dist/domain/venue-name.js +71 -0
- package/dist/domain/venue-name.js.map +1 -0
- package/dist/folder-venue-rule.d.ts +51 -0
- package/dist/folder-venue-rule.d.ts.map +1 -0
- package/dist/folder-venue-rule.js +96 -0
- package/dist/folder-venue-rule.js.map +1 -0
- package/dist/init.d.ts +6 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +3 -3
- package/dist/init.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +5 -1
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/new-paper.d.ts +19 -5
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +23 -4
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-settings.d.ts +3 -0
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +20 -4
- package/dist/paper-settings.js.map +1 -1
- package/dist/ports/submission-portal.d.ts +19 -0
- package/dist/ports/submission-portal.d.ts.map +1 -0
- package/dist/ports/submission-portal.js +2 -0
- package/dist/ports/submission-portal.js.map +1 -0
- package/dist/presets.d.ts +24 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +35 -3
- package/dist/presets.js.map +1 -1
- package/dist/rules-config.js +1 -1
- package/dist/submission.d.ts +61 -0
- package/dist/submission.d.ts.map +1 -0
- package/dist/submission.js +211 -0
- package/dist/submission.js.map +1 -0
- package/dist/tex-requirements.d.ts +3 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +3 -0
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +6 -7
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +10 -14
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/configuration.md +17 -10
- package/docs/rules/paper/folder-venue-leftover.md +104 -0
- package/docs/rules/talk/captions-cover.md +1 -1
- package/docs/rules/talk/duration-floor.md +1 -1
- package/docs/rules.md +44 -42
- package/docs/skills.md +5 -4
- package/docs/submission.md +64 -0
- package/docs/talk.md +1 -1
- package/lib/paper-config.ts +6 -1
- package/lib/skill-checks.d.mts +7 -0
- package/lib/skill-trigger-cases.mjs +11 -0
- package/package.json +2 -2
- package/presets/aidc.jsonc +6 -0
- package/presets/venue-profile.schema.json +17 -1
- package/skills/paper-pipeline/paper-pipeline.harness.mjs +4 -0
- package/skills/submission-portal/SKILL.md +69 -0
- package/skills/submission-portal/SKILL.md.spec.ts +71 -0
- package/skills/submission-portal/submission-portal.eval.mjs +15 -0
- package/skills/submission-portal/submission-portal.test.ts +81 -0
- package/skills/submit-paper/SKILL.md +2 -1
- package/skills/submit-paper/SKILL.md.spec.ts +1 -0
- package/skills/verify-citations/SKILL.md +8 -4
- package/skills/verify-citations/SKILL.md.spec.ts +7 -3
- package/skills/verify-citations/scripts/verify-cites.mjs +23 -5
- package/skills/verify-citations/scripts/verify-cites.test.mjs +76 -5
- package/src/adapters/hotcrp/cassette/README.md +24 -0
- package/src/adapters/hotcrp/cassette/get-noauth.json +10 -0
- package/src/adapters/hotcrp/cassette/get-paper.json +39 -0
- package/src/adapters/hotcrp/cassette/post-dot.json +21 -0
- package/src/adapters/hotcrp/cassette/post-ok.json +12 -0
- package/src/adapters/hotcrp/index.ts +11 -0
- package/src/adapters/hotcrp/portal.io.ts +130 -0
- package/src/adapters/hotcrp/portal.test.ts +503 -0
- package/src/adapters/hotcrp/response.ts +181 -0
- package/src/cli.test.ts +109 -0
- package/src/cli.ts +140 -21
- package/src/domain/result.test.ts +9 -1
- package/src/domain/result.ts +5 -0
- package/src/domain/submission.test.ts +45 -0
- package/src/domain/submission.ts +141 -0
- package/src/domain/venue-name.test.ts +115 -0
- package/src/domain/venue-name.ts +129 -0
- package/src/folder-venue-rule.test.ts +182 -0
- package/src/folder-venue-rule.ts +141 -0
- package/src/init.test.ts +22 -0
- package/src/init.ts +9 -2
- package/src/new-paper.harness.mjs +11 -9
- package/src/new-paper.test.ts +129 -8
- package/src/new-paper.ts +38 -7
- package/src/paper-settings.test.ts +12 -1
- package/src/paper-settings.ts +25 -5
- package/src/parse-args.test.ts +19 -0
- package/src/ports/submission-portal.ts +26 -0
- package/src/presets.test.ts +22 -0
- package/src/presets.ts +46 -3
- package/src/rule-cases.test.ts +19 -0
- package/src/rules-config.ts +1 -1
- package/src/submission.test.ts +496 -0
- package/src/submission.ts +311 -0
- package/src/tex-requirements.ts +8 -0
- package/src/tex-venue-rules.test.ts +1 -1
- package/src/tex-venue-rules.ts +12 -25
- package/src/types.ts +13 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `paperlint submission show|update [paper-dir]` — read and change a paper's submission on the
|
|
3
|
+
* venue's portal, through the portal's API.
|
|
4
|
+
*
|
|
5
|
+
* The command is named by the job, not by the portal: WHERE the submission lives is data. The venue
|
|
6
|
+
* preset declares the portal (`"portal": { "kind": "hotcrp", "url": … }`), the paper's
|
|
7
|
+
* `paperlint.json` declares which submission is its own (`"submission": { "id": 7 }`), and one
|
|
8
|
+
* adapter per portal kind does the talking (`src/adapters/hotcrp/`).
|
|
9
|
+
*
|
|
10
|
+
* show status, id, title, paper type, topics, abstract length, the PDF the portal holds — and
|
|
11
|
+
* whether it is the local `paper.pdf`, by sha256
|
|
12
|
+
* update send the PDF (and, if asked, an abstract, or "submitted"). A DRY RUN unless `--save`:
|
|
13
|
+
* the portal checks the change and keeps nothing
|
|
14
|
+
*
|
|
15
|
+
* 🔴 THE TOKEN IS AN INPUT, AND ONLY AN INPUT. It comes from the environment (the root reads it), is
|
|
16
|
+
* handed to the adapter, and never appears in anything printed here.
|
|
17
|
+
*/
|
|
18
|
+
import { basename, join, resolve } from "node:path";
|
|
19
|
+
import { absolutePath } from "./domain/paths.ts";
|
|
20
|
+
import { err, ok, type Result } from "./domain/result.ts";
|
|
21
|
+
import { sha256Hex, type Sha256 } from "./domain/sha256.ts";
|
|
22
|
+
import {
|
|
23
|
+
supportedPortal,
|
|
24
|
+
wordCount,
|
|
25
|
+
type PortalFailure,
|
|
26
|
+
type PortalMessage,
|
|
27
|
+
type SubmissionView,
|
|
28
|
+
type SupportedPortal,
|
|
29
|
+
type UpdateOutcome,
|
|
30
|
+
} from "./domain/submission.ts";
|
|
31
|
+
import type { Files } from "./ports/files.ts";
|
|
32
|
+
import type { SubmissionPortal } from "./ports/submission-portal.ts";
|
|
33
|
+
import { paperPreset, paperPresetProblem, type PresetDeps } from "./presets.ts";
|
|
34
|
+
import { CONFIG_FILE } from "#lib/paper-config";
|
|
35
|
+
|
|
36
|
+
/** The environment variable each portal kind's token is read from. */
|
|
37
|
+
export const TOKEN_ENV: Readonly<Record<SupportedPortal["kind"], string>> = {
|
|
38
|
+
hotcrp: "HOTCRP_TOKEN",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Where each portal kind lets an author create a token. */
|
|
42
|
+
const TOKEN_HELP: Readonly<Record<SupportedPortal["kind"], string>> = {
|
|
43
|
+
hotcrp:
|
|
44
|
+
"create one on the HotCRP site under Account settings → Developer, with the scopes submeta:read and document:read (show) and submeta:write and document:write (update)",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const SUBCOMMANDS = ["show", "update"] as const;
|
|
48
|
+
export type Subcommand = (typeof SUBCOMMANDS)[number];
|
|
49
|
+
const isSubcommand = (s: string | undefined): s is Subcommand =>
|
|
50
|
+
SUBCOMMANDS.some((x) => x === s);
|
|
51
|
+
|
|
52
|
+
/** What one `submission` run was asked, after the command line. */
|
|
53
|
+
export interface SubmissionArgs {
|
|
54
|
+
readonly sub: string | undefined;
|
|
55
|
+
readonly paperDir: string;
|
|
56
|
+
/** Positional arguments past the paper directory: refused. */
|
|
57
|
+
readonly extra: readonly string[];
|
|
58
|
+
/** `--pdf <file>`, relative to `cwd`; null: the paper's own PDF. */
|
|
59
|
+
readonly pdf: string | null;
|
|
60
|
+
/** `--abstract <file>`, relative to `cwd`; null: leave the abstract as it is. */
|
|
61
|
+
readonly abstract: string | null;
|
|
62
|
+
readonly submit: boolean;
|
|
63
|
+
readonly save: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SubmissionDeps {
|
|
67
|
+
readonly files: Files;
|
|
68
|
+
readonly presets: PresetDeps;
|
|
69
|
+
readonly cwd: string;
|
|
70
|
+
/** The value of an environment variable; the root reads the environment. */
|
|
71
|
+
readonly env: (name: string) => string | undefined;
|
|
72
|
+
readonly portalFor: (p: SupportedPortal, token: string) => SubmissionPortal;
|
|
73
|
+
readonly log: (line: string) => void;
|
|
74
|
+
readonly err: (line: string) => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A paper's portal, its submission there, and the PDF the paper builds. */
|
|
78
|
+
export interface SubmissionTarget {
|
|
79
|
+
readonly portal: SupportedPortal;
|
|
80
|
+
readonly id: number;
|
|
81
|
+
/** The paper's built PDF, absolute. */
|
|
82
|
+
readonly pdf: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const configOf = (dir: string): string => join(dir, CONFIG_FILE);
|
|
86
|
+
|
|
87
|
+
/** The paper directory → its portal and submission, or the line naming the file and key to set. */
|
|
88
|
+
export function submissionTarget(
|
|
89
|
+
paperDir: string,
|
|
90
|
+
deps: PresetDeps,
|
|
91
|
+
): Result<SubmissionTarget, string> {
|
|
92
|
+
const p = paperPreset(paperDir, deps);
|
|
93
|
+
const problem = paperPresetProblem(paperDir, p);
|
|
94
|
+
if (problem !== null) return err(problem);
|
|
95
|
+
if (p.kind !== "resolved")
|
|
96
|
+
return err(
|
|
97
|
+
`${configOf(paperDir)}: no "extends" — the venue preset it names declares the portal ("portal": { "kind": …, "url": … })`,
|
|
98
|
+
);
|
|
99
|
+
const declared = p.preset.portal;
|
|
100
|
+
if (declared === null)
|
|
101
|
+
return err(
|
|
102
|
+
`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`,
|
|
103
|
+
);
|
|
104
|
+
const sub = p.settings.submission;
|
|
105
|
+
if (sub === null)
|
|
106
|
+
return err(
|
|
107
|
+
`${configOf(paperDir)}: no "submission" — add "submission": { "id": <the submission number on ${declared.url}> }`,
|
|
108
|
+
);
|
|
109
|
+
const portal = supportedPortal(declared);
|
|
110
|
+
if (!portal.ok) return portal;
|
|
111
|
+
const pdf = join(paperDir, p.settings.pdf ?? "paper.pdf");
|
|
112
|
+
return ok({ portal: portal.value, id: sub.id, pdf });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The token from the environment, or the line naming the variable and where to get one. */
|
|
116
|
+
export function tokenFor(
|
|
117
|
+
portal: SupportedPortal,
|
|
118
|
+
env: (name: string) => string | undefined,
|
|
119
|
+
): Result<string, string> {
|
|
120
|
+
const name = TOKEN_ENV[portal.kind];
|
|
121
|
+
const v = env(name)?.trim();
|
|
122
|
+
return v
|
|
123
|
+
? ok(v)
|
|
124
|
+
: err(
|
|
125
|
+
`${name} is not set — the ${portal.kind} API needs a token: ${TOKEN_HELP[portal.kind]}. Then: export ${name}=…`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const iso = (seconds: number | null): string =>
|
|
130
|
+
seconds === null ? "—" : new Date(seconds * 1000).toISOString();
|
|
131
|
+
|
|
132
|
+
const messageLine = (m: PortalMessage): string =>
|
|
133
|
+
` portal says: ${m.field === null ? "" : `${m.field}: `}${m.message}`;
|
|
134
|
+
|
|
135
|
+
/** A failure in words: HTTP status and the portal's own messages. Never the token. */
|
|
136
|
+
export function failureText(f: PortalFailure): string {
|
|
137
|
+
switch (f.kind) {
|
|
138
|
+
case "refused": {
|
|
139
|
+
const said = f.messages.map((m) => m.message).filter(Boolean);
|
|
140
|
+
return `the portal refused (HTTP ${String(f.httpStatus)})${said.length ? `: ${said.join("; ")}` : ""}`;
|
|
141
|
+
}
|
|
142
|
+
case "unreachable":
|
|
143
|
+
return `the portal cannot be reached: ${f.detail}`;
|
|
144
|
+
case "malformed":
|
|
145
|
+
return `the portal answered HTTP ${String(f.httpStatus)}, but not as its API does: ${f.detail}`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Whether the portal holds the local build, in one line. */
|
|
150
|
+
function matchLine(view: SubmissionView, local: Sha256 | null): string {
|
|
151
|
+
if (local === null) return "match unknown — no local PDF to compare";
|
|
152
|
+
if (view.pdf === null) return "match NO — the portal holds no PDF";
|
|
153
|
+
if (view.pdf.sha256 === null)
|
|
154
|
+
return `match unknown — the portal names a ${view.pdf.hash.replace(/-.*$/s, "")} hash, not a sha256`;
|
|
155
|
+
return view.pdf.sha256 === local
|
|
156
|
+
? "match yes — the portal holds this build"
|
|
157
|
+
: "match NO — the portal holds a different PDF than the local one";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const pdfLine = (view: SubmissionView): string =>
|
|
161
|
+
view.pdf === null
|
|
162
|
+
? "portal PDF none"
|
|
163
|
+
: `portal PDF ${view.pdf.hash}, ${view.pdf.size === null ? "?" : String(view.pdf.size)} bytes, uploaded ${iso(view.pdf.uploadedAt)}`;
|
|
164
|
+
|
|
165
|
+
/** `show`'s report. Pure. */
|
|
166
|
+
export function showLines(
|
|
167
|
+
t: SubmissionTarget,
|
|
168
|
+
view: SubmissionView,
|
|
169
|
+
local: { readonly shown: string; readonly sha256: Sha256 | null },
|
|
170
|
+
): readonly string[] {
|
|
171
|
+
return [
|
|
172
|
+
`submission ${String(view.id)} on ${t.portal.url} (${t.portal.kind})`,
|
|
173
|
+
`status ${view.status}${view.submittedAt === null ? "" : `, submitted ${iso(view.submittedAt)}`}`,
|
|
174
|
+
`title ${view.title ?? "—"}`,
|
|
175
|
+
`paper type ${view.paperType ?? "—"}`,
|
|
176
|
+
`topics ${view.topics.length ? view.topics.join("; ") : "—"}`,
|
|
177
|
+
`abstract ${view.abstract === null ? "—" : `${String(wordCount(view.abstract))} words`}`,
|
|
178
|
+
pdfLine(view),
|
|
179
|
+
`local PDF ${local.shown} ${local.sha256 === null ? "(missing)" : `sha2-${local.sha256}`}`,
|
|
180
|
+
matchLine(view, local.sha256),
|
|
181
|
+
...view.messages.map(messageLine),
|
|
182
|
+
];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** `update`'s report. Pure. */
|
|
186
|
+
export function updateLines(
|
|
187
|
+
o: UpdateOutcome,
|
|
188
|
+
local: { readonly shown: string; readonly sha256: Sha256 },
|
|
189
|
+
save: boolean,
|
|
190
|
+
): readonly string[] {
|
|
191
|
+
const mode = save
|
|
192
|
+
? o.dryRun
|
|
193
|
+
? "SAVE WITHHELD by the portal — nothing changed"
|
|
194
|
+
: "saved"
|
|
195
|
+
: "dry run — the portal checked the change and kept nothing; --save sends it for real";
|
|
196
|
+
return [
|
|
197
|
+
mode,
|
|
198
|
+
`HTTP ${String(o.httpStatus)}`,
|
|
199
|
+
`valid ${o.valid ? "yes" : "NO"}`,
|
|
200
|
+
`changes ${o.changes.length ? o.changes.join(", ") : "—"}`,
|
|
201
|
+
`local PDF ${local.shown} sha2-${local.sha256}`,
|
|
202
|
+
...o.messages.map(messageLine),
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const shownPath = (cwd: string, p: string): string =>
|
|
207
|
+
p.startsWith(`${cwd}/`) ? p.slice(cwd.length + 1) : p;
|
|
208
|
+
|
|
209
|
+
/** The file's bytes, or null when there is none. */
|
|
210
|
+
const bytesAt = (files: Files, p: string): Uint8Array | null =>
|
|
211
|
+
files.readBytes(absolutePath(p));
|
|
212
|
+
|
|
213
|
+
/** `show`: read the submission and compare its PDF with the local one. */
|
|
214
|
+
async function runShow(
|
|
215
|
+
t: SubmissionTarget,
|
|
216
|
+
portal: SubmissionPortal,
|
|
217
|
+
pdfPath: string,
|
|
218
|
+
deps: SubmissionDeps,
|
|
219
|
+
): Promise<number> {
|
|
220
|
+
const view = await portal.show(t.id);
|
|
221
|
+
if (!view.ok) return (deps.err(failureText(view.error)), 1);
|
|
222
|
+
const bytes = bytesAt(deps.files, pdfPath);
|
|
223
|
+
const local = {
|
|
224
|
+
shown: shownPath(deps.cwd, pdfPath),
|
|
225
|
+
sha256: bytes === null ? null : sha256Hex(bytes),
|
|
226
|
+
};
|
|
227
|
+
showLines(t, view.value, local).forEach((l) => {
|
|
228
|
+
deps.log(l);
|
|
229
|
+
});
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The abstract file's text, or the line saying it is missing; null when none was asked for. */
|
|
234
|
+
function abstractOf(
|
|
235
|
+
a: SubmissionArgs,
|
|
236
|
+
deps: SubmissionDeps,
|
|
237
|
+
): Result<string | null, string> {
|
|
238
|
+
if (a.abstract === null) return ok(null);
|
|
239
|
+
const p = resolve(deps.cwd, a.abstract);
|
|
240
|
+
const bytes = bytesAt(deps.files, p);
|
|
241
|
+
return bytes === null
|
|
242
|
+
? err(`--abstract: no file at ${shownPath(deps.cwd, p)}`)
|
|
243
|
+
: ok(new TextDecoder().decode(bytes).trim());
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** `update`: send the PDF (and the abstract, and "submitted" when asked); a dry run unless saving. */
|
|
247
|
+
async function runUpdate(
|
|
248
|
+
t: SubmissionTarget,
|
|
249
|
+
portal: SubmissionPortal,
|
|
250
|
+
a: SubmissionArgs & { readonly pdfPath: string },
|
|
251
|
+
deps: SubmissionDeps,
|
|
252
|
+
): Promise<number> {
|
|
253
|
+
const bytes = bytesAt(deps.files, a.pdfPath);
|
|
254
|
+
const shown = shownPath(deps.cwd, a.pdfPath);
|
|
255
|
+
if (bytes === null)
|
|
256
|
+
return (
|
|
257
|
+
deps.err(`no PDF at ${shown} — build the paper, or name one with --pdf`),
|
|
258
|
+
2
|
|
259
|
+
);
|
|
260
|
+
const abstract = abstractOf(a, deps);
|
|
261
|
+
if (!abstract.ok) return (deps.err(abstract.error), 2);
|
|
262
|
+
const change = {
|
|
263
|
+
pdf: { name: basename(a.pdfPath), bytes },
|
|
264
|
+
abstract: abstract.value,
|
|
265
|
+
submit: a.submit,
|
|
266
|
+
};
|
|
267
|
+
const r = await portal.update(t.id, change, { save: a.save });
|
|
268
|
+
if (!r.ok) return (deps.err(failureText(r.error)), 1);
|
|
269
|
+
const local = { shown, sha256: sha256Hex(bytes) };
|
|
270
|
+
updateLines(r.value, local, a.save).forEach((l) => {
|
|
271
|
+
deps.log(l);
|
|
272
|
+
});
|
|
273
|
+
const fine =
|
|
274
|
+
r.value.valid && r.value.httpStatus >= 200 && r.value.httpStatus < 300;
|
|
275
|
+
return fine ? 0 : 1;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Why the command line cannot be run as asked; null when it can. Pure. */
|
|
279
|
+
export function usageProblem(a: SubmissionArgs): string | null {
|
|
280
|
+
if (!isSubcommand(a.sub))
|
|
281
|
+
return `\`submission\` needs a subcommand: ${SUBCOMMANDS.join(" or ")} — got \`${a.sub ?? ""}\``;
|
|
282
|
+
if (a.extra.length > 0)
|
|
283
|
+
return `\`submission ${a.sub}\` takes one paper folder — got also \`${a.extra.join(" ")}\``;
|
|
284
|
+
const updateOnly = [
|
|
285
|
+
a.abstract === null ? null : "--abstract",
|
|
286
|
+
a.submit ? "--submit" : null,
|
|
287
|
+
a.save ? "--save" : null,
|
|
288
|
+
].filter((f) => f !== null);
|
|
289
|
+
return a.sub === "show" && updateOnly.length > 0
|
|
290
|
+
? `${updateOnly.join(", ")}: only \`submission update\` changes anything; \`show\` reads`
|
|
291
|
+
: null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** `paperlint submission <show|update> [paper-dir]` — exit 0, 1 (the portal said no), or 2 (usage). */
|
|
295
|
+
export async function runSubmission(
|
|
296
|
+
a: SubmissionArgs,
|
|
297
|
+
deps: SubmissionDeps,
|
|
298
|
+
): Promise<number> {
|
|
299
|
+
const usage = usageProblem(a);
|
|
300
|
+
if (usage !== null) return (deps.err(usage), 2);
|
|
301
|
+
const paperDir = resolve(deps.cwd, a.paperDir);
|
|
302
|
+
const t = submissionTarget(paperDir, deps.presets);
|
|
303
|
+
if (!t.ok) return (deps.err(t.error), 2);
|
|
304
|
+
const token = tokenFor(t.value.portal, deps.env);
|
|
305
|
+
if (!token.ok) return (deps.err(token.error), 2);
|
|
306
|
+
const portal = deps.portalFor(t.value.portal, token.value);
|
|
307
|
+
const pdfPath = a.pdf === null ? t.value.pdf : resolve(deps.cwd, a.pdf);
|
|
308
|
+
return a.sub === "show"
|
|
309
|
+
? runShow(t.value, portal, pdfPath, deps)
|
|
310
|
+
: runUpdate(t.value, portal, { ...a, pdfPath }, deps);
|
|
311
|
+
}
|
package/src/tex-requirements.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
type VenueTalk,
|
|
35
35
|
type VenueTalkJson,
|
|
36
36
|
} from "./domain/talk.ts";
|
|
37
|
+
import type { VenuePortal } from "./domain/submission.ts";
|
|
37
38
|
|
|
38
39
|
/** CTAN package name → the names that prove it is installed. */
|
|
39
40
|
export type PackageProofs = Readonly<Record<string, readonly string[]>>;
|
|
@@ -193,6 +194,8 @@ export interface PresetFile {
|
|
|
193
194
|
readonly registerAnchors: readonly RegisterAnchor[] | null;
|
|
194
195
|
/** What the venue asks a presenter to send; null when the file has no `talk` block. */
|
|
195
196
|
readonly talk: VenueTalk | null;
|
|
197
|
+
/** Where the venue takes submissions (`paperlint submission`); null when the file does not say. */
|
|
198
|
+
readonly portal: VenuePortal | null;
|
|
196
199
|
}
|
|
197
200
|
|
|
198
201
|
type KindsJson = Readonly<
|
|
@@ -237,6 +240,7 @@ interface PresetJson {
|
|
|
237
240
|
} & Readonly<Record<RegisterMeasureName, number>>)[];
|
|
238
241
|
};
|
|
239
242
|
readonly talk?: VenueTalkJson;
|
|
243
|
+
readonly portal?: VenuePortal;
|
|
240
244
|
}
|
|
241
245
|
|
|
242
246
|
/** An optional field as the typed preset holds it: absent is null. */
|
|
@@ -342,6 +346,10 @@ export function parsePreset(
|
|
|
342
346
|
rules: j.rules ?? {},
|
|
343
347
|
registerAnchors: anchorsOf(j.register),
|
|
344
348
|
talk: j.talk === undefined ? null : venueTalkOf(j.talk),
|
|
349
|
+
portal:
|
|
350
|
+
j.portal === undefined
|
|
351
|
+
? null
|
|
352
|
+
: { kind: j.portal.kind, url: j.portal.url },
|
|
345
353
|
};
|
|
346
354
|
}
|
|
347
355
|
|
|
@@ -719,7 +719,7 @@ describe("otherVenues", () => {
|
|
|
719
719
|
const aidc = resolvePreset("paperlint:aidc", from, deps);
|
|
720
720
|
expect(aidc.ok).toBe(true);
|
|
721
721
|
if (!aidc.ok) return;
|
|
722
|
-
expect(otherVenues(aidc.value,
|
|
722
|
+
expect(otherVenues(aidc.value, deps).map((o) => o.label)).toEqual([
|
|
723
723
|
"acm-sigconf",
|
|
724
724
|
"agenticdev",
|
|
725
725
|
"aisec",
|
package/src/tex-venue-rules.ts
CHANGED
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
* `pdf/profile` already say so, once. A preset that names no template: silent — there is nothing to
|
|
28
28
|
* compare, and a template-family preset is where it is named.
|
|
29
29
|
*/
|
|
30
|
-
import { basename, dirname
|
|
31
|
-
import { CONFIG_FILE } from "#lib/paper-config";
|
|
30
|
+
import { basename, dirname } from "node:path";
|
|
32
31
|
import {
|
|
33
32
|
collapse,
|
|
34
33
|
documentClassLine,
|
|
@@ -46,13 +45,8 @@ import {
|
|
|
46
45
|
type TextRun,
|
|
47
46
|
} from "./domain/tex-document.ts";
|
|
48
47
|
import type { LatexReader } from "./ports/latex.ts";
|
|
49
|
-
import {
|
|
50
|
-
|
|
51
|
-
resolvePreset,
|
|
52
|
-
SHIPPED_PREFIX,
|
|
53
|
-
shippedPresets,
|
|
54
|
-
type Preset,
|
|
55
|
-
} from "./presets.ts";
|
|
48
|
+
import { paperPreset, shippedVenueNames, type Preset } from "./presets.ts";
|
|
49
|
+
import type { NamedVenue } from "./domain/venue-name.ts";
|
|
56
50
|
import type { PaperSource } from "./domain/paper-source.ts";
|
|
57
51
|
import { readPaper, reportInPaper } from "./tex-paper.ts";
|
|
58
52
|
import type { RequiredSection } from "./tex-requirements.ts";
|
|
@@ -255,10 +249,7 @@ export function judgeRequiredSections(
|
|
|
255
249
|
}
|
|
256
250
|
|
|
257
251
|
/** Another shipped venue: the word for it, and what it is called in a paper's text. */
|
|
258
|
-
export
|
|
259
|
-
readonly label: string;
|
|
260
|
-
readonly aliases: readonly string[];
|
|
261
|
-
}
|
|
252
|
+
export type OtherVenue = NamedVenue;
|
|
262
253
|
|
|
263
254
|
/** `s` as a pattern that matches it literally. */
|
|
264
255
|
const literal = (s: string): string =>
|
|
@@ -440,21 +431,17 @@ export const FORMAT_RULE_LEVELS: Readonly<
|
|
|
440
431
|
};
|
|
441
432
|
|
|
442
433
|
/**
|
|
443
|
-
* Every shipped venue but the ones on this paper's own chain,
|
|
444
|
-
*
|
|
434
|
+
* Every shipped venue but the ones on this paper's own chain, from the one source of venue names
|
|
435
|
+
* (`shippedVenueNames`). A preset all of whose files are on this paper's chain is this venue or one
|
|
436
|
+
* it extends.
|
|
445
437
|
*/
|
|
446
438
|
export function otherVenues(
|
|
447
|
-
preset: Preset,
|
|
448
|
-
fromFile: string,
|
|
439
|
+
preset: Pick<Preset, "chain">,
|
|
449
440
|
deps: VenueRuleDeps,
|
|
450
441
|
): readonly OtherVenue[] {
|
|
451
|
-
return
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
return r.ok && !r.value.chain.every((f) => preset.chain.includes(f))
|
|
455
|
-
? [{ label: r.value.label, aliases: r.value.aliases }]
|
|
456
|
-
: [];
|
|
457
|
-
});
|
|
442
|
+
return shippedVenueNames(deps)
|
|
443
|
+
.filter((v) => !v.chain.every((f) => preset.chain.includes(f)))
|
|
444
|
+
.map(({ label, aliases }) => ({ label, aliases }));
|
|
458
445
|
}
|
|
459
446
|
|
|
460
447
|
/** What the venue-conformance rules are built with: the preset store, and the LaTeX reader. */
|
|
@@ -476,7 +463,7 @@ function readingOf(
|
|
|
476
463
|
const p = paperPreset(dir, deps);
|
|
477
464
|
if (p.kind !== "resolved") return null;
|
|
478
465
|
const preset = p.preset;
|
|
479
|
-
const others = () => otherVenues(preset,
|
|
466
|
+
const others = () => otherVenues(preset, deps);
|
|
480
467
|
const paper = readPaper(filename, src, deps);
|
|
481
468
|
return {
|
|
482
469
|
reading: { src: paper.text, latex: deps.latex, preset, others },
|
package/src/types.ts
CHANGED
|
@@ -68,6 +68,19 @@ export interface Args {
|
|
|
68
68
|
venue: string | null;
|
|
69
69
|
/** `--kind <kind>` for `new`: one of the venue preset's kinds. */
|
|
70
70
|
kind: string | null;
|
|
71
|
+
/** `--pdf <file>` for `submission`: the PDF to compare or send; null is the paper's own. */
|
|
72
|
+
pdf: string | null;
|
|
73
|
+
/** `--abstract <file>` for `submission update`: a plain-text abstract to send. */
|
|
74
|
+
abstract: string | null;
|
|
75
|
+
/** `--submit` for `submission update`: mark the submission submitted. */
|
|
76
|
+
submit: boolean;
|
|
77
|
+
/** `--save` for `submission update`: really change the submission; without it, a dry run. */
|
|
78
|
+
save: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* `--allow-venue-name` for `new`: create the folder even though its name names a venue. Skips
|
|
81
|
+
* that one refusal and no other.
|
|
82
|
+
*/
|
|
83
|
+
allowVenueName: boolean;
|
|
71
84
|
/** `--hooks=<mode>` — parsed only so it can be REFUSED by name rather than read as a path. */
|
|
72
85
|
hooksMode: string | null;
|
|
73
86
|
help?: boolean;
|