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
|
@@ -17,17 +17,18 @@
|
|
|
17
17
|
*
|
|
18
18
|
* A `.txt` is the text of the rendered PDF — the page a reviewer actually reads.
|
|
19
19
|
*
|
|
20
|
-
* The figure captions are measured too
|
|
20
|
+
* The figure captions are measured too, in the report: every `\caption{}` in `paper.tex` and the
|
|
21
21
|
* files it includes, or, beside a rendered page, in `figures/*.tex`.
|
|
22
22
|
*
|
|
23
|
-
* Advisory:
|
|
23
|
+
* Advisory: the report prints every check and exits 0. `--flags-only` prints the checks the report
|
|
24
|
+
* marks FLAG, each with the lines under it, and nothing on a clean paper; it exits 1 when there is
|
|
25
|
+
* one — the form for a hook.
|
|
24
26
|
*/
|
|
25
27
|
import { paperProse } from "#src/paper-prose";
|
|
26
28
|
import { paperRecord } from "#src/paper-record";
|
|
27
|
-
import { latexReader } from "#src/adapters/latex/index";
|
|
29
|
+
import { captionsOf, latexReader, parseLatex } from "#src/adapters/latex/index";
|
|
28
30
|
import { nodeFiles } from "#src/adapters/node/index";
|
|
29
31
|
import { sourcesCodec } from "#src/adapters/sources-record/index";
|
|
30
|
-
import { texToMdast } from "#eslint-rules/latex-language";
|
|
31
32
|
|
|
32
33
|
const THRESHOLDS = {
|
|
33
34
|
metadiscourseTotalPer1000: {
|
|
@@ -363,77 +364,116 @@ function figureFiles(pagePath, fs, path) {
|
|
|
363
364
|
}
|
|
364
365
|
|
|
365
366
|
/**
|
|
366
|
-
* Every `\caption
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
* for the rules that read notes. So a caption is skipped when its argument is blank in the
|
|
370
|
-
* projection or it starts inside a comment: a quoted or commented-out `\caption` is not one.
|
|
367
|
+
* Every `\caption` of `sources` (`{ file, text }`) a reader sees, measured. The captions come from the
|
|
368
|
+
* parsed source (`captionsOf`, src/adapters/latex/captions.ts): its optional argument, star and spacing
|
|
369
|
+
* are the parser's, and a caption that is commented out, in verbatim or in a macro's definition is not one.
|
|
371
370
|
*/
|
|
372
371
|
function captions(sources) {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
depth = 1,
|
|
382
|
-
j = i;
|
|
383
|
-
while (j < s.length && depth) {
|
|
384
|
-
if (s[j] === "{") depth++;
|
|
385
|
-
else if (s[j] === "}") depth--;
|
|
386
|
-
j++;
|
|
387
|
-
}
|
|
388
|
-
if (
|
|
389
|
-
!/\S/.test(seen.slice(i, j - 1)) ||
|
|
390
|
-
comments.some(([a, b]) => m.index >= a && m.index < b)
|
|
391
|
-
)
|
|
392
|
-
continue;
|
|
393
|
-
const text = s
|
|
394
|
-
.slice(i, j - 1)
|
|
395
|
-
.replace(/\\[a-zA-Z]+\s*/g, " ")
|
|
396
|
-
.replace(/[{}]/g, " ")
|
|
397
|
-
.replace(/---/g, " ")
|
|
398
|
-
.replace(/\s+/g, " ")
|
|
399
|
-
.trim();
|
|
400
|
-
if (!text) continue;
|
|
401
|
-
// A non-empty caption splits into at least one sentence.
|
|
402
|
-
const sents = splitSentences(text);
|
|
403
|
-
out.push({
|
|
404
|
-
file: f,
|
|
405
|
-
words: words(text),
|
|
372
|
+
return sources.flatMap(({ file, text }) =>
|
|
373
|
+
captionsOf(parseLatex(text)).map((raw) => {
|
|
374
|
+
// A dash with no spaces round it joins two words in the count, as it never did here.
|
|
375
|
+
const caption = raw.replace(/---/g, " ");
|
|
376
|
+
const sents = splitSentences(caption);
|
|
377
|
+
return {
|
|
378
|
+
file,
|
|
379
|
+
words: words(caption),
|
|
406
380
|
longest: Math.max(...sents.map(words)),
|
|
407
381
|
longestSentence: sents.slice().sort((a, b) => words(b) - words(a))[0],
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
|
|
382
|
+
};
|
|
383
|
+
}),
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* The findings on the captions of the file `f`: one line per caption sentence or caption over its
|
|
389
|
+
* limit. A paper's captions are in its own files — paper.tex and what it includes, wherever a figure
|
|
390
|
+
* is set; a rendered page has the `.tex` of `figures/` beside it. The report and `--flags-only` both
|
|
391
|
+
* read them from here.
|
|
392
|
+
*/
|
|
393
|
+
function captionFindings(f, tex, fs, path) {
|
|
394
|
+
return captions(tex ? tex.files : figureFiles(f, fs, path)).flatMap((c) =>
|
|
395
|
+
CAPTION_LIMITS.filter(over(c)).map(say(c)),
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* The two limits a caption is held to: the `THRESHOLDS` key, what of a caption it is measured on, and
|
|
401
|
+
* how a finding reads. A caption is over a limit when its measure exceeds the threshold's `warn`.
|
|
402
|
+
*/
|
|
403
|
+
const CAPTION_LIMITS = [
|
|
404
|
+
{
|
|
405
|
+
key: "captionSentenceWords",
|
|
406
|
+
measure: (c) => c.longest,
|
|
407
|
+
say: (c, warn) =>
|
|
408
|
+
`${c.file}: caption sentence of ${c.longest} words (warn >${warn}) — "${c.longestSentence.slice(0, 110)}…"`,
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
key: "captionWords",
|
|
412
|
+
measure: (c) => c.words,
|
|
413
|
+
say: (c, warn) =>
|
|
414
|
+
`${c.file}: caption is ${c.words} words (warn >${warn}) — a caption this long is a section under a picture`,
|
|
415
|
+
},
|
|
416
|
+
];
|
|
417
|
+
const over = (c) => (limit) => limit.measure(c) > THRESHOLDS[limit.key].warn;
|
|
418
|
+
const say = (c) => (limit) => limit.say(c, THRESHOLDS[limit.key].warn);
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Every line of the report as data, in the order it is printed: `ok` is true (passed), false (a
|
|
422
|
+
* finding, marked `FLAG`) or null (a figure, no check), `under` the lines that name the offenders.
|
|
423
|
+
* The report prints all of it; `--flags-only` prints the findings: one list, two renderings.
|
|
424
|
+
*/
|
|
425
|
+
function checks(r, captionLines) {
|
|
426
|
+
const T = THRESHOLDS;
|
|
427
|
+
return [
|
|
428
|
+
{
|
|
429
|
+
ok: r.metadiscoursePer1000 <= T.metadiscourseTotalPer1000.warn,
|
|
430
|
+
text: `all metadiscourse/1000w ${r.metadiscoursePer1000} (Hyland RA baseline 64.9, warn >${T.metadiscourseTotalPer1000.warn})`,
|
|
431
|
+
under: [],
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
ok: null,
|
|
435
|
+
text: `nominalisations/1000w ${r.nominalisationsPer1000}`,
|
|
436
|
+
under: [],
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
ok: r.contrastivePer10k <= T.contrastivePer10k.warn,
|
|
440
|
+
text: `"rather than"/instead-of/10,000w ${r.contrastivePer10k} (warn >${T.contrastivePer10k.warn}; not epanorthosis, no published baseline)`,
|
|
441
|
+
under: [],
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
ok: r.wastedStress.length === 0,
|
|
445
|
+
text: `sentences ending on a cross-ref/citation/hedge (wasted stress position): ${r.wastedStress.length}`,
|
|
446
|
+
under: r.wastedStress.map((s) => `…${s.slice(-70)}`),
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
ok: r.negatedSentencePct <= T.negatedMainClausePct.warn,
|
|
450
|
+
text: `sentences whose main claim is a negation: ${r.negatedSentencePct}%`,
|
|
451
|
+
under: [],
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
ok: captionLines.length === 0,
|
|
455
|
+
text: `figure captions over the limit: ${captionLines.length}`,
|
|
456
|
+
under: captionLines,
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
ok: null,
|
|
460
|
+
text: `topic positions (whose story is this?): ${r.topics.map((t) => `"${t}"`).join(" | ")}`,
|
|
461
|
+
under: [],
|
|
462
|
+
},
|
|
463
|
+
];
|
|
412
464
|
}
|
|
413
465
|
|
|
414
|
-
|
|
415
|
-
|
|
466
|
+
const MARK = { true: "ok ", false: "FLAG", null: " " };
|
|
467
|
+
|
|
468
|
+
function report(name, r, rows) {
|
|
416
469
|
console.log(`\n=== ${name} — ${r.words} words, ${r.sentences} sentences ===`);
|
|
417
470
|
console.log(
|
|
418
471
|
`sentence lengths ${JSON.stringify(r.sentenceLens)} mean ${r.meanLen} sd ${r.sdLen} CV ${r.cv}`,
|
|
419
472
|
);
|
|
420
|
-
|
|
421
|
-
`${
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
console.log(
|
|
425
|
-
`${flag(r.contrastivePer10k <= THRESHOLDS.contrastivePer10k.warn)} "rather than"/instead-of/10,000w ${r.contrastivePer10k} (warn >${THRESHOLDS.contrastivePer10k.warn}; not epanorthosis, no published baseline)`,
|
|
426
|
-
);
|
|
427
|
-
console.log(
|
|
428
|
-
`${flag(r.wastedStress.length === 0)} sentences ending on a cross-ref/citation/hedge (wasted stress position): ${r.wastedStress.length}`,
|
|
429
|
-
);
|
|
430
|
-
r.wastedStress.forEach((s) => console.log(` > …${s.slice(-70)}`));
|
|
431
|
-
console.log(
|
|
432
|
-
`${flag(r.negatedSentencePct <= THRESHOLDS.negatedMainClausePct.warn)} sentences whose main claim is a negation: ${r.negatedSentencePct}%`,
|
|
433
|
-
);
|
|
434
|
-
console.log(
|
|
435
|
-
` topic positions (whose story is this?): ${r.topics.map((t) => `"${t}"`).join(" | ")}`,
|
|
436
|
-
);
|
|
473
|
+
rows.forEach(({ ok, text, under }) => {
|
|
474
|
+
console.log(`${MARK[ok]} ${text}`);
|
|
475
|
+
under.forEach((l) => console.log(` > ${l}`));
|
|
476
|
+
});
|
|
437
477
|
}
|
|
438
478
|
|
|
439
479
|
const args = process.argv.slice(2);
|
|
@@ -474,14 +514,14 @@ function pdfTextOnly(t) {
|
|
|
474
514
|
.replace(/\f/g, "\n\n");
|
|
475
515
|
}
|
|
476
516
|
|
|
477
|
-
//
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
517
|
+
// `--flags-only` prints the findings of the report (every line it marks FLAG, with the lines under
|
|
518
|
+
// it) and nothing on a clean paper, and exits 1 when there is one: the form for a hook. The report
|
|
519
|
+
// prints all of `checks` and exits 0.
|
|
520
|
+
const mode = args.includes("--flags-only")
|
|
521
|
+
? "flags"
|
|
522
|
+
: args.includes("--headings")
|
|
523
|
+
? "headings"
|
|
524
|
+
: "report";
|
|
485
525
|
let flagged = 0;
|
|
486
526
|
for (const f of args.filter((a) => !a.startsWith("--"))) {
|
|
487
527
|
const raw = fs.readFileSync(f, "utf8");
|
|
@@ -509,33 +549,25 @@ for (const f of args.filter((a) => !a.startsWith("--"))) {
|
|
|
509
549
|
process.exit(2);
|
|
510
550
|
}
|
|
511
551
|
const r = analyse(text, sentences);
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if (c.longest > THRESHOLDS.captionSentenceWords.warn)
|
|
518
|
-
lines.push(
|
|
519
|
-
` ${c.file}: caption sentence of ${c.longest} words (warn >${THRESHOLDS.captionSentenceWords.warn}) — "${c.longestSentence.slice(0, 110)}…"`,
|
|
520
|
-
);
|
|
521
|
-
if (c.words > THRESHOLDS.captionWords.warn)
|
|
522
|
-
lines.push(
|
|
523
|
-
` ${c.file}: caption is ${c.words} words (warn >${THRESHOLDS.captionWords.warn}) — a caption this long is a section under a picture`,
|
|
524
|
-
);
|
|
525
|
-
}
|
|
526
|
-
if (lines.length) {
|
|
527
|
-
flagged += lines.length;
|
|
552
|
+
const rows = () => checks(r, captionFindings(f, tex, fs, path));
|
|
553
|
+
if (mode === "flags") {
|
|
554
|
+
const found = rows().filter((row) => row.ok === false);
|
|
555
|
+
if (found.length) {
|
|
556
|
+
flagged += found.length;
|
|
528
557
|
// The header STATES the count, so a caller need not count output lines — header and footer
|
|
529
558
|
// are not findings.
|
|
530
559
|
console.error(
|
|
531
|
-
`✍️ prose-lint — ${f.split("/").pop()} — ${
|
|
560
|
+
`✍️ prose-lint — ${f.split("/").pop()} — ${found.length} finding(s):`,
|
|
532
561
|
);
|
|
533
|
-
|
|
562
|
+
found.forEach(({ text, under }) => {
|
|
563
|
+
console.error(` ${text}`);
|
|
564
|
+
under.forEach((l) => console.error(` > ${l}`));
|
|
565
|
+
});
|
|
534
566
|
console.error(
|
|
535
|
-
" run prose-lint.mjs on the file without --flags-only for
|
|
567
|
+
" run prose-lint.mjs on the file without --flags-only for every metric",
|
|
536
568
|
);
|
|
537
569
|
}
|
|
538
|
-
} else if (
|
|
570
|
+
} else if (mode === "headings") {
|
|
539
571
|
// The whole-document question, printed as one list. Not a gate: "does this heading name a
|
|
540
572
|
// concrete noun" is judgement and pretending otherwise would be the exact failure this file
|
|
541
573
|
// exists to stop. What was missing is cheaper and was the real cause — nobody had ever seen the
|
|
@@ -558,10 +590,10 @@ for (const f of args.filter((a) => !a.startsWith("--"))) {
|
|
|
558
590
|
'a linter, a configuration, a repository, a rule — not "answers", "kinds", "moves".',
|
|
559
591
|
);
|
|
560
592
|
} else {
|
|
561
|
-
report(f.split("/").pop(), r);
|
|
593
|
+
report(f.split("/").pop(), r, rows());
|
|
562
594
|
}
|
|
563
595
|
}
|
|
564
|
-
if (
|
|
596
|
+
if (mode !== "flags")
|
|
565
597
|
console.log(
|
|
566
598
|
"\n(thresholds and their sources are in THRESHOLDS at the top of this file)",
|
|
567
599
|
);
|
|
@@ -8,7 +8,7 @@ import { join } from "node:path";
|
|
|
8
8
|
import { dirname } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { expect, test } from "vitest";
|
|
11
|
-
import { cpSync } from "node:fs";
|
|
11
|
+
import { cpSync, readFileSync } from "node:fs";
|
|
12
12
|
import { layRecord, recordedTree } from "../../test/recorded-fixture.ts";
|
|
13
13
|
import { runNode, useTempDir, writeTree } from "../../test/support.ts";
|
|
14
14
|
|
|
@@ -62,6 +62,16 @@ const TREE = {
|
|
|
62
62
|
"commented/paper.tex": tex(
|
|
63
63
|
`Prose here.\n% \\caption{Commented ${LONG_CAPTION_SENTENCE}.}\n\\begin{verbatim}\n\\caption{Quoted ${LONG_CAPTION_SENTENCE}.}\n\\end{verbatim}\n\\begin{figure}\\caption{Real ${LONG_CAPTION_SENTENCE}.}\\end{figure}`,
|
|
64
64
|
),
|
|
65
|
+
// The caption command with an optional argument, and with a space before its brace.
|
|
66
|
+
"optarg/paper.tex": tex(
|
|
67
|
+
`Prose here. \\begin{figure}\\caption[Short]{${LONG_CAPTION_SENTENCE}.}\\end{figure}`,
|
|
68
|
+
),
|
|
69
|
+
"spaced/paper.tex": tex(
|
|
70
|
+
`Prose here. \\begin{figure}\\caption {${LONG_CAPTION_SENTENCE}.}\\end{figure}`,
|
|
71
|
+
),
|
|
72
|
+
"negated/paper.tex": tex(
|
|
73
|
+
"It is not fast. It is not small. It is not red. Nothing here is plain.",
|
|
74
|
+
),
|
|
65
75
|
"ends-on-marks/paper.tex": tex(
|
|
66
76
|
"The method holds on every input~\\cite{knuth}. The proof is given in full (Section~\\ref{proof}). The result is new.",
|
|
67
77
|
),
|
|
@@ -83,6 +93,9 @@ const READ: Readonly<Record<string, readonly string[]>> = {
|
|
|
83
93
|
inline: ["paper.tex", "sec/results.tex"],
|
|
84
94
|
commented: ["paper.tex"],
|
|
85
95
|
"ends-on-marks": ["paper.tex"],
|
|
96
|
+
negated: ["paper.tex"],
|
|
97
|
+
optarg: ["paper.tex"],
|
|
98
|
+
spaced: ["paper.tex"],
|
|
86
99
|
empty: ["paper.tex"],
|
|
87
100
|
};
|
|
88
101
|
writeTree(
|
|
@@ -182,6 +195,23 @@ test("--flags-only: an over-long caption flags and exits 1", () => {
|
|
|
182
195
|
expect(r.stderr).toMatchSnapshot();
|
|
183
196
|
});
|
|
184
197
|
|
|
198
|
+
test("--flags-only: a metric over its threshold (not a caption) is a finding too, and the report flags the same line", () => {
|
|
199
|
+
const file = join(root, "negated", "paper.tex");
|
|
200
|
+
const only = runNode(SCRIPT, ["--flags-only", file]);
|
|
201
|
+
const report = runNode(SCRIPT, [file]);
|
|
202
|
+
// Guards: the mode promised "print nothing on a clean paper, any output means a finding" and
|
|
203
|
+
// looked only at captions; the metrics the report FLAGs are findings of the same kind.
|
|
204
|
+
assert.deepEqual(
|
|
205
|
+
{ status: only.status, stdout: only.stdout },
|
|
206
|
+
{ status: 1, stdout: "" },
|
|
207
|
+
);
|
|
208
|
+
expect(only.stderr).toMatchSnapshot();
|
|
209
|
+
assert.match(
|
|
210
|
+
report.stdout,
|
|
211
|
+
/^FLAG sentences whose main claim is a negation: 75%$/m,
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
185
215
|
test("--flags-only: a paper with no figures directory is clean, exit 0 and silent", () => {
|
|
186
216
|
assert.deepEqual(
|
|
187
217
|
runNode(SCRIPT, ["--flags-only", join(root, "clean", "paper.tex")]),
|
|
@@ -248,17 +278,13 @@ test("🔴 --flags-only reads the captions of the paper.tex and of the files it
|
|
|
248
278
|
stdout: r.stdout,
|
|
249
279
|
flagged: r.stderr
|
|
250
280
|
.split("\n")
|
|
251
|
-
.filter((l) => l.startsWith("
|
|
281
|
+
.filter((l) => l.startsWith(" > "))
|
|
252
282
|
.map((l) => l.split(":")[0]),
|
|
253
283
|
},
|
|
254
284
|
{
|
|
255
285
|
status: 1,
|
|
256
286
|
stdout: "",
|
|
257
|
-
flagged: [
|
|
258
|
-
" paper.tex",
|
|
259
|
-
" sec/results.tex",
|
|
260
|
-
" run prose-lint.mjs on the file without --flags-only for the sentences",
|
|
261
|
-
],
|
|
287
|
+
flagged: [" > paper.tex", " > sec/results.tex"],
|
|
262
288
|
},
|
|
263
289
|
);
|
|
264
290
|
});
|
|
@@ -274,8 +300,14 @@ test("--flags-only on a rendered page reads the captions in figures/*.tex beside
|
|
|
274
300
|
join(root, "rendered", "page.txt"),
|
|
275
301
|
]);
|
|
276
302
|
assert.deepEqual(
|
|
277
|
-
{
|
|
278
|
-
|
|
303
|
+
{
|
|
304
|
+
status: r.status,
|
|
305
|
+
first: r.stderr
|
|
306
|
+
.split("\n")
|
|
307
|
+
.find((l) => l.startsWith(" > "))
|
|
308
|
+
?.split(":")[0],
|
|
309
|
+
},
|
|
310
|
+
{ status: 1, first: " > figures/f.tex" },
|
|
279
311
|
);
|
|
280
312
|
// …and a page with no figures/ beside it has no caption to flag.
|
|
281
313
|
assert.deepEqual(runNode(SCRIPT, ["--flags-only", join(root, "page.txt")]), {
|
|
@@ -296,9 +328,86 @@ test("🔴 a caption commented out or quoted in verbatim is not measured; a real
|
|
|
296
328
|
status: r.status,
|
|
297
329
|
findings: r.stderr
|
|
298
330
|
.split("\n")
|
|
299
|
-
.filter((l) => l.startsWith("
|
|
331
|
+
.filter((l) => l.startsWith(" > paper.tex:"))
|
|
300
332
|
.map((l) => /— "(\w+)/.exec(l)?.[1]),
|
|
301
333
|
},
|
|
302
334
|
{ status: 1, findings: ["Real"] },
|
|
303
335
|
);
|
|
304
336
|
});
|
|
337
|
+
|
|
338
|
+
/** The `prose-lint.mjs` invocations SKILL.md documents (Step 6), their arguments with `<paper-dir>` filled in. */
|
|
339
|
+
const documentedInvocations = (paperDir: string): readonly string[][] =>
|
|
340
|
+
readFileSync(join(dirname(SCRIPT), "SKILL.md"), "utf8")
|
|
341
|
+
.split("\n")
|
|
342
|
+
.flatMap((line) => {
|
|
343
|
+
const m =
|
|
344
|
+
/node \$\{CLAUDE_SKILL_DIR\}\/prose-lint\.mjs ([^#]+?)\s*(#|$)/.exec(
|
|
345
|
+
line,
|
|
346
|
+
);
|
|
347
|
+
return m?.[1] === undefined
|
|
348
|
+
? []
|
|
349
|
+
: [m[1].replaceAll("<paper-dir>", paperDir).split(/\s+/)];
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("🔴 the invocation SKILL.md documents reports an over-long caption, with no flag the skill does not name", () => {
|
|
353
|
+
const documented = documentedInvocations(join(root, "p"));
|
|
354
|
+
// Guards: the report is the documented way to run the checks (Step 6), and captions were
|
|
355
|
+
// measured only under an undocumented flag — an over-long caption produced no finding.
|
|
356
|
+
const report = documented.find((a) => !a.includes("--headings"));
|
|
357
|
+
assert.ok(report !== undefined, "SKILL.md documents a report invocation");
|
|
358
|
+
const r = runNode(SCRIPT, report);
|
|
359
|
+
assert.deepEqual(
|
|
360
|
+
{
|
|
361
|
+
status: r.status,
|
|
362
|
+
flagged: r.stdout
|
|
363
|
+
.split("\n")
|
|
364
|
+
.filter(
|
|
365
|
+
(l) => l.startsWith("FLAG figure captions") || l.includes("caption"),
|
|
366
|
+
),
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
status: 0,
|
|
370
|
+
flagged: [
|
|
371
|
+
"FLAG figure captions over the limit: 2",
|
|
372
|
+
' > figures/fig1.tex: caption sentence of 46 words (warn >40) — "Big. word0 word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12 word13 word14 word15 wor…"',
|
|
373
|
+
" > figures/fig1.tex: caption is 106 words (warn >100) — a caption this long is a section under a picture",
|
|
374
|
+
],
|
|
375
|
+
},
|
|
376
|
+
);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("the report says a paper whose captions are within the limits is clean on that line", () => {
|
|
380
|
+
const r = runNode(SCRIPT, [join(root, "clean", "paper.tex")]);
|
|
381
|
+
// Guards: the line is printed on a clean paper too, so its absence is never read as a pass.
|
|
382
|
+
assert.match(r.stdout, /^ok {3}figure captions over the limit: 0$/m);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("🔴 a caption with an optional argument, or a space before its brace, is measured", () => {
|
|
386
|
+
const found = (dir: string) => {
|
|
387
|
+
const r = runNode(SCRIPT, [join(root, dir, "paper.tex")]);
|
|
388
|
+
return [
|
|
389
|
+
r.status,
|
|
390
|
+
/^FLAG figure captions over the limit: (\d+)$/m.exec(r.stdout)?.[1],
|
|
391
|
+
];
|
|
392
|
+
};
|
|
393
|
+
// Guards: the scan matched `\caption{` only, so `\caption[Short]{…}` and `\caption {…}` were never
|
|
394
|
+
// read and the report printed "ok ... 0" for a long caption.
|
|
395
|
+
expect([found("optarg"), found("spaced")]).toEqual([
|
|
396
|
+
[0, "1"],
|
|
397
|
+
[0, "1"],
|
|
398
|
+
]);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("🔴 the caption of the package's own p1 fixture, which has an optional argument, is reported", () => {
|
|
402
|
+
const p1 = join(
|
|
403
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
404
|
+
"..",
|
|
405
|
+
"..",
|
|
406
|
+
"fixtures",
|
|
407
|
+
"paper-sources",
|
|
408
|
+
"p1",
|
|
409
|
+
"paper.tex",
|
|
410
|
+
);
|
|
411
|
+
const r = runNode(SCRIPT, ["--flags-only", p1]);
|
|
412
|
+
assert.match(r.stderr, /— "Optional-arg caption this sentence/);
|
|
413
|
+
});
|
|
@@ -1,69 +1,85 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: submission-portal
|
|
3
|
-
description: "
|
|
3
|
+
description: "Create, read or change a paper's submission on the venue's HotCRP portal via `paperlint submission new|show|update`: register the paper, replace the PDF or abstract, mark it submitted, and verify by sha256 that the portal holds the latest build. Use when the paper is not yet on HotCRP, when its PDF or abstract there needs replacing, when asked whether it is truly submitted, or before a deadline. Composes with submit-paper (what to submit)."
|
|
4
4
|
allowed-tools: [Read, Grep, Glob, Bash]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<!-- vigiles:sha256:
|
|
7
|
+
<!-- vigiles:sha256:03bc35623dbd7911 compiled from skills/submission-portal/SKILL.md.spec.ts -->
|
|
8
8
|
|
|
9
|
-
# submission-portal — read and update the submission on the portal, then prove it landed
|
|
9
|
+
# submission-portal — create, read and update the submission on the portal, then prove it landed
|
|
10
10
|
|
|
11
|
-
`paperlint submission`
|
|
12
|
-
supported; for any other portal the command says so and the upload is done by hand.
|
|
11
|
+
`paperlint submission` creates, reads and updates the paper's submission on the venue's portal.
|
|
12
|
+
Only HotCRP is supported; for any other portal the command says so and the upload is done by hand.
|
|
13
|
+
|
|
14
|
+
## What the command does, and what stays on the site
|
|
15
|
+
|
|
16
|
+
`submission new` creates the submission with the title, the abstract, the authors and the PDF.
|
|
17
|
+
Everything else — conflicts, topics, the venue's own fields — is done on the site by the author: the
|
|
18
|
+
command prints that list, with the submission's page, after every run.
|
|
19
|
+
|
|
20
|
+
## Token
|
|
21
|
+
|
|
22
|
+
One variable per site: `HOTCRP_TOKEN_MSR2027` for `msr2027.hotcrp.com`; for a site not on
|
|
23
|
+
hotcrp.com, its whole host (with a port other than the default) and path. A plain `HOTCRP_TOKEN` is not read. When the command reports
|
|
24
|
+
a missing, misnamed or refused token, **relay its message to the author verbatim and stop**: it
|
|
25
|
+
already says where on the site to make one, which permissions to tick, and which variable to set.
|
|
26
|
+
🔴 Never ask for the token in chat, never print it, never write it to a file — the author sets it in
|
|
27
|
+
their own environment.
|
|
13
28
|
|
|
14
29
|
## What must be in place
|
|
15
30
|
|
|
16
31
|
- **The venue preset declares the portal**: `"portal": { "kind": "hotcrp", "url": "<the site>" }`
|
|
17
|
-
(`paperlint:aidc` and `paperlint:msr` have it).
|
|
18
|
-
- **
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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.
|
|
32
|
+
(`paperlint:aidc` and `paperlint:msr` have it).
|
|
33
|
+
- **For `new`: `"authors"` in the paper's `paperlint.json`** — `[{ "name", "email",
|
|
34
|
+
"affiliation" }]`, every author in the paper's order. The author writes this list; never fill it
|
|
35
|
+
in from the paper, a profile or memory.
|
|
36
|
+
- **For `show` and `update`: `"submission": { "id": <number> }`** — `new --save` writes it.
|
|
24
37
|
|
|
25
|
-
A missing key or token stops the command with exit 2 and a line naming
|
|
26
|
-
variable — fix exactly that and run again.
|
|
38
|
+
A missing key or token stops the command with exit 2 and a line naming what to set — fix exactly that.
|
|
27
39
|
|
|
28
40
|
## The procedure — always in this order
|
|
29
41
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
`
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
`papers/my-paper` stands for the paper's folder.
|
|
43
|
+
|
|
44
|
+
0. **Create it, once — only while the paper has no `submission` id.**
|
|
45
|
+
`npx paperlint submission new papers/my-paper` is a dry run: the portal checks it and keeps
|
|
46
|
+
nothing. It prints the title, the authors and the whole abstract (read from the source's
|
|
47
|
+
`\begin{abstract}`). Read them with the author; an abstract the command cannot read, or one
|
|
48
|
+
that reads wrong, is given as a plain-text file with `--abstract <file>`.
|
|
49
|
+
1. **Ask the author** for an explicit yes, then `npx paperlint submission new papers/my-paper --save`,
|
|
50
|
+
and do the printed "on the site" list with the author.
|
|
51
|
+
2. **Read it.** `npx paperlint submission show papers/my-paper` — status, the PDF the portal holds,
|
|
52
|
+
and `match`: whether it is the local build. Rebuild first if the local PDF is stale.
|
|
53
|
+
3. **Dry run the change.** `npx paperlint submission update papers/my-paper` (`--abstract <file>`,
|
|
54
|
+
`--submit` to mark it submitted). Exit 1 means the portal would refuse it — report why, stop.
|
|
55
|
+
4. **Ask the author** for an explicit yes, then the same command with `--save`.
|
|
56
|
+
5. **Prove it landed.** `show` again: `match` must read `yes — the portal holds this build`, and
|
|
57
|
+
the status must be what was intended. Anything else is not done — say so.
|
|
58
|
+
|
|
59
|
+
🔴 `--save` creates or changes a real submission that reviewers will see: never without the
|
|
60
|
+
author's yes in this conversation, never "to test", never because a dry run passed.
|
|
48
61
|
|
|
49
62
|
```bash
|
|
50
|
-
npx paperlint
|
|
51
|
-
|
|
63
|
+
npx paperlint build papers/my-paper
|
|
64
|
+
# only while the paper has no submission id:
|
|
65
|
+
npx paperlint submission new papers/my-paper
|
|
66
|
+
# … the author reads the dry run and says yes …
|
|
67
|
+
npx paperlint submission new papers/my-paper --save
|
|
68
|
+
npx paperlint submission show papers/my-paper
|
|
69
|
+
npx paperlint submission update papers/my-paper --submit
|
|
52
70
|
# … the author reads the dry run and says yes …
|
|
53
|
-
npx paperlint submission update papers
|
|
54
|
-
npx paperlint submission show papers
|
|
71
|
+
npx paperlint submission update papers/my-paper --submit --save
|
|
72
|
+
npx paperlint submission show papers/my-paper
|
|
55
73
|
```
|
|
56
74
|
|
|
57
75
|
## Reading the answers
|
|
58
76
|
|
|
59
|
-
- `match yes` is the only proof the reviewers get the local build;
|
|
60
|
-
|
|
61
|
-
- `
|
|
62
|
-
|
|
63
|
-
HotCRP's own; quote it.
|
|
64
|
-
- `SAVE WITHHELD by the portal` — HotCRP did not save; nothing changed.
|
|
77
|
+
- `match yes` is the only proof the reviewers get the local build; `submitted` alone says nothing
|
|
78
|
+
about WHICH PDF.
|
|
79
|
+
- `portal says: …` lines are the portal's own words; quote them.
|
|
80
|
+
- `SAVE WITHHELD` or `NOTHING CREATED` — the portal saved nothing.
|
|
65
81
|
|
|
66
82
|
## What this skill does not do
|
|
67
83
|
|
|
68
|
-
It does not decide whether the paper is ready (
|
|
69
|
-
|
|
84
|
+
It does not decide whether the paper is ready (`harden-paper`), anonymize it, or set the portal's
|
|
85
|
+
other fields — see `submit-paper`.
|