paperlint 4.1.0 → 4.2.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/CLAUDE.md +24 -0
- package/CONTRIBUTING.md +7 -0
- package/dist/adapters/latex/document-class.d.ts +6 -5
- package/dist/adapters/latex/document-class.d.ts.map +1 -1
- package/dist/adapters/latex/document-class.js +18 -10
- package/dist/adapters/latex/document-class.js.map +1 -1
- package/dist/adapters/latex/includes.d.ts +12 -0
- package/dist/adapters/latex/includes.d.ts.map +1 -0
- package/dist/adapters/latex/includes.js +42 -0
- package/dist/adapters/latex/includes.js.map +1 -0
- package/dist/adapters/latex/index.d.ts +9 -5
- package/dist/adapters/latex/index.d.ts.map +1 -1
- package/dist/adapters/latex/index.js +8 -1
- package/dist/adapters/latex/index.js.map +1 -1
- package/dist/adapters/latex/nodes.d.ts +5 -1
- package/dist/adapters/latex/nodes.d.ts.map +1 -1
- package/dist/adapters/latex/nodes.js +6 -2
- package/dist/adapters/latex/nodes.js.map +1 -1
- package/dist/adapters/latex/parse.d.ts.map +1 -1
- package/dist/adapters/latex/parse.js +1 -0
- package/dist/adapters/latex/parse.js.map +1 -1
- package/dist/adapters/latex/prose.d.ts +8 -0
- package/dist/adapters/latex/prose.d.ts.map +1 -0
- package/dist/adapters/latex/prose.js +353 -0
- package/dist/adapters/latex/prose.js.map +1 -0
- package/dist/claim-provenance.d.ts +34 -0
- package/dist/claim-provenance.d.ts.map +1 -0
- package/dist/claim-provenance.js +146 -0
- package/dist/claim-provenance.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +20 -0
- package/dist/cli.js.map +1 -1
- package/dist/domain/paper-source.d.ts +58 -0
- package/dist/domain/paper-source.d.ts.map +1 -0
- package/dist/domain/paper-source.js +109 -0
- package/dist/domain/paper-source.js.map +1 -0
- package/dist/domain/sentences.d.ts +26 -0
- package/dist/domain/sentences.d.ts.map +1 -0
- package/dist/domain/sentences.js +48 -0
- package/dist/domain/sentences.js.map +1 -0
- package/dist/domain/tex-document.d.ts +39 -4
- package/dist/domain/tex-document.d.ts.map +1 -1
- package/dist/domain/tex-document.js.map +1 -1
- package/dist/eslint-rules/paper-typography.d.ts +7 -0
- package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
- package/dist/eslint-rules/paper-typography.js +53 -1
- package/dist/eslint-rules/paper-typography.js.map +1 -1
- package/dist/ports/latex.d.ts +12 -1
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/register.d.ts +40 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +134 -0
- package/dist/register.js.map +1 -0
- package/dist/tex-paper.d.ts +19 -0
- package/dist/tex-paper.d.ts.map +1 -0
- package/dist/tex-paper.js +105 -0
- package/dist/tex-paper.js.map +1 -0
- package/dist/tex-venue-rules.d.ts +7 -2
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +31 -17
- package/dist/tex-venue-rules.js.map +1 -1
- package/docs/rules/tex/claim-provenance.md +83 -0
- package/docs/rules/tex/missing-input.md +58 -0
- package/docs/rules/tex/register.md +77 -0
- package/docs/rules/tex/required-section.md +3 -1
- package/docs/rules/tex/template.md +12 -0
- package/docs/rules/tex/venue-leftover.md +4 -2
- package/docs/rules.md +3 -0
- package/eslint-rules/paper-typography.test.mjs +48 -1
- package/eslint-rules/paper-typography.ts +72 -1
- package/eslint.config.mjs +1 -0
- package/fixtures/real-markdown-paper/baseline.ts +8 -7
- package/package.json +2 -1
- package/skills/cold-read-diff/SKILL.md +31 -10
- package/skills/cold-read-diff/SKILL.md.spec.ts +30 -9
- package/skills/draft-paper/SKILL.md +2 -2
- package/skills/draft-paper/SKILL.md.spec.ts +1 -1
- package/skills/grade-paper-writing/SKILL.md +109 -44
- package/skills/grade-paper-writing/SKILL.md.spec.ts +108 -43
- package/skills/harden-paper/SKILL.md +35 -36
- package/skills/harden-paper/SKILL.md.spec.ts +34 -35
- package/skills/paper-adversarial-review/SKILL.md +2 -2
- package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
- package/skills/paper-pipeline/SKILL.md +3 -3
- package/skills/paper-pipeline/SKILL.md.spec.ts +2 -2
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +6 -6
- package/skills/paper-pipeline/references/writing-craft.md +53 -18
- package/skills/pc-panel-review/SKILL.md +36 -11
- package/skills/pc-panel-review/SKILL.md.spec.ts +35 -10
- package/src/adapters/latex/document-class.ts +20 -12
- package/src/adapters/latex/includes.ts +66 -0
- package/src/adapters/latex/index.test.ts +175 -1
- package/src/adapters/latex/index.ts +14 -4
- package/src/adapters/latex/nodes.ts +11 -2
- package/src/adapters/latex/parse.ts +1 -0
- package/src/adapters/latex/prose.ts +484 -0
- package/src/claim-provenance.test.ts +352 -0
- package/src/claim-provenance.ts +217 -0
- package/src/cli.ts +20 -0
- package/src/domain/paper-source.test.ts +156 -0
- package/src/domain/paper-source.ts +233 -0
- package/src/domain/sentences.ts +67 -0
- package/src/domain/tex-document.ts +36 -3
- package/src/ports/latex.ts +13 -0
- package/src/register.test.ts +165 -0
- package/src/register.ts +177 -0
- package/src/rule-cases.test.ts +54 -4
- package/src/tex-paper.test.ts +131 -0
- package/src/tex-paper.ts +152 -0
- package/src/tex-venue-rules.test.ts +87 -5
- package/src/tex-venue-rules.ts +62 -30
- package/skills/harden-paper/fixtures/uncited-assertions-sample.md +0 -43
- package/skills/harden-paper/fixtures/uncited-assertions-sample.tex +0 -77
package/dist/tex-venue-rules.js
CHANGED
|
@@ -24,6 +24,7 @@ import { basename, dirname, join } from "node:path";
|
|
|
24
24
|
import { CONFIG_FILE } from "#lib/paper-config";
|
|
25
25
|
import { collapse, documentClassLine, missingOptions, runText, spanIn, } from "./domain/tex-document.js";
|
|
26
26
|
import { paperPreset, resolvePreset, SHIPPED_PREFIX, shippedPresets, } from "./presets.js";
|
|
27
|
+
import { readPaper, reportInPaper } from "./tex-paper.js";
|
|
27
28
|
// ── the judges: what the source says + a resolved preset → findings. Pure. ─────────
|
|
28
29
|
/** A place as a finding points at it: its span, or null — the top of the file — when unplaced. */
|
|
29
30
|
const spanOf = (p) => (p.kind === "at" ? p.span : null);
|
|
@@ -38,6 +39,22 @@ function judgeClass(line, want, data) {
|
|
|
38
39
|
at,
|
|
39
40
|
}));
|
|
40
41
|
}
|
|
42
|
+
/** A candidate as the message names it: its line, or an empty `\documentclass{}`. */
|
|
43
|
+
const candidateLine = (c) => c.kind === "class" ? documentClassLine(c) : "\\documentclass{}";
|
|
44
|
+
/**
|
|
45
|
+
* Several class lines behind a TeX switch: satisfied when any candidate is the template's class
|
|
46
|
+
* with its options; otherwise one finding, at the first candidate, naming every one.
|
|
47
|
+
*/
|
|
48
|
+
function judgeCandidates(candidates, want, data) {
|
|
49
|
+
const matches = candidates.some((c) => c.kind === "class" && judgeClass(c, want, data).length === 0);
|
|
50
|
+
if (matches)
|
|
51
|
+
return [];
|
|
52
|
+
const named = candidates.map((c) => `\`${candidateLine(c)}\``).join(", ");
|
|
53
|
+
const found = { ...data, count: candidates.length, candidates: named };
|
|
54
|
+
return [
|
|
55
|
+
{ messageId: "noCandidate", data: found, at: spanOf(candidates[0].place) },
|
|
56
|
+
];
|
|
57
|
+
}
|
|
41
58
|
/** The paper's class line against a template that was read: present, naming a class, that class. */
|
|
42
59
|
function judgeClassLine(line, want, data) {
|
|
43
60
|
switch (line.kind) {
|
|
@@ -47,6 +64,8 @@ function judgeClassLine(line, want, data) {
|
|
|
47
64
|
return [{ messageId: "emptyClass", data, at: spanOf(line.place) }];
|
|
48
65
|
case "class":
|
|
49
66
|
return judgeClass(line, want, data);
|
|
67
|
+
case "ambiguous":
|
|
68
|
+
return judgeCandidates(line.candidates, want, data);
|
|
50
69
|
}
|
|
51
70
|
}
|
|
52
71
|
/**
|
|
@@ -148,6 +167,7 @@ const RULES = {
|
|
|
148
167
|
badTemplate: '{{file}}: the preset\'s "template" {{text}} is neither a \\documentclass line nor a class name, so the class cannot be checked',
|
|
149
168
|
wrongClass: "the class is `{{got}}`, and {{venue}} requires `{{template}}` — the page size, fonts and layout the venue checks come from the class",
|
|
150
169
|
missingOption: "the class option `{{option}}` is missing: {{venue}} requires `{{template}}`",
|
|
170
|
+
noCandidate: "none of the {{count}} \\documentclass lines is `{{template}}`, which {{venue}} requires: {{candidates}}. The source picks one behind a TeX switch, which is not evaluated; make one of them the venue's",
|
|
151
171
|
},
|
|
152
172
|
},
|
|
153
173
|
},
|
|
@@ -201,8 +221,9 @@ export function otherVenues(preset, fromFile, deps) {
|
|
|
201
221
|
});
|
|
202
222
|
}
|
|
203
223
|
/**
|
|
204
|
-
* What a judge reads for the paper whose `paper.tex` is `filename
|
|
205
|
-
* resolved preset (`pdf/measured` and
|
|
224
|
+
* What a judge reads for the paper whose `paper.tex` is `filename` — the paper with its includes
|
|
225
|
+
* spliced (`readPaper`) — or null when the paper has no resolved preset (`pdf/measured` and
|
|
226
|
+
* `pdf/profile` already say so).
|
|
206
227
|
*/
|
|
207
228
|
function readingOf(filename, src, deps) {
|
|
208
229
|
const dir = dirname(filename);
|
|
@@ -211,18 +232,11 @@ function readingOf(filename, src, deps) {
|
|
|
211
232
|
return null;
|
|
212
233
|
const preset = p.preset;
|
|
213
234
|
const others = () => otherVenues(preset, join(dir, CONFIG_FILE), deps);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
findings.forEach((f) => {
|
|
220
|
-
context.report({
|
|
221
|
-
loc: { start: at(f.at?.start ?? 0), end: at(f.at?.end ?? 0) },
|
|
222
|
-
messageId: f.messageId,
|
|
223
|
-
data: f.data,
|
|
224
|
-
});
|
|
225
|
-
});
|
|
235
|
+
const paper = readPaper(filename, src, deps);
|
|
236
|
+
return {
|
|
237
|
+
reading: { src: paper.text, latex: deps.latex, preset, others },
|
|
238
|
+
paper,
|
|
239
|
+
};
|
|
226
240
|
}
|
|
227
241
|
function rule(name, deps) {
|
|
228
242
|
const { judge, meta } = RULES[name];
|
|
@@ -235,9 +249,9 @@ function rule(name, deps) {
|
|
|
235
249
|
return {
|
|
236
250
|
root() {
|
|
237
251
|
const sc = context.sourceCode;
|
|
238
|
-
const
|
|
239
|
-
if (
|
|
240
|
-
|
|
252
|
+
const r = readingOf(context.filename, sc.raw ?? sc.text, deps);
|
|
253
|
+
if (r !== null)
|
|
254
|
+
reportInPaper(context, meta.messages, r.paper, judge(r.reading));
|
|
241
255
|
},
|
|
242
256
|
};
|
|
243
257
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tex-venue-rules.js","sourceRoot":"","sources":["../src/tex-venue-rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EACL,QAAQ,EACR,iBAAiB,EACjB,cAAc,EACd,OAAO,EACP,MAAM,
|
|
1
|
+
{"version":3,"file":"tex-venue-rules.js","sourceRoot":"","sources":["../src/tex-venue-rules.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# tex/claim-provenance
|
|
2
|
+
|
|
3
|
+
**Level:** warn · **Reads:** `paper.tex`
|
|
4
|
+
|
|
5
|
+
## What it catches
|
|
6
|
+
|
|
7
|
+
A sentence of the body that states a number and does not say whose the number is. The sentence is
|
|
8
|
+
reported once, however many numbers it holds. Any one of these owners silences it:
|
|
9
|
+
|
|
10
|
+
| owner | in the sentence |
|
|
11
|
+
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
12
|
+
| a work outside the paper | a `\cite` (any of natbib's or biblatex's forms), a `\url`, an `\href`, or a footnote that cites or links |
|
|
13
|
+
| the authors | _we_, _our_, _ours_, _us_ (the capitalised country _US_ is not the authors) |
|
|
14
|
+
| a place in the paper | `\ref`, `\cref`, `\autoref` and their kin, or the words _Section 4_, _Table 2_, _Fig. 3_, _Appendix B_, _§4_ |
|
|
15
|
+
| a claimant named as one | _advertises_, _claims_: _"the skill advertises a 65% reduction"_, _"(claim 63%)"_ |
|
|
16
|
+
| the sample the number counts | _n = 134_ (in prose or in math), or a count followed by what it counts: _48 runs_, _48 independent runs_, _of 1,836 repositories_ |
|
|
17
|
+
|
|
18
|
+
It reads the body of the whole paper — `paper.tex` with the files it `\input`s, `\include`s or
|
|
19
|
+
`\subfile`s spliced in — from the start of the document, the abstract included, to `\appendix` or the
|
|
20
|
+
bibliography. A sentence in an included file is reported at that include, with the file and line at
|
|
21
|
+
the front of the message. Footnotes are read as sentences of their own. A citation or a footnote set right after
|
|
22
|
+
the full stop belongs to the sentence the full stop ends.
|
|
23
|
+
|
|
24
|
+
A number is digits a reader sees — `42\%`, `0.37`, `1,836`, `3x`, `3×`. Not a number: a year on its
|
|
25
|
+
own (1900–2099); a number right after a capitalised word inside the sentence (_Claude 3_, _Python
|
|
26
|
+
3.12_, _Node 22_), which is a name; the level of an interval (the 95 of _95% CI_); digits glued to a letter, a hyphen or a point (`GPT-4`, `COVID-19`, `v1.2`, `10k`,
|
|
27
|
+
`0x1F`); anything inside math, a table, a figure, a caption, a heading (a run-in one too: `\textbf{Threats.}` opening a paragraph), code, a comment, a macro
|
|
28
|
+
definition, or the title block.
|
|
29
|
+
|
|
30
|
+
## Why
|
|
31
|
+
|
|
32
|
+
A reviewer who cannot tell a property of the world from a cited finding from the authors' own
|
|
33
|
+
measurement says so, and scores the paper's soundness on it. Academic prose carries the owner by
|
|
34
|
+
default — _we found_, _[12] report_, _(Table 2)_ — and a sentence rewritten for punch loses it first:
|
|
35
|
+
_"Under test the rules held in 97% of cases"_ has no subject, and a reader takes it
|
|
36
|
+
for a result from the literature. This rule is the floor under the rules that shorten sentences.
|
|
37
|
+
|
|
38
|
+
## Examples
|
|
39
|
+
|
|
40
|
+
```latex
|
|
41
|
+
Agents ignore 42\% of the rules they are given. % warns
|
|
42
|
+
Under test the rules held in 97\% of cases. % warns: no one ran the test
|
|
43
|
+
We find that agents ignore 42\% of the rules. % silent: the authors
|
|
44
|
+
Agents ignore 42\% of the rules~\cite{smith2025}. % silent: a citation
|
|
45
|
+
Agents ignore 42\% of the rules (Table~\ref{t:rates}). % silent: a place in the paper
|
|
46
|
+
Of 1,836 repositories, 12\% ship hooks. % silent: the sample
|
|
47
|
+
In 2024 the first agents shipped with GPT-4. % silent: a year and a name
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The first line reports:
|
|
51
|
+
|
|
52
|
+
> «42%» — whose number is this? A reader cannot tell a cited finding from your own measurement from
|
|
53
|
+
> a general fact. Say it in the sentence: cite the work (\cite), claim it (we measured, our census),
|
|
54
|
+
> point to where it is shown (\ref, Section N, Table N), or name the sample it counts (n = 134, 48
|
|
55
|
+
> runs)
|
|
56
|
+
|
|
57
|
+
## Options / preset fields
|
|
58
|
+
|
|
59
|
+
None. It is on, as a warning, for every `paper.tex`; turn it off or up in the `rules` setting like
|
|
60
|
+
any other rule.
|
|
61
|
+
|
|
62
|
+
## What it does not check
|
|
63
|
+
|
|
64
|
+
- Whether the owner is the right one: a citation that does not report the number, or a _we_ in a
|
|
65
|
+
sentence about someone else's work, silences the sentence all the same.
|
|
66
|
+
- What a custom macro prints: `\rate{}` defined as `42\%` is not a number here.
|
|
67
|
+
- Which numbers are names, beyond the two shapes it knows: a number after a lowercase word (_version
|
|
68
|
+
3 of the model_) is read as a quantity, and so is the number of a name that opens the sentence
|
|
69
|
+
(_Claude 3 fails …_).
|
|
70
|
+
- Whether a count followed by a plural noun is really the sample (_3 times faster_ passes as one).
|
|
71
|
+
- Markdown papers (`paper.md`): it reads the LaTeX parse tree only.
|
|
72
|
+
|
|
73
|
+
## How to fix
|
|
74
|
+
|
|
75
|
+
Say whose the number is, in the sentence — usually by putting back the subject a punchy rewrite took
|
|
76
|
+
out: _"We ran the rules in 48 sessions and saw no violation"_, not _"No violation in 48 runs."_ A
|
|
77
|
+
number that is common knowledge, or quoted on purpose, keeps its sentence with ESLint's directive and
|
|
78
|
+
the reason:
|
|
79
|
+
|
|
80
|
+
```latex
|
|
81
|
+
% eslint-disable-next-line tex/claim-provenance -- the venue's own page limit, quoted
|
|
82
|
+
The limit is 8.
|
|
83
|
+
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# tex/missing-input
|
|
2
|
+
|
|
3
|
+
**Level:** warn · **Reads:** `paper.tex`, and every file it includes
|
|
4
|
+
|
|
5
|
+
## What it catches
|
|
6
|
+
|
|
7
|
+
An `\input`, `\include` or `\subfile` that names a file which is not there. The path is resolved the
|
|
8
|
+
way TeX resolves it, from the paper's directory: `name.tex` first, then `name` as written. An
|
|
9
|
+
include inside an included file is resolved from the paper's directory too.
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
The rules that read the parse tree — `tex/template`, `tex/required-section`, `tex/venue-leftover`,
|
|
14
|
+
`tex/claim-provenance` — read the whole paper: `paper.tex` with every included file spliced in where
|
|
15
|
+
it is included, as TeX reads it. A file that is not there cannot be read, and without this rule the
|
|
16
|
+
others would report a clean body for text they never saw. TeX stops on it too.
|
|
17
|
+
|
|
18
|
+
## Examples
|
|
19
|
+
|
|
20
|
+
```latex
|
|
21
|
+
\input{sections/results} % warns when neither sections/results.tex nor sections/results exists
|
|
22
|
+
\input{sections/intro} % silent: sections/intro.tex is there
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The first line reports:
|
|
26
|
+
|
|
27
|
+
> `sections/results` is not there (tried `sections/results.tex` and `sections/results`, from the
|
|
28
|
+
> paper's directory): TeX stops on it, and no rule read what it should hold
|
|
29
|
+
|
|
30
|
+
A missing file included by another included file is reported at the include in `paper.tex` that
|
|
31
|
+
brought that file in, and the message names the file that holds it.
|
|
32
|
+
|
|
33
|
+
## Options / preset fields
|
|
34
|
+
|
|
35
|
+
None.
|
|
36
|
+
|
|
37
|
+
## What it does not check
|
|
38
|
+
|
|
39
|
+
- A path built from a macro (`\input{\dir/intro}`): macros are not expanded, so the path is looked
|
|
40
|
+
up as written.
|
|
41
|
+
- `\import{dir}{file}` and other packages' include macros.
|
|
42
|
+
|
|
43
|
+
## How to fix
|
|
44
|
+
|
|
45
|
+
Correct the path, or add the file. A file that a build step writes (a table of generated numbers)
|
|
46
|
+
is absent before that step runs — keep the include with a disable directive and the reason:
|
|
47
|
+
|
|
48
|
+
```latex
|
|
49
|
+
% eslint-disable-next-line tex/missing-input -- written by repro/numbers.sh before the build
|
|
50
|
+
\input{generated/numbers}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Where findings in included files are reported
|
|
54
|
+
|
|
55
|
+
ESLint reports a finding in the file it lints, and paperlint lints `paper.tex`. A finding of any of
|
|
56
|
+
the rules above whose text came from an included file is reported at the include in `paper.tex`,
|
|
57
|
+
with the included file, line and column at the front of its message
|
|
58
|
+
(`sections/results.tex:12:3: …`). A disable directive for it goes above that include.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# tex/register
|
|
2
|
+
|
|
3
|
+
**Level:** warn · **Reads:** `paper.tex` and the files it includes
|
|
4
|
+
|
|
5
|
+
## What it catches
|
|
6
|
+
|
|
7
|
+
A body in which more than 0.8 sentences per 1000 words open with _And_, _So_, _But_, _Nor_, _Or_ or
|
|
8
|
+
_Yet_. It is reported once, at the body's first sentence, with the count, the rate and the first
|
|
9
|
+
three such sentences.
|
|
10
|
+
|
|
11
|
+
It reads the body of the whole paper — `paper.tex` with the files it `\input`s, `\include`s or
|
|
12
|
+
`\subfile`s spliced in — from the start of the document, the abstract included, to `\appendix` or the
|
|
13
|
+
bibliography: paragraphs, list items and footnotes. Headings are not sentences, and neither is a
|
|
14
|
+
run-in heading — a bold or italic phrase ending in `.` or `:` that opens a paragraph or an item
|
|
15
|
+
(`\textbf{Threats.} …`). Captions, tables, figures, code and math environments are left out. A
|
|
16
|
+
citation, a cross-reference or inline math counts as one word, as it reads on the page. Sentences
|
|
17
|
+
are cut exactly as [`tex/claim-provenance`](claim-provenance.md) cuts them.
|
|
18
|
+
|
|
19
|
+
A conjunction counts only as the capitalised first word of a sentence; the _and_ that opens a list
|
|
20
|
+
item continuing the sentence before the list does not.
|
|
21
|
+
|
|
22
|
+
A body under 2 000 words is not judged: there one sentence decides the rate.
|
|
23
|
+
|
|
24
|
+
## Why
|
|
25
|
+
|
|
26
|
+
Reviewers called our prose "too informal" and "a blogpost". Measured against eleven accepted
|
|
27
|
+
papers, sentences opening with a coordinating conjunction set our four drafts apart:
|
|
28
|
+
|
|
29
|
+
| measure | 11 accepted papers | our 4 drafts |
|
|
30
|
+
| ------------------------------------------------------------ | ---------------------------: | -----------: |
|
|
31
|
+
| sentences opening with And/So/But/Nor/Or/Yet, per 1000 words | 0–0.58 (the ACSAC papers: 0) | 0.29–2.98 |
|
|
32
|
+
|
|
33
|
+
The limit sits above the accepted range with room to spare: the rule flags a body that has drifted
|
|
34
|
+
to a post's register, not one a little above the median. Mean sentence length did not separate the
|
|
35
|
+
two groups. The share of sentences under eight words did not either, once measured on the parse
|
|
36
|
+
tree: 3.3–7.3% on the two accepted papers in this repository's corpus, 5.1–7.0% on three of ours.
|
|
37
|
+
|
|
38
|
+
On the corpus (`fixtures/accepted-papers/`) the rule measures:
|
|
39
|
+
|
|
40
|
+
| paper | per 1000 words |
|
|
41
|
+
| ----------------------------------- | -------------: |
|
|
42
|
+
| `secure-acsac24` (ACSAC 2024) | 0.00 |
|
|
43
|
+
| `llm-splained-acsac25` (ACSAC 2025) | 0.00 |
|
|
44
|
+
| `agenticdev-acm26` (ours, accepted) | 1.60 |
|
|
45
|
+
|
|
46
|
+
The vocabulary of machine-written text (_delve_, _crucial_) is a different question, and a different
|
|
47
|
+
check; a paper can avoid every such word and still read as a post.
|
|
48
|
+
|
|
49
|
+
## Examples
|
|
50
|
+
|
|
51
|
+
```latex
|
|
52
|
+
We measured the cost on four tools. But the bill barely moves. % a conjunction start
|
|
53
|
+
We measured the cost on four tools, and the bill barely moves. % none
|
|
54
|
+
We measured the cost on four tools. However, the bill barely moves. % none
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
On a body where such sentences run above the limit, the rule reports:
|
|
58
|
+
|
|
59
|
+
> 7 sentences open with And, So, But, Nor, Or or Yet — 1.60 per 1000 words (first: «…», «…», «…»).
|
|
60
|
+
> Accepted papers measured 0–0.58; this rule warns above 0.8. Join the sentence to the one before,
|
|
61
|
+
> or open with the connective a paper uses (However, Therefore, Moreover)
|
|
62
|
+
|
|
63
|
+
## Options / preset fields
|
|
64
|
+
|
|
65
|
+
In a paper's `paperlint.json`:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"rules": {
|
|
70
|
+
"tex/register": ["warn", { "conjunctionStartsPer1000": 0.8 }]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
| option | default | what it is |
|
|
76
|
+
| -------------------------- | ------: | ---------------------------------------------------------- |
|
|
77
|
+
| `conjunctionStartsPer1000` | 0.8 | the conjunction starts per 1000 words above which it warns |
|
|
@@ -9,6 +9,9 @@ the required sections by title in `required_sections`; the paper must have a `\s
|
|
|
9
9
|
`\section*` with exactly that title (whitespace collapsed), and, when the preset says
|
|
10
10
|
`position: "last"`, no section of the main text may come after it.
|
|
11
11
|
|
|
12
|
+
It reads the sections of the whole paper, the files `paper.tex` includes spliced in where they are
|
|
13
|
+
included, so a required section kept in `sections/llm-usage.tex` counts.
|
|
14
|
+
|
|
12
15
|
It only knows the sections a preset lists. A paper whose `paperlint.json` names no preset, or a
|
|
13
16
|
preset with no `required_sections`, gets no finding.
|
|
14
17
|
|
|
@@ -66,7 +69,6 @@ the bibliography or an appendix is fine. Without `position`, anywhere will do.
|
|
|
66
69
|
## What it does not check
|
|
67
70
|
|
|
68
71
|
- Whether LLMs were used, or what the section says.
|
|
69
|
-
- Sections pulled in with `\input` or `\include`: the rule reads `paper.tex` alone.
|
|
70
72
|
|
|
71
73
|
## How to fix
|
|
72
74
|
|
|
@@ -8,6 +8,17 @@ The paper is not set in the class its venue requires. The rule compares the `\do
|
|
|
8
8
|
of `paper.tex` with the preset's `template`: the class must be the same, and every option the
|
|
9
9
|
template names must be there. The paper may add options of its own.
|
|
10
10
|
|
|
11
|
+
A source that keeps one file for several venues and picks the class behind a TeX switch has several
|
|
12
|
+
`\documentclass` lines. The switch is not evaluated: each line is a candidate, the paper passes when
|
|
13
|
+
one of them is the template's class with its options, and when none is, one finding at the first
|
|
14
|
+
candidate names every one:
|
|
15
|
+
|
|
16
|
+
```latex
|
|
17
|
+
\def\venue{2}
|
|
18
|
+
\if\venue1 \documentclass{article} \fi
|
|
19
|
+
\if\venue2 \documentclass[conference,compsoc]{IEEEtran} \fi % passes: the second candidate matches
|
|
20
|
+
```
|
|
21
|
+
|
|
11
22
|
It only knows the class a preset names. A paper whose `paperlint.json` names no preset, or a
|
|
12
23
|
preset with no `template`, gets no finding.
|
|
13
24
|
|
|
@@ -55,6 +66,7 @@ and no option. A value that is neither is reported as a finding that names the p
|
|
|
55
66
|
requires is there.
|
|
56
67
|
- The version of the class file (#94).
|
|
57
68
|
- A `\documentclass` inside a comment: the rule reads the parse tree, so it does not count.
|
|
69
|
+
- Which branch of a TeX switch builds: with several class lines, one matching candidate is enough.
|
|
58
70
|
|
|
59
71
|
## How to fix
|
|
60
72
|
|
|
@@ -11,7 +11,9 @@ now going to VenueB.
|
|
|
11
11
|
It only knows the venues paperlint ships a preset for, by each preset's `name` and `aliases`. A
|
|
12
12
|
venue with no preset is not recognized.
|
|
13
13
|
|
|
14
|
-
It
|
|
14
|
+
It reads the whole paper — `paper.tex` with the files it `\input`s, `\include`s or `\subfile`s
|
|
15
|
+
spliced in; a name found in an included file is reported at that include, with the file and line at
|
|
16
|
+
the front of the message. It looks at what a reader sees — prose, the author block, captions, footnotes, a citation's note in
|
|
15
17
|
square brackets, and front-matter macros such as `\acmConference` — and ignores comments, citation
|
|
16
18
|
keys, labels, URLs (a `\url`, and the address of an `\href` — its link text is read), math, code and
|
|
17
19
|
the bibliography. Names are matched as whole words, case-sensitive.
|
|
@@ -58,7 +60,7 @@ reported.
|
|
|
58
60
|
## What it does not check
|
|
59
61
|
|
|
60
62
|
- A venue no shipped preset describes.
|
|
61
|
-
- A name split by markup (`\textbf{AI}Sec`)
|
|
63
|
+
- A name split by markup (`\textbf{AI}Sec`).
|
|
62
64
|
|
|
63
65
|
## How to fix
|
|
64
66
|
|
package/docs/rules.md
CHANGED
|
@@ -21,6 +21,9 @@ reads and when it fails. Errors fail `paperlint lint`; warnings print and do not
|
|
|
21
21
|
| [`tex/template`](rules/tex/template.md) | error | `paper.tex` → `paperlint.json` | the `\documentclass` is not the preset's `template` class, or lacks an option it names — [page](rules/tex/template.md) |
|
|
22
22
|
| [`tex/required-section`](rules/tex/required-section.md) | error | `paper.tex` → `paperlint.json` | a section the preset requires (AIDC: «LLM Usage Statement», last) is missing or not where it must be — [page](rules/tex/required-section.md) |
|
|
23
23
|
| [`tex/venue-leftover`](rules/tex/venue-leftover.md) | warn | `paper.tex` → `paperlint.json` | the text names another shipped venue than the one the paper extends («Submission to VenueA 2026» in a paper now set for VenueB) — [page](rules/tex/venue-leftover.md) |
|
|
24
|
+
| [`tex/claim-provenance`](rules/tex/claim-provenance.md) | warn | `paper.tex` | a sentence of the body states a number and does not say whose it is — no citation or link, no _we_/_our_, no `\ref` or "Table 2", no sample ("n = 134", "48 runs") — [page](rules/tex/claim-provenance.md) |
|
|
25
|
+
| [`tex/missing-input`](rules/tex/missing-input.md) | warn | `paper.tex` and the files it includes | an `\input`, `\include` or `\subfile` names a file that is not there, so no rule read its text — [page](rules/tex/missing-input.md) |
|
|
26
|
+
| [`tex/register`](rules/tex/register.md) | warn | `paper.tex` and the files it includes | the body runs to a post's register: more than 0.8 sentences per 1000 words open with _And_, _So_, _But_, _Nor_, _Or_ or _Yet_ — the limit is an option — [page](rules/tex/register.md) |
|
|
24
27
|
| `review/frontmatter` | error | `reviews/*.md` | the review's frontmatter fails paperlint's JSON Schema — a `findings` record without `id`/`status`, an unknown field, or an **open** finding with no `cause` (`skill-defect` · `missing-skill` · `hook` · `rule`). A review with no `findings` key is not checked for findings |
|
|
25
28
|
| `sibling/frontmatter` | warn | `siblings/*.md` (not `README.md`) | a sibling card's frontmatter has no `read:` saying how much of the competing paper was read (`full` · `abstract` · `none`) |
|
|
26
29
|
| `pdf/fresh` | error | `paper.tex` → `_build/paper.facts.json` | the paper names a venue and the facts cannot be judged: not JSON, a schema other than 2, or they describe a PDF that is gone or differs from the one on disk (its SHA-256) |
|
|
@@ -16,7 +16,10 @@ import { readFileSync } from "node:fs";
|
|
|
16
16
|
import { dirname, join } from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
import { texLanguage } from "./latex-language.ts";
|
|
19
|
-
import typography, {
|
|
19
|
+
import typography, {
|
|
20
|
+
crefDefinitionRanges,
|
|
21
|
+
texVisibleRuns,
|
|
22
|
+
} from "./paper-typography.ts";
|
|
20
23
|
import bib from "./bib-reachable-entry.ts";
|
|
21
24
|
|
|
22
25
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -118,6 +121,50 @@ describe("paper/section-word", () => {
|
|
|
118
121
|
expect(ids(await lint(src), "paper/section-word")).toEqual([]);
|
|
119
122
|
});
|
|
120
123
|
|
|
124
|
+
it("🔴 a cleveref name definition is not prose: silent, and --fix leaves it as written", async () => {
|
|
125
|
+
const src =
|
|
126
|
+
"\\documentclass{article}\n\\usepackage{cleveref}\n\\crefname{section}{§}{§§}\n\\Crefname{section}{§}{§§}\n" +
|
|
127
|
+
"\\begin{document}\nText.\n\\end{document}\n";
|
|
128
|
+
expect(ids(await lint(src), "paper/section-word")).toEqual([]);
|
|
129
|
+
expect(await fixed(src)).toBe(src);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("a definition's range: its attached arguments, or the groups after a macro the parser did not sign", () => {
|
|
133
|
+
const signed = "\\crefname{section}{}{§§}";
|
|
134
|
+
expect(crefDefinitionRanges(signed)).toEqual([[0, signed.length]]);
|
|
135
|
+
const at = (s, e) => ({ start: { offset: s }, end: { offset: e } });
|
|
136
|
+
const tree = (macro, after) => ({ content: [macro, ...after] });
|
|
137
|
+
expect(
|
|
138
|
+
crefDefinitionRanges("", {
|
|
139
|
+
parse: () =>
|
|
140
|
+
tree({ type: "macro", content: "Crefname", position: at(0, 9) }, [
|
|
141
|
+
{ type: "group", content: [], position: at(9, 14) },
|
|
142
|
+
{ type: "string", content: "x", position: at(14, 15) },
|
|
143
|
+
]),
|
|
144
|
+
}),
|
|
145
|
+
).toEqual([[0, 14]]);
|
|
146
|
+
// A node with no position has no range; an argument with no content ends nothing.
|
|
147
|
+
expect(
|
|
148
|
+
crefDefinitionRanges("", {
|
|
149
|
+
parse: () =>
|
|
150
|
+
tree(
|
|
151
|
+
{
|
|
152
|
+
type: "macro",
|
|
153
|
+
content: "crefname",
|
|
154
|
+
args: [{ type: "argument" }],
|
|
155
|
+
},
|
|
156
|
+
[],
|
|
157
|
+
),
|
|
158
|
+
}),
|
|
159
|
+
).toEqual([]);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("…while a § in prose next to such a definition is still reported", async () => {
|
|
163
|
+
const src =
|
|
164
|
+
"\\documentclass{article}\n\\crefname{section}{§}{§§}\n\\begin{document}\nSee §5.\n\\end{document}\n";
|
|
165
|
+
expect(ids(await lint(src), "paper/section-word")).toHaveLength(1);
|
|
166
|
+
});
|
|
167
|
+
|
|
121
168
|
it("markdown: `§5` → `Section 5`, code untouched", async () => {
|
|
122
169
|
const src = "See §5 and `§3` here.\n";
|
|
123
170
|
expect(ids(await lint(src, "paper.md"), "paper/section-word")).toHaveLength(
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
*/
|
|
62
62
|
import assert from "node:assert/strict";
|
|
63
63
|
import { getParser } from "@unified-latex/unified-latex-util-parse";
|
|
64
|
+
import { takeWhile } from "remeda";
|
|
64
65
|
import type { Nodes } from "mdast";
|
|
65
66
|
import type { TexArg, TexNode, TexRoot } from "./latex-language.ts";
|
|
66
67
|
import type {
|
|
@@ -121,7 +122,77 @@ function skippedRanges(
|
|
|
121
122
|
}
|
|
122
123
|
const bib = bibRange(raw);
|
|
123
124
|
if (bib) out.push([bib.start, bib.end]);
|
|
124
|
-
return out;
|
|
125
|
+
return [...out, ...crefDefinitionRanges(raw)];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* cleveref's name and format definitions (`\crefname{section}{§}{§§}`): the label a reference
|
|
130
|
+
* prints is set there, not written as prose, so a sign in them is not a sign in the text.
|
|
131
|
+
*/
|
|
132
|
+
const CREF_DEFINITIONS: ReadonlySet<string> = new Set([
|
|
133
|
+
"crefname",
|
|
134
|
+
"Crefname",
|
|
135
|
+
"crefformat",
|
|
136
|
+
"Crefformat",
|
|
137
|
+
"crefrangeformat",
|
|
138
|
+
"Crefrangeformat",
|
|
139
|
+
"crefmultiformat",
|
|
140
|
+
"Crefmultiformat",
|
|
141
|
+
"crefrangemultiformat",
|
|
142
|
+
"Crefrangemultiformat",
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
const isGroup = (n: TexNode): boolean => n.type === "group";
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* A macro's range with its arguments: the ones the parser attached (cleveref's are signed) or the
|
|
149
|
+
* `{…}` groups standing right after it when it was given none — from the first to the last offset
|
|
150
|
+
* any of them carries. An argument's closing brace is one character past its last node.
|
|
151
|
+
*/
|
|
152
|
+
function withArguments(
|
|
153
|
+
m: TexNode & { readonly type: "macro" },
|
|
154
|
+
after: readonly TexNode[],
|
|
155
|
+
): readonly [number, number][] {
|
|
156
|
+
const offsets = [
|
|
157
|
+
m.position?.start.offset,
|
|
158
|
+
m.position?.end.offset,
|
|
159
|
+
...(m.args ?? []).map((a) => {
|
|
160
|
+
const last = a.content?.at(-1)?.position?.end.offset;
|
|
161
|
+
return last === undefined ? undefined : last + 1;
|
|
162
|
+
}),
|
|
163
|
+
takeWhile(after, isGroup).at(-1)?.position?.end.offset,
|
|
164
|
+
].filter((o): o is number => o !== undefined);
|
|
165
|
+
return offsets.length === 0
|
|
166
|
+
? []
|
|
167
|
+
: [[Math.min(...offsets), Math.max(...offsets)]];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The ranges of the cleveref definitions in the preamble and in the document body. `parse` is
|
|
172
|
+
* unified-latex's; a test hands in a tree with the fields the walk tolerates being absent.
|
|
173
|
+
*/
|
|
174
|
+
export function crefDefinitionRanges(
|
|
175
|
+
raw: string,
|
|
176
|
+
{
|
|
177
|
+
parse = (s: string): TexRoot => getParser().parse(s),
|
|
178
|
+
}: { readonly parse?: (s: string) => TexRoot } = {},
|
|
179
|
+
): readonly [number, number][] {
|
|
180
|
+
const root = parse(raw);
|
|
181
|
+
const doc = root.content.find(
|
|
182
|
+
(n): n is Extract<TexNode, { type: "environment" | "mathenv" }> =>
|
|
183
|
+
n.type === "environment" && n.env === "document",
|
|
184
|
+
);
|
|
185
|
+
const lists: readonly (readonly TexNode[])[] = [
|
|
186
|
+
root.content,
|
|
187
|
+
doc?.content ?? [],
|
|
188
|
+
];
|
|
189
|
+
return lists.flatMap((list) =>
|
|
190
|
+
list.flatMap((n, i) =>
|
|
191
|
+
n.type === "macro" && CREF_DEFINITIONS.has(n.content)
|
|
192
|
+
? withArguments(n, list.slice(i + 1))
|
|
193
|
+
: [],
|
|
194
|
+
),
|
|
195
|
+
);
|
|
125
196
|
}
|
|
126
197
|
const inside = (ranges: readonly [number, number][], i: number) =>
|
|
127
198
|
ranges.some(([s, e]) => i >= s && i < e);
|
package/eslint.config.mjs
CHANGED
|
@@ -428,6 +428,7 @@ const TYPESCRIPT_ONLY = [
|
|
|
428
428
|
"test/*.{js,mjs,cjs}",
|
|
429
429
|
"test/e2e/**/*.{js,mjs,cjs}",
|
|
430
430
|
"test/fixtures/layers/src/**/*.{js,mjs,cjs}",
|
|
431
|
+
"fixtures/accepted-papers/**/*.{js,mjs,cjs}",
|
|
431
432
|
"fixtures/real-markdown-paper/**/*.{js,mjs,cjs}",
|
|
432
433
|
"skills/paper-pipeline/scripts/fixtures/**/*.{js,mjs,cjs}",
|
|
433
434
|
"skills/plan-paper-timeline/fixtures/**/*.{js,mjs,cjs}",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ONE reader of `baseline.json`, shared by
|
|
3
|
-
*
|
|
4
|
-
* a consumer actually got)
|
|
5
|
-
*
|
|
2
|
+
* ONE reader of `baseline.json`, shared by every run that compares against one: the harness beside
|
|
3
|
+
* this file (the repository's own `bin/paperlint.mjs`), `test/e2e/install/install.e2e.ts` (the
|
|
4
|
+
* binary a consumer actually got), and `fixtures/accepted-papers/accepted-papers.test.ts` (one
|
|
5
|
+
* baseline per accepted paper). Two copies of "growth fails, a drop never does" would drift the
|
|
6
|
+
* first time one of them is tightened, and the runs would then disagree about the same rules.
|
|
6
7
|
*/
|
|
7
8
|
import { readFileSync } from "node:fs";
|
|
8
9
|
import { dirname, join } from "node:path";
|
|
@@ -21,10 +22,10 @@ const LintOutput = z.array(
|
|
|
21
22
|
|
|
22
23
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
23
24
|
|
|
24
|
-
/** The recorded
|
|
25
|
-
export function recordedFindings(): Counts {
|
|
25
|
+
/** The counts recorded in `dir`'s `baseline.json` (this article's by default), `{ruleId: n}`. */
|
|
26
|
+
export function recordedFindings(dir: string = HERE): Counts {
|
|
26
27
|
return Baseline.parse(
|
|
27
|
-
JSON.parse(readFileSync(join(
|
|
28
|
+
JSON.parse(readFileSync(join(dir, "baseline.json"), "utf8")),
|
|
28
29
|
).findings;
|
|
29
30
|
}
|
|
30
31
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paperlint",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "A linter for scientific papers written in LaTeX: catches mistakes before you submit to a conference or journal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -110,6 +110,7 @@
|
|
|
110
110
|
"eslint-rules",
|
|
111
111
|
"eslint.config.mjs",
|
|
112
112
|
"fixtures",
|
|
113
|
+
"!fixtures/accepted-papers",
|
|
113
114
|
"!fixtures/real-markdown-paper",
|
|
114
115
|
"fixtures/real-markdown-paper/baseline.*",
|
|
115
116
|
"hooks",
|