paperlint 11.1.2 → 12.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/dist/build.d.ts.map +1 -1
- package/dist/build.js +18 -8
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/latex-log.d.ts +61 -26
- package/dist/latex-log.d.ts.map +1 -1
- package/dist/latex-log.js +59 -6
- package/dist/latex-log.js.map +1 -1
- package/dist/latex-loop.d.ts +21 -6
- package/dist/latex-loop.d.ts.map +1 -1
- package/dist/latex-loop.js +32 -12
- package/dist/latex-loop.js.map +1 -1
- package/dist/submission-confirm-hook.d.ts +27 -19
- package/dist/submission-confirm-hook.d.ts.map +1 -1
- package/dist/submission-confirm-hook.js +302 -95
- package/dist/submission-confirm-hook.js.map +1 -1
- package/docs/configuration.md +30 -1
- package/docs/e2e.md +1 -0
- package/docs/layout.md +22 -22
- package/docs/submission.md +32 -8
- package/fixtures/build-e2e/lost-character/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/lost-character/expect.json +15 -0
- package/fixtures/build-e2e/lost-character/paper.tex +8 -0
- package/package.json +1 -1
- package/skills/submission-portal/SKILL.md +18 -12
- package/skills/submission-portal/SKILL.md.spec.ts +17 -11
- package/src/build.test.ts +46 -3
- package/src/build.ts +20 -8
- package/src/cli.test.ts +3 -3
- package/src/cli.ts +2 -2
- package/src/latex-log.harness.ts +145 -14
- package/src/latex-log.ts +105 -16
- package/src/latex-loop.harness.ts +70 -34
- package/src/latex-loop.ts +51 -30
- package/src/submission-confirm-hook.test.ts +493 -60
- package/src/submission-confirm-hook.ts +448 -116
package/docs/layout.md
CHANGED
|
@@ -136,28 +136,28 @@ A check runs at one of four moments:
|
|
|
136
136
|
The lint-only job skips what needs a build (pages, fonts, references).
|
|
137
137
|
- **On demand** — a skill, when you ask for what it does. A skill stops nothing; it writes.
|
|
138
138
|
|
|
139
|
-
| what | kind | when it runs | what it stops
|
|
140
|
-
| ------------------------------------------------------------------------------------------- | ------- | ----------------------------------------------------------- |
|
|
141
|
-
| `paper-folder-layout` | hook | before an agent creates a file (Edit, Write) | a new entry the layout does not allow, at a paper's root or the top of its `repro/`; never an existing file
|
|
142
|
-
| `paper-edit-guard` | hook | before an agent's shell command | a shell command that writes or moves a paper's source, which would skip the hooks that run after an edit
|
|
143
|
-
| `submission-confirm` | hook | before an agent's shell command |
|
|
144
|
-
| `paper-skills-nudge` | hook | after an agent edits a paper's source | nothing: it shows the agent the pre-submit checklist
|
|
145
|
-
| `paper-status-gates` | hook | after an agent edits a paper's source | nothing: it prints the paper's readiness and its unrun gates, for a person reading the transcript
|
|
146
|
-
| [`paper/allowed-files-only`](rules/paper/allowed-files-only.md) | rule | every `paperlint lint`, and CI | a stray file or folder at a paper's root (error)
|
|
147
|
-
| [`repro/allowed-files-only`](rules/repro/allowed-files-only.md) | rule | every `paperlint lint`, and CI | a loose entry at the top of `repro/`, a folder that is not a study's, a study folder without `README.md` (error)
|
|
148
|
-
| [`repro/study-registered`](rules/repro/study-registered.md) | rule | every `paperlint lint`, and CI | a study folder without its row in `repro/studies.json`, a row that does not match, a measurement or plan that does not hold, an index out of sync (error)
|
|
149
|
-
| [`repro/study-plan-name`](rules/repro/study-plan-name.md) | rule | every `paperlint lint`, and CI | nothing: it warns when a study's plan is still named `prereg.json`
|
|
150
|
-
| [`repro/outputs-before-decision`](rules/repro/outputs-before-decision.md) | rule | every `paperlint lint`, and CI (with the whole history) | an output of a study decided before its run that git shows was not made after the decision
|
|
151
|
-
| `paperlint studies gate` | command | before a decide-first study's run | the run, until the decision is committed
|
|
152
|
-
| [`study/declare-sample`](rules/study/declare-sample.md) | rule | every `paperlint lint`, and CI, once a paper turns it on | a study's `prereg.json` without its population, strata, screens, coverage or the author's check (experimental, off by default)
|
|
153
|
-
| [`paper/folder-venue-leftover`](rules/paper/folder-venue-leftover.md) | rule | every `paperlint lint`, and CI | nothing: it warns when the folder is named after another venue
|
|
154
|
-
| `review/frontmatter`, `sibling/frontmatter` | rule | every `paperlint lint`, and CI | a review whose findings break the schema (error); a sibling card that does not say how much was read (warning)
|
|
155
|
-
| `paper/stages`, `paper/source` | rule | every `paperlint lint`, and CI | a stage whose frozen PDF or source is missing or changed (error)
|
|
156
|
-
| the rules on the source (`tex/*`, `paper/*`, `bib/*`) | rule | every `paperlint lint`, and CI | what each one's [row](rules.md) says
|
|
157
|
-
| the rules on what `paperlint build` measured (`pdf/*`, `format/page-limit`, the references) | rule | every `paperlint lint` after a build; CI's full job | a PDF over the page limit, in the wrong font or format, or with a bad reference
|
|
158
|
-
| `study-accepted-papers` | skill | when you ask; stage 2 (modes A, B) and stage 5 (modes C, D) | nothing: it fills the venue's corpus, and writes the strong-accept scorecard in `PIPELINE-STATUS.md`
|
|
159
|
-
| `build-benchmark` | skill | when you ask; stage 3 | nothing: it registers the study with `paperlint studies new`, writes its design, fills its `plan.json`, and updates its row when it ends
|
|
160
|
-
| `build-label-page` | skill | when you ask; stage 3 | nothing: it builds the labelling page, and exports the answers into the study's `data/`
|
|
139
|
+
| what | kind | when it runs | what it stops |
|
|
140
|
+
| ------------------------------------------------------------------------------------------- | ------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
141
|
+
| `paper-folder-layout` | hook | before an agent creates a file (Edit, Write) | a new entry the layout does not allow, at a paper's root or the top of its `repro/`; never an existing file |
|
|
142
|
+
| `paper-edit-guard` | hook | before an agent's shell command | a shell command that writes or moves a paper's source, which would skip the hooks that run after an edit |
|
|
143
|
+
| `submission-confirm` | hook | before an agent's shell command | a submission send (`--save`, `--submit`), or a command naming a portal's host, that the person has not said yes to in a question naming its PDF's sha256 (or the host and the command) |
|
|
144
|
+
| `paper-skills-nudge` | hook | after an agent edits a paper's source | nothing: it shows the agent the pre-submit checklist |
|
|
145
|
+
| `paper-status-gates` | hook | after an agent edits a paper's source | nothing: it prints the paper's readiness and its unrun gates, for a person reading the transcript |
|
|
146
|
+
| [`paper/allowed-files-only`](rules/paper/allowed-files-only.md) | rule | every `paperlint lint`, and CI | a stray file or folder at a paper's root (error) |
|
|
147
|
+
| [`repro/allowed-files-only`](rules/repro/allowed-files-only.md) | rule | every `paperlint lint`, and CI | a loose entry at the top of `repro/`, a folder that is not a study's, a study folder without `README.md` (error) |
|
|
148
|
+
| [`repro/study-registered`](rules/repro/study-registered.md) | rule | every `paperlint lint`, and CI | a study folder without its row in `repro/studies.json`, a row that does not match, a measurement or plan that does not hold, an index out of sync (error) |
|
|
149
|
+
| [`repro/study-plan-name`](rules/repro/study-plan-name.md) | rule | every `paperlint lint`, and CI | nothing: it warns when a study's plan is still named `prereg.json` |
|
|
150
|
+
| [`repro/outputs-before-decision`](rules/repro/outputs-before-decision.md) | rule | every `paperlint lint`, and CI (with the whole history) | an output of a study decided before its run that git shows was not made after the decision |
|
|
151
|
+
| `paperlint studies gate` | command | before a decide-first study's run | the run, until the decision is committed |
|
|
152
|
+
| [`study/declare-sample`](rules/study/declare-sample.md) | rule | every `paperlint lint`, and CI, once a paper turns it on | a study's `prereg.json` without its population, strata, screens, coverage or the author's check (experimental, off by default) |
|
|
153
|
+
| [`paper/folder-venue-leftover`](rules/paper/folder-venue-leftover.md) | rule | every `paperlint lint`, and CI | nothing: it warns when the folder is named after another venue |
|
|
154
|
+
| `review/frontmatter`, `sibling/frontmatter` | rule | every `paperlint lint`, and CI | a review whose findings break the schema (error); a sibling card that does not say how much was read (warning) |
|
|
155
|
+
| `paper/stages`, `paper/source` | rule | every `paperlint lint`, and CI | a stage whose frozen PDF or source is missing or changed (error) |
|
|
156
|
+
| the rules on the source (`tex/*`, `paper/*`, `bib/*`) | rule | every `paperlint lint`, and CI | what each one's [row](rules.md) says |
|
|
157
|
+
| the rules on what `paperlint build` measured (`pdf/*`, `format/page-limit`, the references) | rule | every `paperlint lint` after a build; CI's full job | a PDF over the page limit, in the wrong font or format, or with a bad reference |
|
|
158
|
+
| `study-accepted-papers` | skill | when you ask; stage 2 (modes A, B) and stage 5 (modes C, D) | nothing: it fills the venue's corpus, and writes the strong-accept scorecard in `PIPELINE-STATUS.md` |
|
|
159
|
+
| `build-benchmark` | skill | when you ask; stage 3 | nothing: it registers the study with `paperlint studies new`, writes its design, fills its `plan.json`, and updates its row when it ends |
|
|
160
|
+
| `build-label-page` | skill | when you ask; stage 3 | nothing: it builds the labelling page, and exports the answers into the study's `data/` |
|
|
161
161
|
|
|
162
162
|
The hooks are wired by `paperlint init` ([`install.md`](install.md)). A rule's level and options:
|
|
163
163
|
[`rules.md`](rules.md).
|
package/docs/submission.md
CHANGED
|
@@ -111,14 +111,38 @@ The command refuses one thing by itself: `--save` when no build facts describe t
|
|
|
111
111
|
of other bytes) exits 2 and sends nothing. Rebuild (`paperlint build`) and run the dry run again.
|
|
112
112
|
Everything else, including over the limit, is the person's call.
|
|
113
113
|
|
|
114
|
-
**The `submission-confirm` hook** (installed by `paperlint init`)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
send.
|
|
114
|
+
**The `submission-confirm` hook** (installed by `paperlint init`) stops any `submission new|update`
|
|
115
|
+
with `--save` or `--submit` until the person has said yes to that exact PDF in that exact folder. The flow:
|
|
116
|
+
|
|
117
|
+
1. Run the dry run (the same command without `--save`) and read its venue check.
|
|
118
|
+
2. Ask the person with Claude Code's question tool (`AskUserQuestion`), with the options "Yes, send
|
|
119
|
+
it" and "No". The question names the paper folder (its full path, as the hook prints it), the PDF and the
|
|
120
|
+
first 12 characters of its sha256, the body pages, and quotes the dry run's venue check.
|
|
121
|
+
3. On a yes, run the send.
|
|
122
|
+
|
|
123
|
+
Without a yes, the hook stops the send and tells the agent what to ask, with the folder and the sha256 filled in.
|
|
124
|
+
It reads the answer from the session's transcript. The question must contain the folder's full path
|
|
125
|
+
and the PDF's sha256, so a yes to an older build, another PDF, or the same bytes in another folder
|
|
126
|
+
(the same manuscript for another venue) does not count. One yes lets one send through; the next send
|
|
127
|
+
needs a new question. That holds inside one command too: each send in a chain (`… --save && … --save`)
|
|
128
|
+
takes a yes of its own, so one yes naming two shas, or two sends of the same PDF, covers only one. A send whose PDF cannot be found or told for certain is stopped, and so is
|
|
129
|
+
any send when the transcript cannot be read.
|
|
130
|
+
|
|
131
|
+
A command that names a portal's host (a HotCRP-hosted site, or a host a venue preset declares),
|
|
132
|
+
whatever program it runs, may send around the venue check. It runs only after a yes to a question
|
|
133
|
+
that contains that host and the command's first 60 characters; the hook prints both when it stops
|
|
134
|
+
the command, the start compared as written (the host in any case). One yes lets one such command through, and a command that also sends a paper needs a separate yes for each. If the venue presets cannot be read, only
|
|
135
|
+
HotCRP-hosted sites and paperlint sends are recognised, and the hook says so when it stops one;
|
|
136
|
+
other commands run as usual. The hook does not see an upload from a script file that names the
|
|
137
|
+
host only inside the file.
|
|
138
|
+
|
|
139
|
+
The agent writes the question, so a deliberately misleading question still passes. The hook
|
|
140
|
+
catches an agent that sends without asking, or asks about the wrong PDF; it is not a security
|
|
141
|
+
boundary against an agent that deliberately disguises a send.
|
|
142
|
+
|
|
143
|
+
Why a question and not Claude Code's own permission prompt: in the Claude mobile app that prompt
|
|
144
|
+
shows only the command line, not which paper or PDF it sends. The question shows everything, on
|
|
145
|
+
every client.
|
|
122
146
|
|
|
123
147
|
## What it does not do
|
|
124
148
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "pdflatex exits 0 and writes a PDF without a character the font has no glyph for; the build fails on the log line (FAILING_WARNINGS in src/latex-log.ts).",
|
|
3
|
+
"build": {
|
|
4
|
+
"outcome": "failed"
|
|
5
|
+
},
|
|
6
|
+
"says": [
|
|
7
|
+
"✗ compile: pdflatex exited 0, but its final log carries a warning that fails the build",
|
|
8
|
+
"lost-character — the font has no glyph for this character, so pdflatex left it out of the PDF:",
|
|
9
|
+
"Missing character: There is no",
|
|
10
|
+
"in font cmr10!"
|
|
11
|
+
],
|
|
12
|
+
"saysNot": [
|
|
13
|
+
"✓"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
% A paper whose font has no glyph for one character. pdflatex prints "Missing character: There is no
|
|
2
|
+
% ... in font cmr10!" to the log (LaTeX sets \tracinglostchars=2), exits 0 and writes a PDF without
|
|
3
|
+
% it -- the way an abstract once printed 7 x 10^13 for 7 x 10^-13. `paperlint build` must fail, quote
|
|
4
|
+
% the log line, and leave no paper.pdf.
|
|
5
|
+
\documentclass{article}
|
|
6
|
+
\begin{document}
|
|
7
|
+
A rate of $7\times 10^{13}$ and a character cmr10 cannot draw: \char"FF.
|
|
8
|
+
\end{document}
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@ description: "Create, read or change a paper's submission on the venue's HotCRP
|
|
|
4
4
|
allowed-tools: [Read, Grep, Glob, Bash, Edit, AskUserQuestion]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<!-- vigiles:sha256:
|
|
7
|
+
<!-- vigiles:sha256:8b1352dd55af2d2b compiled from skills/submission-portal/SKILL.md.spec.ts -->
|
|
8
8
|
|
|
9
9
|
# submission-portal — create, read and update the submission on the portal, then prove it landed
|
|
10
10
|
|
|
@@ -62,25 +62,31 @@ A missing key or token stops the command with exit 2 and a line naming what to s
|
|
|
62
62
|
`\begin{abstract}`) and each `field` it sends. Read them with the author; an abstract the
|
|
63
63
|
command cannot read, or one that reads wrong, is given as a plain-text file with
|
|
64
64
|
`--abstract <file>`.
|
|
65
|
-
2. **
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
2. **Ask, then send.** Ask the author with `AskUserQuestion` — options "Yes, send it" and "No" —
|
|
66
|
+
naming the paper folder (its full path, as the hook prints it), the PDF and its sha256, the body
|
|
67
|
+
pages, and quoting the dry run's venue
|
|
68
|
+
check (its lint findings, or "venue rules at their default options: no errors"), plus what goes
|
|
69
|
+
(title, authors, abstract, each field). On a yes, run
|
|
70
|
+
`npx paperlint submission new papers/my-paper --save`. Then the printed "still to do" list
|
|
71
|
+
with the author.
|
|
69
72
|
3. **Read it.** `npx paperlint submission show papers/my-paper` — status, the PDF the portal holds,
|
|
70
73
|
and `match`: whether it is the local build. Rebuild first if the local PDF is stale.
|
|
71
74
|
4. **Dry run the change.** `npx paperlint submission update papers/my-paper` (`--abstract <file>`,
|
|
72
75
|
`--submit` to mark it submitted); it sends the paper's `"fields"` too. Exit 1 means the portal
|
|
73
76
|
would refuse it, or did not take a topic — report why, stop.
|
|
74
|
-
5. **
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
5. **Ask, then send.** Ask the author with `AskUserQuestion` as in step 2: the question names the
|
|
78
|
+
paper folder (full path), the PDF's sha256 and quotes the dry run's venue check, and says what changes. On a yes, run the same
|
|
79
|
+
command with `--save`. Where the `submission-confirm` hook is installed, a send without that
|
|
80
|
+
yes is stopped, and the hook says what to ask, with the folder and the sha256 filled in; ask exactly that. One
|
|
81
|
+
yes covers one send: to send again, or to send a second paper in the same command, ask again. A refusal by the command itself (exit 2) means
|
|
82
|
+
the last build is not of this PDF: rebuild, dry run again.
|
|
77
83
|
6. **Prove it landed.** `show` again: `match` must read `yes — the portal holds this build`, and
|
|
78
84
|
the status must be what was intended. Anything else is not done — say so.
|
|
79
85
|
|
|
80
86
|
🔴 `--save` creates or changes a real submission that reviewers will see: never without the
|
|
81
87
|
author's own yes, never "to test", never because a dry run passed. Before every `--save`, ask the
|
|
82
|
-
author with `AskUserQuestion`,
|
|
83
|
-
hook is installed,
|
|
88
|
+
author with `AskUserQuestion`, naming the paper folder, the PDF's sha256 and quoting the dry run's venue check; where
|
|
89
|
+
the `submission-confirm` hook is installed, it stops any send without that yes.
|
|
84
90
|
|
|
85
91
|
```bash
|
|
86
92
|
npx paperlint build papers/my-paper
|
|
@@ -88,11 +94,11 @@ npx paperlint submission topics papers/my-paper
|
|
|
88
94
|
# … AskUserQuestion: which topics; the chosen ones into "fields" …
|
|
89
95
|
# only while the paper has no submission id:
|
|
90
96
|
npx paperlint submission new papers/my-paper
|
|
91
|
-
# … the
|
|
97
|
+
# … AskUserQuestion naming the folder, the PDF's sha256 and the venue check; the author says yes …
|
|
92
98
|
npx paperlint submission new papers/my-paper --save
|
|
93
99
|
npx paperlint submission show papers/my-paper
|
|
94
100
|
npx paperlint submission update papers/my-paper --submit
|
|
95
|
-
# … the
|
|
101
|
+
# … AskUserQuestion naming the folder, the PDF's sha256 and the venue check; the author says yes …
|
|
96
102
|
npx paperlint submission update papers/my-paper --submit --save
|
|
97
103
|
npx paperlint submission show papers/my-paper
|
|
98
104
|
```
|
|
@@ -66,25 +66,31 @@ A missing key or token stops the command with exit 2 and a line naming what to s
|
|
|
66
66
|
\`\\begin{abstract}\`) and each \`field\` it sends. Read them with the author; an abstract the
|
|
67
67
|
command cannot read, or one that reads wrong, is given as a plain-text file with
|
|
68
68
|
\`--abstract <file>\`.
|
|
69
|
-
2. **
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
2. **Ask, then send.** Ask the author with \`AskUserQuestion\` — options "Yes, send it" and "No" —
|
|
70
|
+
naming the paper folder (its full path, as the hook prints it), the PDF and its sha256, the body
|
|
71
|
+
pages, and quoting the dry run's venue
|
|
72
|
+
check (its lint findings, or "venue rules at their default options: no errors"), plus what goes
|
|
73
|
+
(title, authors, abstract, each field). On a yes, run
|
|
74
|
+
\`npx paperlint submission new papers/my-paper --save\`. Then the printed "still to do" list
|
|
75
|
+
with the author.
|
|
73
76
|
3. **Read it.** \`npx paperlint submission show papers/my-paper\` — status, the PDF the portal holds,
|
|
74
77
|
and \`match\`: whether it is the local build. Rebuild first if the local PDF is stale.
|
|
75
78
|
4. **Dry run the change.** \`npx paperlint submission update papers/my-paper\` (\`--abstract <file>\`,
|
|
76
79
|
\`--submit\` to mark it submitted); it sends the paper's \`"fields"\` too. Exit 1 means the portal
|
|
77
80
|
would refuse it, or did not take a topic — report why, stop.
|
|
78
|
-
5. **
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
5. **Ask, then send.** Ask the author with \`AskUserQuestion\` as in step 2: the question names the
|
|
82
|
+
paper folder (full path), the PDF's sha256 and quotes the dry run's venue check, and says what changes. On a yes, run the same
|
|
83
|
+
command with \`--save\`. Where the \`submission-confirm\` hook is installed, a send without that
|
|
84
|
+
yes is stopped, and the hook says what to ask, with the folder and the sha256 filled in; ask exactly that. One
|
|
85
|
+
yes covers one send: to send again, or to send a second paper in the same command, ask again. A refusal by the command itself (exit 2) means
|
|
86
|
+
the last build is not of this PDF: rebuild, dry run again.
|
|
81
87
|
6. **Prove it landed.** \`show\` again: \`match\` must read \`yes — the portal holds this build\`, and
|
|
82
88
|
the status must be what was intended. Anything else is not done — say so.
|
|
83
89
|
|
|
84
90
|
🔴 \`--save\` creates or changes a real submission that reviewers will see: never without the
|
|
85
91
|
author's own yes, never "to test", never because a dry run passed. Before every \`--save\`, ask the
|
|
86
|
-
author with \`AskUserQuestion\`,
|
|
87
|
-
hook is installed,
|
|
92
|
+
author with \`AskUserQuestion\`, naming the paper folder, the PDF's sha256 and quoting the dry run's venue check; where
|
|
93
|
+
the \`submission-confirm\` hook is installed, it stops any send without that yes.
|
|
88
94
|
|
|
89
95
|
\`\`\`bash
|
|
90
96
|
npx paperlint build papers/my-paper
|
|
@@ -92,11 +98,11 @@ npx paperlint submission topics papers/my-paper
|
|
|
92
98
|
# … AskUserQuestion: which topics; the chosen ones into "fields" …
|
|
93
99
|
# only while the paper has no submission id:
|
|
94
100
|
npx paperlint submission new papers/my-paper
|
|
95
|
-
# … the
|
|
101
|
+
# … AskUserQuestion naming the folder, the PDF's sha256 and the venue check; the author says yes …
|
|
96
102
|
npx paperlint submission new papers/my-paper --save
|
|
97
103
|
npx paperlint submission show papers/my-paper
|
|
98
104
|
npx paperlint submission update papers/my-paper --submit
|
|
99
|
-
# … the
|
|
105
|
+
# … AskUserQuestion naming the folder, the PDF's sha256 and the venue check; the author says yes …
|
|
100
106
|
npx paperlint submission update papers/my-paper --submit --save
|
|
101
107
|
npx paperlint submission show papers/my-paper
|
|
102
108
|
\`\`\`
|
package/src/build.test.ts
CHANGED
|
@@ -265,12 +265,55 @@ test("compile: a document whose .aux never settles did not converge", async () =
|
|
|
265
265
|
assert.match(line, /^ {2}✗ compile: pdflatex did not converge\n/);
|
|
266
266
|
});
|
|
267
267
|
|
|
268
|
-
test("compile:
|
|
269
|
-
const
|
|
268
|
+
test("🔴 compile: pdflatex exits 0 but its log says a character was dropped — the build fails, quoting the line, and the PDF goes", async () => {
|
|
269
|
+
const dir = paper();
|
|
270
|
+
// The log as pdflatex writes it: raw UTF-8 bytes, which the build reads as latin1 and shows as UTF-8.
|
|
271
|
+
const { r, line } = await build(dir, {
|
|
272
|
+
log: [
|
|
273
|
+
"(./paper.aux)",
|
|
274
|
+
"Missing character: There is no ⁻ in font cmr10!",
|
|
275
|
+
"Overfull \\hbox (1.2pt too wide) in paragraph at lines 4--5",
|
|
276
|
+
"Missing character: There is no ⁻ in font cmr10!",
|
|
277
|
+
"",
|
|
278
|
+
].join("\n"),
|
|
279
|
+
});
|
|
280
|
+
assert.equal(r.status, "failed");
|
|
281
|
+
assert.equal(
|
|
282
|
+
line,
|
|
283
|
+
[
|
|
284
|
+
" ✗ compile: pdflatex exited 0, but its final log carries a warning that fails the build",
|
|
285
|
+
" lost-character — the font has no glyph for this character, so pdflatex left it out of the PDF:",
|
|
286
|
+
" Missing character: There is no ⁻ in font cmr10! (2 times)",
|
|
287
|
+
` full log: ${join(dir, "paper.log").replace(root, "<root>")}`,
|
|
288
|
+
" paper.pdf removed — a stale PDF must not pass for this build",
|
|
289
|
+
].join("\n"),
|
|
290
|
+
);
|
|
291
|
+
assert.equal(existsSync(join(dir, "paper.pdf")), false);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("compile: undefined references in the final log fail the build — no longer a note on a green one", async () => {
|
|
295
|
+
const { r, line } = await build(paper(), {
|
|
270
296
|
log: "LaTeX Warning: There were undefined references.\n",
|
|
271
297
|
});
|
|
298
|
+
assert.equal(r.status, "failed");
|
|
299
|
+
assert.match(
|
|
300
|
+
line,
|
|
301
|
+
/\n {6}undefined-references — .*:\n {8}LaTeX Warning: There were undefined references\.\n/,
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("compile: warnings off the failing list — overfull boxes, a font substitution, one undefined reference — leave the build green", async () => {
|
|
306
|
+
const { r } = await build(paper(), {
|
|
307
|
+
log: [
|
|
308
|
+
"Overfull \\hbox (1.2pt too wide) in paragraph at lines 4--5",
|
|
309
|
+
"LaTeX Font Warning: Font shape `OT1/cmr/bx/sc' undefined",
|
|
310
|
+
"(Font) using `OT1/cmr/bx/n' instead on input line 3.",
|
|
311
|
+
"LaTeX Warning: Reference `sec:x' on page 1 undefined on input line 4.",
|
|
312
|
+
"",
|
|
313
|
+
].join("\n"),
|
|
314
|
+
});
|
|
272
315
|
assert.equal(r.status, "built");
|
|
273
|
-
assert.
|
|
316
|
+
assert.doesNotMatch(r.notes?.[0] ?? "", /⚠️/);
|
|
274
317
|
});
|
|
275
318
|
|
|
276
319
|
test("references: without a reader the step says it was not wired; with one and no checker, the record says not checked", async () => {
|
package/src/build.ts
CHANGED
|
@@ -83,6 +83,7 @@ import {
|
|
|
83
83
|
auxBib,
|
|
84
84
|
bibtexExcerpt,
|
|
85
85
|
errorExcerpt,
|
|
86
|
+
failingWarnings,
|
|
86
87
|
logMarkers,
|
|
87
88
|
unwrapLog,
|
|
88
89
|
} from "./latex-log.ts";
|
|
@@ -90,6 +91,7 @@ import {
|
|
|
90
91
|
nextStep,
|
|
91
92
|
summarize,
|
|
92
93
|
type BibInput,
|
|
94
|
+
type FailCause,
|
|
93
95
|
type Hashes,
|
|
94
96
|
type Observation,
|
|
95
97
|
type Step,
|
|
@@ -401,6 +403,10 @@ function latexPass(
|
|
|
401
403
|
markers: logMarkers(log),
|
|
402
404
|
bib: bibInput(ctx.paperDir),
|
|
403
405
|
fls: readOr(join(ctx.paperDir, `${JOB}.fls`), "utf8"),
|
|
406
|
+
failing: failingWarnings(log).map((h) => ({
|
|
407
|
+
...h,
|
|
408
|
+
quotes: h.quotes.map((q) => ({ ...q, text: fromLatin1(q.text) })),
|
|
409
|
+
})),
|
|
404
410
|
errorLines:
|
|
405
411
|
exitCode === 0
|
|
406
412
|
? []
|
|
@@ -494,6 +500,18 @@ export function compile(ctx: BuildContext): {
|
|
|
494
500
|
|
|
495
501
|
const NO_PDF = `pdflatex exited 0 but wrote no ${JOB}.pdf — does the document have any pages?`;
|
|
496
502
|
|
|
503
|
+
/** The head of a failed compile: which program, and how it ended. */
|
|
504
|
+
function whereFailed(bin: string, cause: FailCause): string {
|
|
505
|
+
switch (cause.kind) {
|
|
506
|
+
case "exit":
|
|
507
|
+
return `${bin} exited with ${String(cause.code)}`;
|
|
508
|
+
case "no-convergence":
|
|
509
|
+
return `${bin} did not converge`;
|
|
510
|
+
case "log-warnings":
|
|
511
|
+
return `${bin} exited 0, but its final log carries a warning that fails the build`;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
497
515
|
const plural = (n: number, one: string, many: string): string =>
|
|
498
516
|
`${String(n)} ${n === 1 ? one : many}`;
|
|
499
517
|
|
|
@@ -517,10 +535,7 @@ export const compileStep: BuildStep = {
|
|
|
517
535
|
const { end, latex, bibtex, run } = compile(ctx);
|
|
518
536
|
if (end.kind === "fail") {
|
|
519
537
|
const bin = end.step === "latex" ? "pdflatex" : "bibtex";
|
|
520
|
-
const where =
|
|
521
|
-
end.cause.kind === "exit"
|
|
522
|
-
? `${bin} exited with ${String(end.cause.code)}`
|
|
523
|
-
: `${bin} did not converge`;
|
|
538
|
+
const where = whereFailed(bin, end.cause);
|
|
524
539
|
const log = join(
|
|
525
540
|
ctx.paperDir,
|
|
526
541
|
`${JOB}.${end.step === "latex" ? "log" : "blg"}`,
|
|
@@ -545,12 +560,9 @@ export const compileStep: BuildStep = {
|
|
|
545
560
|
ok: false,
|
|
546
561
|
lines: [NO_PDF, `full log: ${join(ctx.paperDir, `${JOB}.log`)}`],
|
|
547
562
|
};
|
|
548
|
-
const warn = end.warnings.length
|
|
549
|
-
? ` — ⚠️ the final log still reports ${end.warnings.join(", ")}`
|
|
550
|
-
: "";
|
|
551
563
|
return {
|
|
552
564
|
ok: true,
|
|
553
|
-
note: `${plural(latex, "pdflatex pass", "pdflatex passes")}, ${plural(bibtex, "bibtex run", "bibtex runs")}
|
|
565
|
+
note: `${plural(latex, "pdflatex pass", "pdflatex passes")}, ${plural(bibtex, "bibtex run", "bibtex runs")}; ${recordingNote(ctx.recordSources(ctx.paperDir, run))}`,
|
|
554
566
|
};
|
|
555
567
|
},
|
|
556
568
|
};
|
package/src/cli.test.ts
CHANGED
|
@@ -242,7 +242,7 @@ test("runHook paper-folder-layout: in-process, on the real disk — a refusal on
|
|
|
242
242
|
);
|
|
243
243
|
});
|
|
244
244
|
|
|
245
|
-
test("runHook submission-confirm: in-process —
|
|
245
|
+
test("runHook submission-confirm: in-process — a refusal on stdout for a send with no yes, nothing for anything else, and a refusal when its input cannot be read", () => {
|
|
246
246
|
const hook = (stdin: () => string) => {
|
|
247
247
|
const out: string[] = [];
|
|
248
248
|
const code = runHook("submission-confirm", {
|
|
@@ -260,7 +260,7 @@ test("runHook submission-confirm: in-process — an ask on stdout for a send, no
|
|
|
260
260
|
assert.equal(send.code, 0);
|
|
261
261
|
assert.match(
|
|
262
262
|
send.out.join(""),
|
|
263
|
-
/^\{"hookSpecificOutput":\{"hookEventName":"PreToolUse","permissionDecision":"
|
|
263
|
+
/^\{"hookSpecificOutput":\{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Not run: this sends the paper to the venue's portal/,
|
|
264
264
|
);
|
|
265
265
|
assert.deepEqual(hook(command("npx paperlint submission show nope")), {
|
|
266
266
|
code: 0,
|
|
@@ -272,7 +272,7 @@ test("runHook submission-confirm: in-process — an ask on stdout for a send, no
|
|
|
272
272
|
assert.equal(unreadable.code, 0);
|
|
273
273
|
assert.match(
|
|
274
274
|
unreadable.out.join(""),
|
|
275
|
-
/"permissionDecision":"
|
|
275
|
+
/"permissionDecision":"deny","permissionDecisionReason":"Not run: paperlint could not read this command/,
|
|
276
276
|
);
|
|
277
277
|
});
|
|
278
278
|
|
package/src/cli.ts
CHANGED
|
@@ -1247,7 +1247,7 @@ const IN_PROCESS_HOOKS: Readonly<Record<string, (io: HookIo) => number>> = {
|
|
|
1247
1247
|
}
|
|
1248
1248
|
return 0;
|
|
1249
1249
|
},
|
|
1250
|
-
// Never advisory-silent: an input it cannot read is
|
|
1250
|
+
// Never advisory-silent: an input it cannot read is refused, not let through.
|
|
1251
1251
|
"submission-confirm": ({ stdin, out }) => {
|
|
1252
1252
|
const said = ((): ReturnType<typeof submissionConfirm> => {
|
|
1253
1253
|
try {
|
|
@@ -1717,7 +1717,7 @@ function projectPresets(cwd: string): readonly Preset[] {
|
|
|
1717
1717
|
|
|
1718
1718
|
/**
|
|
1719
1719
|
* The host of every venue portal paperlint knows of from `cwd` — the shipped presets', and those the
|
|
1720
|
-
* project's papers extend: what `submission-confirm`
|
|
1720
|
+
* project's papers extend: what `submission-confirm` refuses when a request goes straight there.
|
|
1721
1721
|
*/
|
|
1722
1722
|
export function portalHosts(cwd: string): readonly string[] {
|
|
1723
1723
|
return [...resolveShipped(PRESET_DEPS), ...projectPresets(cwd)].flatMap(
|