paperlint 6.2.3 → 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/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,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: submission-portal
|
|
3
|
+
description: "Read or change a paper's submission on the venue's HotCRP portal via `paperlint submission show|update`: replace the PDF or abstract, mark it submitted, check it is really \"Submitted\", and verify by sha256 that the portal holds the latest build. Use when the PDF or abstract on HotCRP needs replacing, when asked whether the paper is truly submitted or which PDF reviewers see, or before a deadline. Composes with submit-paper (what to submit); this gets it onto the portal and proves it landed."
|
|
4
|
+
allowed-tools: [Read, Grep, Glob, Bash]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!-- vigiles:sha256:deb3d74f5c3f93a8 compiled from skills/submission-portal/SKILL.md.spec.ts -->
|
|
8
|
+
|
|
9
|
+
# submission-portal — read and update the submission on the portal, then prove it landed
|
|
10
|
+
|
|
11
|
+
`paperlint submission` talks to the venue's submission portal through its API. Only HotCRP is
|
|
12
|
+
supported; for any other portal the command says so and the upload is done by hand.
|
|
13
|
+
|
|
14
|
+
## What must be in place
|
|
15
|
+
|
|
16
|
+
- **The venue preset declares the portal**: `"portal": { "kind": "hotcrp", "url": "<the site>" }`
|
|
17
|
+
(`paperlint:aidc` has it). For your own venue preset, add it there.
|
|
18
|
+
- **The paper's `paperlint.json` names its submission**: `"submission": { "id": <number> }` — the
|
|
19
|
+
number HotCRP shows for the paper.
|
|
20
|
+
- **A token in `HOTCRP_TOKEN`**, created by the author on the HotCRP site under Account settings →
|
|
21
|
+
Developer (scopes `submeta:read` and `document:read` to read; `submeta:write` and
|
|
22
|
+
`document:write` to update). Never ask for it in chat, never print it, never write it to a file:
|
|
23
|
+
the command reads it from the environment and sends it only as the `Authorization` header.
|
|
24
|
+
|
|
25
|
+
A missing key or token stops the command with exit 2 and a line naming the file and key, or the
|
|
26
|
+
variable — fix exactly that and run again.
|
|
27
|
+
|
|
28
|
+
## The procedure — always in this order
|
|
29
|
+
|
|
30
|
+
1. **Read it.** `npx paperlint submission show papers/<paper>` — status, title, type, topics,
|
|
31
|
+
abstract length, the PDF the portal holds, and `match`: whether that PDF's sha256 equals the
|
|
32
|
+
local `paper.pdf`. Rebuild first (`npx paperlint build papers/<paper>`) if the local PDF is stale.
|
|
33
|
+
2. **Dry run.** `npx paperlint submission update papers/<paper>` — sends the PDF (add
|
|
34
|
+
`--abstract <file>` for a plain-text abstract, `--submit` to mark it submitted) with HotCRP's
|
|
35
|
+
`dry_run=1`: the portal checks the change and **keeps nothing**. Read `valid`, `changes` and every
|
|
36
|
+
`portal says:` line. Exit 1 means the portal would refuse it — report why, do not go on.
|
|
37
|
+
3. **Ask the author.** Show the dry run's output and ask for an explicit yes to change the real
|
|
38
|
+
submission.
|
|
39
|
+
4. **Save — only after that yes.** `npx paperlint submission update papers/<paper> --save` (same
|
|
40
|
+
flags as the dry run). 🔴 `--save` CHANGES A REAL SUBMISSION that reviewers will see. Never run it
|
|
41
|
+
without the author's explicit yes in this conversation, never "to test", and never because a
|
|
42
|
+
dry run was valid.
|
|
43
|
+
5. **Prove it landed.** `npx paperlint submission show papers/<paper>` again: `match` must read
|
|
44
|
+
`yes — the portal holds this build`, and the status must be what was intended (`submitted` if
|
|
45
|
+
`--submit` was sent). Anything else is not done — say so.
|
|
46
|
+
|
|
47
|
+
The same five steps as commands, in order (`<paper>` is the paper's folder):
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx paperlint submission show papers/<paper>
|
|
51
|
+
npx paperlint submission update papers/<paper>
|
|
52
|
+
# … the author reads the dry run and says yes …
|
|
53
|
+
npx paperlint submission update papers/<paper> --save
|
|
54
|
+
npx paperlint submission show papers/<paper>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Reading the answers
|
|
58
|
+
|
|
59
|
+
- `match yes` is the only proof the reviewers get the local build; a status of `submitted` alone
|
|
60
|
+
says nothing about WHICH PDF.
|
|
61
|
+
- `the portal refused (HTTP 401): …` — the token is missing, wrong or expired; `403` — the token
|
|
62
|
+
lacks the scope, or the deadline has passed for this change. The text after the colon is
|
|
63
|
+
HotCRP's own; quote it.
|
|
64
|
+
- `SAVE WITHHELD by the portal` — HotCRP did not save; nothing changed.
|
|
65
|
+
|
|
66
|
+
## What this skill does not do
|
|
67
|
+
|
|
68
|
+
It does not decide whether the paper is ready (that is `harden-paper`), anonymize it, or fill the
|
|
69
|
+
submission form's other fields (authors, conflicts) — see `submit-paper`.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Compiled to SKILL.md by `vigiles compile`. Edit THIS file, never the markdown.
|
|
2
|
+
import { experimental_skill } from "vigiles/spec";
|
|
3
|
+
|
|
4
|
+
export default experimental_skill({
|
|
5
|
+
name: "submission-portal",
|
|
6
|
+
description:
|
|
7
|
+
"Read or change a paper's submission on the venue's HotCRP portal via `paperlint submission show|update`: replace the PDF or abstract, mark it submitted, check it is really \"Submitted\", and verify by sha256 that the portal holds the latest build. Use when the PDF or abstract on HotCRP needs replacing, when asked whether the paper is truly submitted or which PDF reviewers see, or before a deadline. Composes with submit-paper (what to submit); this gets it onto the portal and proves it landed.",
|
|
8
|
+
tools: ["Read", "Grep", "Glob", "Bash"],
|
|
9
|
+
body: `
|
|
10
|
+
# submission-portal — read and update the submission on the portal, then prove it landed
|
|
11
|
+
|
|
12
|
+
\`paperlint submission\` talks to the venue's submission portal through its API. Only HotCRP is
|
|
13
|
+
supported; for any other portal the command says so and the upload is done by hand.
|
|
14
|
+
|
|
15
|
+
## What must be in place
|
|
16
|
+
|
|
17
|
+
- **The venue preset declares the portal**: \`"portal": { "kind": "hotcrp", "url": "<the site>" }\`
|
|
18
|
+
(\`paperlint:aidc\` has it). For your own venue preset, add it there.
|
|
19
|
+
- **The paper's \`paperlint.json\` names its submission**: \`"submission": { "id": <number> }\` — the
|
|
20
|
+
number HotCRP shows for the paper.
|
|
21
|
+
- **A token in \`HOTCRP_TOKEN\`**, created by the author on the HotCRP site under Account settings →
|
|
22
|
+
Developer (scopes \`submeta:read\` and \`document:read\` to read; \`submeta:write\` and
|
|
23
|
+
\`document:write\` to update). Never ask for it in chat, never print it, never write it to a file:
|
|
24
|
+
the command reads it from the environment and sends it only as the \`Authorization\` header.
|
|
25
|
+
|
|
26
|
+
A missing key or token stops the command with exit 2 and a line naming the file and key, or the
|
|
27
|
+
variable — fix exactly that and run again.
|
|
28
|
+
|
|
29
|
+
## The procedure — always in this order
|
|
30
|
+
|
|
31
|
+
1. **Read it.** \`npx paperlint submission show papers/<paper>\` — status, title, type, topics,
|
|
32
|
+
abstract length, the PDF the portal holds, and \`match\`: whether that PDF's sha256 equals the
|
|
33
|
+
local \`paper.pdf\`. Rebuild first (\`npx paperlint build papers/<paper>\`) if the local PDF is stale.
|
|
34
|
+
2. **Dry run.** \`npx paperlint submission update papers/<paper>\` — sends the PDF (add
|
|
35
|
+
\`--abstract <file>\` for a plain-text abstract, \`--submit\` to mark it submitted) with HotCRP's
|
|
36
|
+
\`dry_run=1\`: the portal checks the change and **keeps nothing**. Read \`valid\`, \`changes\` and every
|
|
37
|
+
\`portal says:\` line. Exit 1 means the portal would refuse it — report why, do not go on.
|
|
38
|
+
3. **Ask the author.** Show the dry run's output and ask for an explicit yes to change the real
|
|
39
|
+
submission.
|
|
40
|
+
4. **Save — only after that yes.** \`npx paperlint submission update papers/<paper> --save\` (same
|
|
41
|
+
flags as the dry run). 🔴 \`--save\` CHANGES A REAL SUBMISSION that reviewers will see. Never run it
|
|
42
|
+
without the author's explicit yes in this conversation, never "to test", and never because a
|
|
43
|
+
dry run was valid.
|
|
44
|
+
5. **Prove it landed.** \`npx paperlint submission show papers/<paper>\` again: \`match\` must read
|
|
45
|
+
\`yes — the portal holds this build\`, and the status must be what was intended (\`submitted\` if
|
|
46
|
+
\`--submit\` was sent). Anything else is not done — say so.
|
|
47
|
+
|
|
48
|
+
The same five steps as commands, in order (\`<paper>\` is the paper's folder):
|
|
49
|
+
|
|
50
|
+
\`\`\`bash
|
|
51
|
+
npx paperlint submission show papers/<paper>
|
|
52
|
+
npx paperlint submission update papers/<paper>
|
|
53
|
+
# … the author reads the dry run and says yes …
|
|
54
|
+
npx paperlint submission update papers/<paper> --save
|
|
55
|
+
npx paperlint submission show papers/<paper>
|
|
56
|
+
\`\`\`
|
|
57
|
+
|
|
58
|
+
## Reading the answers
|
|
59
|
+
|
|
60
|
+
- \`match yes\` is the only proof the reviewers get the local build; a status of \`submitted\` alone
|
|
61
|
+
says nothing about WHICH PDF.
|
|
62
|
+
- \`the portal refused (HTTP 401): …\` — the token is missing, wrong or expired; \`403\` — the token
|
|
63
|
+
lacks the scope, or the deadline has passed for this change. The text after the colon is
|
|
64
|
+
HotCRP's own; quote it.
|
|
65
|
+
- \`SAVE WITHHELD by the portal\` — HotCRP did not save; nothing changed.
|
|
66
|
+
|
|
67
|
+
## What this skill does not do
|
|
68
|
+
|
|
69
|
+
It does not decide whether the paper is ready (that is \`harden-paper\`), anonymize it, or fill the
|
|
70
|
+
submission form's other fields (authors, conflicts) — see \`submit-paper\`.`,
|
|
71
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* submission-portal — the PAID tier: does this skill's description actually fire?
|
|
3
|
+
*
|
|
4
|
+
* The prompts live in `lib/skill-trigger-cases.mjs` beside every other skill's, so collisions with
|
|
5
|
+
* `submit-paper` and `osf-artifact-upload` are visible in one table. Measures recall on its own
|
|
6
|
+
* territory and precision against those siblings, against the real harness.
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ NEVER RUN. Written 2026-10-03 with the skill; its rate is UNKNOWN, not assumed good.
|
|
9
|
+
*
|
|
10
|
+
* Costs money; not CI.
|
|
11
|
+
* node skills/submission-portal/submission-portal.eval.mjs [trials]
|
|
12
|
+
*/
|
|
13
|
+
import { runSkillTriggerEval } from "../../lib/skill-eval-kit.mjs";
|
|
14
|
+
|
|
15
|
+
await runSkillTriggerEval("submission-portal");
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* submission-portal — the free tier: the skill's commands are commands the CLI accepts, in the
|
|
3
|
+
* order the skill promises, and the skill passes the shared per-skill checks. No model, no network,
|
|
4
|
+
* no portal.
|
|
5
|
+
*
|
|
6
|
+
* 🔴 WHY THE COMMANDS ARE PARSED, NOT GREPPED: a skill that tells the agent to run a flag the CLI
|
|
7
|
+
* refuses fails at the worst moment — before a deadline, on a real submission. So every
|
|
8
|
+
* `paperlint submission …` the skill writes goes through the CLI's own argument parser and its
|
|
9
|
+
* usage check, and must come out runnable.
|
|
10
|
+
*/
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { test } from "vitest";
|
|
16
|
+
import { commandsIn } from "vigiles";
|
|
17
|
+
import { checkSkill } from "../../lib/skill-checks.mjs";
|
|
18
|
+
import { parseArgs } from "../../src/cli.ts";
|
|
19
|
+
import { usageProblem } from "../../src/submission.ts";
|
|
20
|
+
|
|
21
|
+
const SKILL = readFileSync(
|
|
22
|
+
join(dirname(fileURLToPath(import.meta.url)), "SKILL.md"),
|
|
23
|
+
"utf8",
|
|
24
|
+
);
|
|
25
|
+
const commands = commandsIn(
|
|
26
|
+
SKILL,
|
|
27
|
+
/(^|[|;&]\s*)npx paperlint submission\b/,
|
|
28
|
+
).map((c) => c.text);
|
|
29
|
+
|
|
30
|
+
/** A command as the CLI receives it: the words after `npx paperlint`. */
|
|
31
|
+
const argvOf = (command: string): string[] =>
|
|
32
|
+
command
|
|
33
|
+
.replace(/^.*?npx paperlint /, "")
|
|
34
|
+
.split(/\s+/)
|
|
35
|
+
.filter(Boolean);
|
|
36
|
+
|
|
37
|
+
test("the skill names show, a dry run and a save — and nothing else", () => {
|
|
38
|
+
const kinds = commands.map((c) => {
|
|
39
|
+
const a = parseArgs(argvOf(c));
|
|
40
|
+
return `${String(a.paths[0])}${a.save ? " --save" : ""}`;
|
|
41
|
+
});
|
|
42
|
+
assert.deepEqual([...new Set(kinds)], ["show", "update", "update --save"]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("every `paperlint submission` command in the skill is one the CLI runs", () => {
|
|
46
|
+
assert.ok(
|
|
47
|
+
commands.length > 0,
|
|
48
|
+
"the skill names no `paperlint submission` command",
|
|
49
|
+
);
|
|
50
|
+
for (const c of commands) {
|
|
51
|
+
const a = parseArgs(argvOf(c));
|
|
52
|
+
assert.equal(a.cmd, "submission", c);
|
|
53
|
+
assert.equal(a.unknownFlag, undefined, c);
|
|
54
|
+
assert.equal(a.missingValue, undefined, c);
|
|
55
|
+
const problem = usageProblem({
|
|
56
|
+
sub: a.paths[0],
|
|
57
|
+
paperDir: a.paths[1] ?? ".",
|
|
58
|
+
extra: a.paths.slice(2),
|
|
59
|
+
pdf: a.pdf,
|
|
60
|
+
abstract: a.abstract,
|
|
61
|
+
submit: a.submit,
|
|
62
|
+
save: a.save,
|
|
63
|
+
});
|
|
64
|
+
assert.equal(problem, null, c);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("🔴 the procedure reads, dry-runs, then saves, then reads again — never saves first", () => {
|
|
69
|
+
const order = commands.map((c) => {
|
|
70
|
+
const a = parseArgs(argvOf(c));
|
|
71
|
+
return a.paths[0] === "show" ? "show" : a.save ? "save" : "dry";
|
|
72
|
+
});
|
|
73
|
+
const firstSave = order.indexOf("save");
|
|
74
|
+
assert.ok(firstSave > order.indexOf("dry"), order.join(" → "));
|
|
75
|
+
assert.ok(order.indexOf("dry") > order.indexOf("show"), order.join(" → "));
|
|
76
|
+
assert.ok(order.lastIndexOf("show") > firstSave, order.join(" → "));
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("the shared per-skill checks pass", async () => {
|
|
80
|
+
await checkSkill("submission-portal");
|
|
81
|
+
});
|
|
@@ -4,7 +4,7 @@ description: End-to-end playbook for submitting a peer-reviewed paper to a doubl
|
|
|
4
4
|
allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, WebSearch, WebFetch, Skill]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<!-- vigiles:sha256:
|
|
7
|
+
<!-- vigiles:sha256:767a4757d1df67d3 compiled from skills/submit-paper/SKILL.md.spec.ts -->
|
|
8
8
|
|
|
9
9
|
# submit-paper — get a reviewed paper from "done" to "ready for review"
|
|
10
10
|
|
|
@@ -110,6 +110,7 @@ Many workshop HotCRPs have **no supplementary-upload field** — check first; if
|
|
|
110
110
|
is the step people miss.
|
|
111
111
|
- You can edit until the deadline: use **Replace** on the Submission field to swap the PDF for a late
|
|
112
112
|
fix (e.g. adding the artifact link after you host it). Submit early, upgrade before the deadline.
|
|
113
|
+
- When the venue preset declares its HotCRP portal, replacing the PDF or abstract and proving the portal holds the latest build is `submission-portal` (`paperlint submission show|update`).
|
|
113
114
|
|
|
114
115
|
## 6. Independent-researcher / dossier notes
|
|
115
116
|
- **Name consistency** is the quiet risk: publications, LinkedIn, passport, letters must resolve to one
|
|
@@ -126,6 +126,7 @@ Many workshop HotCRPs have **no supplementary-upload field** — check first; if
|
|
|
126
126
|
is the step people miss.
|
|
127
127
|
- You can edit until the deadline: use **Replace** on the Submission field to swap the PDF for a late
|
|
128
128
|
fix (e.g. adding the artifact link after you host it). Submit early, upgrade before the deadline.
|
|
129
|
+
- When the venue preset declares its HotCRP portal, replacing the PDF or abstract and proving the portal holds the latest build is \`submission-portal\` (\`paperlint submission show|update\`).
|
|
129
130
|
|
|
130
131
|
## 6. Independent-researcher / dossier notes
|
|
131
132
|
- **Name consistency** is the quiet risk: publications, LinkedIn, passport, letters must resolve to one
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# HotCRP cassette
|
|
2
|
+
|
|
3
|
+
Real answers to the requests `paperlint submission` sends, recorded from a live HotCRP instance
|
|
4
|
+
through HotCRP's documented API (`/api/paper`). `portal.test.ts` replays them from its
|
|
5
|
+
local mock server and parses each one through the adapter's real zod schemas: a schema that rejects
|
|
6
|
+
a real answer is the defect class these files exist to catch. A hand-written mock body is what let
|
|
7
|
+
a wrong upload field name through once.
|
|
8
|
+
|
|
9
|
+
Only read-only requests were recorded: one `GET` and two `POST`s with `dry_run=1` — nothing was
|
|
10
|
+
saved. Identifying values (the submission id, title, abstract, authors, topics, the PDF's hash, the
|
|
11
|
+
timestamps) are replaced with fakes; HotCRP's keys, value types, statuses and message texts are
|
|
12
|
+
verbatim.
|
|
13
|
+
|
|
14
|
+
| file | request | HTTP |
|
|
15
|
+
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ---: |
|
|
16
|
+
| `get-paper.json` | `GET /api/paper?p=<id>&word_limit=hard` with a token | 200 |
|
|
17
|
+
| `get-noauth.json` | the same without a token | 401 |
|
|
18
|
+
| `post-dot.json` | `POST /api/paper?p=<id>&dry_run=1`, PDF part named `paper.pdf`, `content_file` the same — PHP renames the field to `paper_pdf`, so HotCRP finds no upload | 200 |
|
|
19
|
+
| `post-ok.json` | the same with the part and `content_file` named `paperlint_pdf` | 200 |
|
|
20
|
+
|
|
21
|
+
To re-record: run the same four requests with `curl` against a HotCRP site where you have a
|
|
22
|
+
submission and a token (`Authorization: bearer $HOTCRP_TOKEN`), always with `dry_run=1` on a `POST`,
|
|
23
|
+
save each body, then replace every identifying value with a fake of the same type before committing.
|
|
24
|
+
These files ship in the npm package with the rest of `src/`, like the tests beside them.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ok": true,
|
|
3
|
+
"paper": {
|
|
4
|
+
"object": "paper",
|
|
5
|
+
"pid": 7,
|
|
6
|
+
"title": "An Example Paper Title",
|
|
7
|
+
"submission": {
|
|
8
|
+
"mimetype": "application/pdf",
|
|
9
|
+
"hash": "sha2-b3e602c135f2455f64aecfdc2e1e7f81dad20c5d69f0981e965b1a0131d47163",
|
|
10
|
+
"timestamp": 1790000000,
|
|
11
|
+
"size": 17,
|
|
12
|
+
"pages": 2
|
|
13
|
+
},
|
|
14
|
+
"abstract": "An example abstract of exactly ten words for the test.",
|
|
15
|
+
"contacts": [
|
|
16
|
+
{
|
|
17
|
+
"email": "author@example.org",
|
|
18
|
+
"given_name": "A.",
|
|
19
|
+
"family_name": "Author",
|
|
20
|
+
"affiliation": "Example University"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"paper_type": "Example paper type",
|
|
24
|
+
"authors": [
|
|
25
|
+
{
|
|
26
|
+
"email": "author@example.org",
|
|
27
|
+
"given_name": "A.",
|
|
28
|
+
"family_name": "Author",
|
|
29
|
+
"affiliation": "Example University"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"topics": ["Example topic one", "Example topic two"],
|
|
33
|
+
"pc_conflicts": {},
|
|
34
|
+
"status": "submitted",
|
|
35
|
+
"submitted": true,
|
|
36
|
+
"submitted_at": 1790000100,
|
|
37
|
+
"modified_at": 1790000100
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ok": false,
|
|
3
|
+
"message_list": [
|
|
4
|
+
{
|
|
5
|
+
"status": 2,
|
|
6
|
+
"field": "submission",
|
|
7
|
+
"message": "<0>Ignored attempt to upload document without any content"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"status": 3,
|
|
11
|
+
"field": "submission"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"status": -3
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"dry_run": true,
|
|
18
|
+
"valid": false,
|
|
19
|
+
"change_list": [],
|
|
20
|
+
"pid": 7
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HotCRP, as the `SubmissionPortal` port: its REST API read and written over `fetch`, its answers
|
|
3
|
+
* parsed here. Everything that changes if HotCRP's API does lives in this folder.
|
|
4
|
+
*/
|
|
5
|
+
export {
|
|
6
|
+
CONTENT_FIELD,
|
|
7
|
+
changeObject,
|
|
8
|
+
hotcrpPortal,
|
|
9
|
+
type HotcrpOptions,
|
|
10
|
+
} from "./portal.io.ts";
|
|
11
|
+
export { parseShow, parseUpdate, sha256OfHash } from "./response.ts";
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HotCRP's REST API (`<site>/api/paper`) as the `SubmissionPortal` port, over Node's `fetch`.
|
|
3
|
+
*
|
|
4
|
+
* Read: `GET /api/paper?p=<id>&word_limit=hard` (scope `submeta:read`; `word_limit=hard` returns the
|
|
5
|
+
* whole abstract instead of one cut at the soft limit).
|
|
6
|
+
*
|
|
7
|
+
* Change: `POST /api/paper?p=<id>` as multipart/form-data — a `json` field holding a submission
|
|
8
|
+
* object, and the PDF as a file part whose field name is the object's `submission.content_file`
|
|
9
|
+
* (the documented form: `curl -F "json=<data.json" -F paper.pdf=@paper.pdf SITEURL/api/paper`).
|
|
10
|
+
* Without `save` the request carries `dry_run=1`: HotCRP checks the change and keeps nothing.
|
|
11
|
+
*
|
|
12
|
+
* 🔴 THE TOKEN goes in the `Authorization` header and nowhere else: not into a URL, a message, or an
|
|
13
|
+
* error. A failure is described by HTTP status, HotCRP's own messages, or the network error's text.
|
|
14
|
+
*/
|
|
15
|
+
import { messageOf } from "../../domain/text.ts";
|
|
16
|
+
import { err, type Result } from "../../domain/result.ts";
|
|
17
|
+
import type {
|
|
18
|
+
PortalFailure,
|
|
19
|
+
SubmissionChange,
|
|
20
|
+
} from "../../domain/submission.ts";
|
|
21
|
+
import type { SubmissionPortal } from "../../ports/submission-portal.ts";
|
|
22
|
+
import { parseShow, parseUpdate } from "./response.ts";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The form field the PDF travels in, and the name the `json` object's `content_file` refers to it
|
|
26
|
+
* by. 🔴 NO DOT, SPACE OR BRACKET: HotCRP looks the name up in PHP's `$_FILES`, and PHP renames a
|
|
27
|
+
* dot or a space in a field name to `_`. The documented `-F paper.pdf=@paper.pdf` therefore arrives
|
|
28
|
+
* as `paper_pdf`, the lookup of `paper.pdf` misses, and HotCRP answers "Ignored attempt to upload
|
|
29
|
+
* document without any content" with `valid: false` — measured on a real HotCRP, 2026-10-03.
|
|
30
|
+
*/
|
|
31
|
+
export const CONTENT_FIELD = "paperlint_pdf";
|
|
32
|
+
|
|
33
|
+
/** Uploads take a while on a slow link; a read should not. */
|
|
34
|
+
const READ_TIMEOUT_MS = 60_000;
|
|
35
|
+
const WRITE_TIMEOUT_MS = 300_000;
|
|
36
|
+
|
|
37
|
+
export interface HotcrpOptions {
|
|
38
|
+
/** The site's base URL, no trailing slash: `https://example.org`. */
|
|
39
|
+
readonly url: string;
|
|
40
|
+
readonly token: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type Answer = Result<
|
|
44
|
+
{ readonly status: number; readonly body: unknown },
|
|
45
|
+
PortalFailure
|
|
46
|
+
>;
|
|
47
|
+
|
|
48
|
+
/** One request → HTTP status and parsed JSON body, or why there is neither. */
|
|
49
|
+
async function call(
|
|
50
|
+
url: string,
|
|
51
|
+
init: { readonly method: string; readonly body?: Readonly<FormData> },
|
|
52
|
+
token: string,
|
|
53
|
+
timeoutMs: number,
|
|
54
|
+
): Promise<Answer> {
|
|
55
|
+
try {
|
|
56
|
+
const r = await fetch(url, {
|
|
57
|
+
...init,
|
|
58
|
+
headers: { Authorization: `bearer ${token}`, Accept: "application/json" },
|
|
59
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
60
|
+
});
|
|
61
|
+
const text = await r.text();
|
|
62
|
+
try {
|
|
63
|
+
return { ok: true, value: { status: r.status, body: JSON.parse(text) } };
|
|
64
|
+
} catch {
|
|
65
|
+
return err({
|
|
66
|
+
kind: "malformed",
|
|
67
|
+
httpStatus: r.status,
|
|
68
|
+
detail: `the answer is not JSON (${r.headers.get("content-type") ?? "no content type"})`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return err({ kind: "unreachable", detail: messageOf(e) });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The submission object a change sends: only the fields it changes. */
|
|
77
|
+
export function changeObject(
|
|
78
|
+
id: number,
|
|
79
|
+
c: SubmissionChange,
|
|
80
|
+
): Readonly<Record<string, unknown>> {
|
|
81
|
+
return {
|
|
82
|
+
object: "paper",
|
|
83
|
+
pid: id,
|
|
84
|
+
...(c.pdf === null ? {} : { submission: { content_file: CONTENT_FIELD } }),
|
|
85
|
+
...(c.abstract === null ? {} : { abstract: c.abstract }),
|
|
86
|
+
...(c.submit ? { status: "submitted" } : {}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function formOf(id: number, c: SubmissionChange): Readonly<FormData> {
|
|
91
|
+
const form = new FormData();
|
|
92
|
+
form.append("json", JSON.stringify(changeObject(id, c)));
|
|
93
|
+
if (c.pdf !== null)
|
|
94
|
+
form.append(
|
|
95
|
+
CONTENT_FIELD,
|
|
96
|
+
new Blob([new Uint8Array(c.pdf.bytes)], { type: "application/pdf" }),
|
|
97
|
+
c.pdf.name,
|
|
98
|
+
);
|
|
99
|
+
return form;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const endpoint = (
|
|
103
|
+
base: string,
|
|
104
|
+
params: Readonly<Record<string, string>>,
|
|
105
|
+
): string => `${base}/api/paper?${new URLSearchParams(params).toString()}`;
|
|
106
|
+
|
|
107
|
+
/** HotCRP at `o.url`, read and changed with `o.token`. */
|
|
108
|
+
export function hotcrpPortal(o: HotcrpOptions): SubmissionPortal {
|
|
109
|
+
return {
|
|
110
|
+
async show(id) {
|
|
111
|
+
const url = endpoint(o.url, { p: String(id), word_limit: "hard" });
|
|
112
|
+
const a = await call(url, { method: "GET" }, o.token, READ_TIMEOUT_MS);
|
|
113
|
+
return a.ok ? parseShow(a.value.status, a.value.body) : a;
|
|
114
|
+
},
|
|
115
|
+
async update(id, change, { save }) {
|
|
116
|
+
const url = endpoint(o.url, {
|
|
117
|
+
p: String(id),
|
|
118
|
+
...(save ? {} : { dry_run: "1" }),
|
|
119
|
+
});
|
|
120
|
+
const body = formOf(id, change);
|
|
121
|
+
const a = await call(
|
|
122
|
+
url,
|
|
123
|
+
{ method: "POST", body },
|
|
124
|
+
o.token,
|
|
125
|
+
WRITE_TIMEOUT_MS,
|
|
126
|
+
);
|
|
127
|
+
return a.ok ? parseUpdate(a.value.status, a.value.body) : a;
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|