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/src/tex-paper.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE PAPER, NOT ITS MAIN FILE — what every rule over the LaTeX parse tree reads, and how it reports.
|
|
3
|
+
*
|
|
4
|
+
* `readPaper` gives a rule `paper.tex` with every `\input`, `\include` and `\subfile` spliced in, the
|
|
5
|
+
* way TeX reads it (`assemblePaper`, src/domain/paper-source.ts). `reportInPaper` puts each finding
|
|
6
|
+
* back where its text came from: a finding in `paper.tex` at its own place; a finding in an included
|
|
7
|
+
* file at the include in `paper.tex` that brought it in — the one place ESLint can point to in this
|
|
8
|
+
* file — with the included file, line and column at the front of the message.
|
|
9
|
+
*
|
|
10
|
+
* tex/missing-input warn an `\input`, `\include` or `\subfile` names a file that is not there
|
|
11
|
+
*
|
|
12
|
+
* A missing file is a finding, not a silent skip: the rules would otherwise report a clean body for
|
|
13
|
+
* text they never read, and TeX stops on it too. Warn, not error: a file a build step writes (a
|
|
14
|
+
* table of generated numbers) is legitimately absent before that step has run.
|
|
15
|
+
*/
|
|
16
|
+
import { basename, dirname, join } from "node:path";
|
|
17
|
+
import {
|
|
18
|
+
assemblePaper,
|
|
19
|
+
originOf,
|
|
20
|
+
type PaperSource,
|
|
21
|
+
} from "./domain/paper-source.ts";
|
|
22
|
+
import type { Span } from "./domain/tex-document.ts";
|
|
23
|
+
import type { LatexReader } from "./ports/latex.ts";
|
|
24
|
+
import type { Files } from "./ports/files.ts";
|
|
25
|
+
import { callerPath } from "./caller-path.ts";
|
|
26
|
+
import type {
|
|
27
|
+
Located,
|
|
28
|
+
TexRuleContext,
|
|
29
|
+
TexRuleModule,
|
|
30
|
+
} from "./tex-venue-rules.ts";
|
|
31
|
+
|
|
32
|
+
/** What reading a paper needs: the disk, and the LaTeX reader. */
|
|
33
|
+
export interface PaperDeps {
|
|
34
|
+
readonly files: Files;
|
|
35
|
+
readonly latex: LatexReader;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The paper whose main file is `filename` with the text `src`, its includes spliced. */
|
|
39
|
+
export function readPaper(
|
|
40
|
+
filename: string,
|
|
41
|
+
src: string,
|
|
42
|
+
deps: PaperDeps,
|
|
43
|
+
): PaperSource {
|
|
44
|
+
const dir = dirname(filename);
|
|
45
|
+
return assemblePaper(basename(filename), src, {
|
|
46
|
+
includes: deps.latex.includes,
|
|
47
|
+
documentBody: deps.latex.documentBody,
|
|
48
|
+
read: (rel) => {
|
|
49
|
+
const b = deps.files.readBytes(callerPath(join(dir, rel)));
|
|
50
|
+
return b === null ? null : new TextDecoder().decode(b);
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Line and column (1-based) of an offset in a text. */
|
|
56
|
+
function lineColumn(text: string, at: number): string {
|
|
57
|
+
const line = text.slice(0, at).split("\n").length;
|
|
58
|
+
const column = at - text.lastIndexOf("\n", at - 1);
|
|
59
|
+
return `${String(line)}:${String(column)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A message template with its data filled in, the way ESLint fills it. */
|
|
63
|
+
const filled = (
|
|
64
|
+
template: string,
|
|
65
|
+
data: Readonly<Record<string, string | number>> = {},
|
|
66
|
+
): string =>
|
|
67
|
+
Object.entries(data).reduce(
|
|
68
|
+
(m, [k, v]) => m.replaceAll(`{{${k}}}`, String(v)),
|
|
69
|
+
template,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
/** Where a finding stands in the main file, and — when its text came from another file — which. */
|
|
73
|
+
interface Placed {
|
|
74
|
+
readonly at: Span | null;
|
|
75
|
+
readonly from: string | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A finding's span of the assembled text, as a place in the main file. */
|
|
79
|
+
function placed(paper: PaperSource, at: Span | null): Placed {
|
|
80
|
+
const origin = at === null ? null : originOf(paper, at);
|
|
81
|
+
if (origin === null) return { at: null, from: null };
|
|
82
|
+
if (origin.file === paper.main) return { at: origin.span, from: null };
|
|
83
|
+
return {
|
|
84
|
+
at: origin.via,
|
|
85
|
+
from: `${origin.file}:${lineColumn(origin.source, origin.span.start)}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Each finding reported in the main file: at its own place, or at the include that brought its text
|
|
91
|
+
* in, named in the message (`sections/results.tex:12:3: …`). Offsets are the assembled text's.
|
|
92
|
+
*/
|
|
93
|
+
export function reportInPaper(
|
|
94
|
+
context: TexRuleContext,
|
|
95
|
+
messages: Readonly<Record<string, string>>,
|
|
96
|
+
paper: PaperSource,
|
|
97
|
+
findings: readonly Located[],
|
|
98
|
+
): void {
|
|
99
|
+
const loc = (i: number) => context.sourceCode.getLocFromIndex(i);
|
|
100
|
+
findings.forEach((f) => {
|
|
101
|
+
const p = placed(paper, f.at);
|
|
102
|
+
const where = {
|
|
103
|
+
start: loc(p.at?.start ?? 0),
|
|
104
|
+
end: loc(p.at?.end ?? 0),
|
|
105
|
+
};
|
|
106
|
+
if (p.from === null)
|
|
107
|
+
context.report({ loc: where, messageId: f.messageId, data: f.data });
|
|
108
|
+
else
|
|
109
|
+
context.report({
|
|
110
|
+
loc: where,
|
|
111
|
+
// A message id the rule does not define is printed as itself rather than dropped.
|
|
112
|
+
message: `${p.from}: ${filled(messages[f.messageId] ?? f.messageId, f.data)}`,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const MISSING_META: TexRuleModule["meta"] = {
|
|
118
|
+
type: "problem",
|
|
119
|
+
docs: {
|
|
120
|
+
description:
|
|
121
|
+
"an \\input, \\include or \\subfile names a file that is not there, so no rule read its text",
|
|
122
|
+
url: "https://github.com/zernie/paperlint/blob/main/docs/rules/tex/missing-input.md",
|
|
123
|
+
},
|
|
124
|
+
schema: [],
|
|
125
|
+
messages: {
|
|
126
|
+
missing:
|
|
127
|
+
"`{{target}}` is not there (tried `{{target}}.tex` and `{{target}}`, from the paper's directory): TeX stops on it, and no rule read what it should hold",
|
|
128
|
+
missingIn:
|
|
129
|
+
"`{{target}}`, which {{file}} includes, is not there (tried `{{target}}.tex` and `{{target}}`, from the paper's directory): TeX stops on it, and no rule read what it should hold",
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** The includes that name no file, each reported where it stands in the main file. */
|
|
134
|
+
export const missingInputRule = (deps: PaperDeps): TexRuleModule => ({
|
|
135
|
+
meta: MISSING_META,
|
|
136
|
+
create(context) {
|
|
137
|
+
return {
|
|
138
|
+
root() {
|
|
139
|
+
const sc = context.sourceCode;
|
|
140
|
+
const paper = readPaper(context.filename, sc.raw ?? sc.text, deps);
|
|
141
|
+
const loc = (i: number) => sc.getLocFromIndex(i);
|
|
142
|
+
paper.missing.forEach((m) => {
|
|
143
|
+
context.report({
|
|
144
|
+
loc: { start: loc(m.via.start), end: loc(m.via.end) },
|
|
145
|
+
messageId: m.file === paper.main ? "missing" : "missingIn",
|
|
146
|
+
data: { target: m.target, file: m.file },
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
});
|
|
@@ -79,11 +79,16 @@ function lint(
|
|
|
79
79
|
report: (d) =>
|
|
80
80
|
out.push({
|
|
81
81
|
rule: `tex/${name}`,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
// A finding from an included file arrives as a whole message, its file at the front.
|
|
83
|
+
messageId: "messageId" in d ? d.messageId : "(message)",
|
|
84
|
+
message:
|
|
85
|
+
"messageId" in d
|
|
86
|
+
? Object.entries(d.data ?? {}).reduce(
|
|
87
|
+
(m, [k, v]) => m.replaceAll(`{{${k}}}`, String(v)),
|
|
88
|
+
rule.meta.messages[d.messageId] ??
|
|
89
|
+
`(no message ${d.messageId})`,
|
|
90
|
+
)
|
|
91
|
+
: d.message,
|
|
87
92
|
line: d.loc.start.line,
|
|
88
93
|
}),
|
|
89
94
|
});
|
|
@@ -183,6 +188,83 @@ describe("tex/template — the class line's states: missing, empty, commented ou
|
|
|
183
188
|
});
|
|
184
189
|
});
|
|
185
190
|
|
|
191
|
+
describe("the venue rules read the whole paper: the files it \\inputs, spliced where they stand", () => {
|
|
192
|
+
it("🔴 a required section kept in an included file is found, and its order judged in the paper", () => {
|
|
193
|
+
const tex = paper(
|
|
194
|
+
"\\documentclass[conference,compsoc]{IEEEtran}",
|
|
195
|
+
"Text.\n\\input{sections/closing}",
|
|
196
|
+
);
|
|
197
|
+
const extra = {
|
|
198
|
+
[`${PAPER}/sections/closing.tex`]:
|
|
199
|
+
"\\section*{LLM Usage Statement}\nNone.\n",
|
|
200
|
+
};
|
|
201
|
+
expect(lint(tex, AIDC, { extra })).toEqual([]);
|
|
202
|
+
// Without the file, the section is missing — and tex/missing-input (not a venue rule) says why.
|
|
203
|
+
expect(ids(lint(tex, AIDC))).toEqual(["tex/required-section:missing"]);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("a leftover venue name in an included file is reported at the \\input, naming the file and line", () => {
|
|
207
|
+
const tex = paper(
|
|
208
|
+
"\\documentclass[conference,compsoc]{IEEEtran}",
|
|
209
|
+
"Text.\n\\input{intro}\n\\section*{LLM Usage Statement}\nNone.",
|
|
210
|
+
);
|
|
211
|
+
const fs = lint(tex, AIDC, {
|
|
212
|
+
extra: {
|
|
213
|
+
[`${PAPER}/intro.tex`]: "One line.\nFirst written for AgenticDev.\n",
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
expect(fs.map((f) => [f.line, f.message.split(": ")[0]])).toEqual([
|
|
217
|
+
[5, "intro.tex:2:19"],
|
|
218
|
+
]);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("tex/template — several \\documentclass lines behind a TeX switch", () => {
|
|
223
|
+
/** One source for two venues, the class picked by `\\if` — as an accepted ACSAC paper does it. */
|
|
224
|
+
const switched = (a: string, b: string) =>
|
|
225
|
+
paper(
|
|
226
|
+
`\\def\\venue{2}\n\\if\\venue1\n${a}\n\\fi\n\\if\\venue2\n${b}\n\\fi`,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
it("passes when one of the candidates is the preset's class with its options", () => {
|
|
230
|
+
expect(
|
|
231
|
+
lint(
|
|
232
|
+
switched(
|
|
233
|
+
"\\documentclass[letterpaper,twocolumn]{article}",
|
|
234
|
+
"\\documentclass[conference,compsoc]{IEEEtran}",
|
|
235
|
+
),
|
|
236
|
+
AIDC,
|
|
237
|
+
),
|
|
238
|
+
).toEqual([]);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("reports once, at the first candidate, naming every candidate, when none is the preset's", () => {
|
|
242
|
+
const fs = lint(
|
|
243
|
+
switched(
|
|
244
|
+
"\\documentclass{article}",
|
|
245
|
+
"\\documentclass[conference]{IEEEtran}",
|
|
246
|
+
),
|
|
247
|
+
AIDC,
|
|
248
|
+
);
|
|
249
|
+
expect(ids(fs)).toEqual(["tex/template:noCandidate"]);
|
|
250
|
+
// Line 4: the first candidate, after the comment, `\\def` and `\\if` lines.
|
|
251
|
+
expect(fs[0]?.line).toBe(4);
|
|
252
|
+
expect(fs[0]?.message).toBe(
|
|
253
|
+
"none of the 2 \\documentclass lines is `\\documentclass[conference,compsoc]{IEEEtran}`, which aidc requires: `\\documentclass{article}`, `\\documentclass[conference]{IEEEtran}`. The source picks one behind a TeX switch, which is not evaluated; make one of them the venue's",
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("an empty candidate is named as such", () => {
|
|
258
|
+
const fs = lint(
|
|
259
|
+
switched("\\documentclass{}", "\\documentclass{article}"),
|
|
260
|
+
AIDC,
|
|
261
|
+
);
|
|
262
|
+
expect(fs[0]?.message).toMatch(
|
|
263
|
+
/`\\documentclass\{\}`, `\\documentclass\{article\}`/,
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
186
268
|
describe("tex/template — the shipped ACM and ACL templates", () => {
|
|
187
269
|
it("🔴 acmart without sigconf — the manuscript format — fails every ACM preset", () => {
|
|
188
270
|
for (const settings of [
|
package/src/tex-venue-rules.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
missingOptions,
|
|
29
29
|
runText,
|
|
30
30
|
spanIn,
|
|
31
|
+
type ClassCandidate,
|
|
31
32
|
type ClassLine,
|
|
32
33
|
type DocumentClass,
|
|
33
34
|
type Heading,
|
|
@@ -44,6 +45,8 @@ import {
|
|
|
44
45
|
shippedPresets,
|
|
45
46
|
type Preset,
|
|
46
47
|
} from "./presets.ts";
|
|
48
|
+
import type { PaperSource } from "./domain/paper-source.ts";
|
|
49
|
+
import { readPaper, reportInPaper } from "./tex-paper.ts";
|
|
47
50
|
import type { RequiredSection } from "./tex-requirements.ts";
|
|
48
51
|
import type { Finding, VenueRuleDeps } from "./venue-rules.ts";
|
|
49
52
|
|
|
@@ -55,19 +58,27 @@ export interface Located extends Finding {
|
|
|
55
58
|
/** The slice of ESLint's rule context these rules use. `raw` exists on the `.tex` language's. */
|
|
56
59
|
export interface TexRuleContext {
|
|
57
60
|
readonly filename: string;
|
|
61
|
+
/** The rule's options from the config, as ESLint validated them against its schema. */
|
|
62
|
+
readonly options?: readonly unknown[];
|
|
58
63
|
readonly sourceCode: {
|
|
59
64
|
readonly text: string;
|
|
60
65
|
readonly raw?: string;
|
|
61
66
|
getLocFromIndex(index: number): Readonly<{ line: number; column: number }>;
|
|
62
67
|
};
|
|
63
|
-
report(
|
|
64
|
-
|
|
65
|
-
readonly
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
report(
|
|
69
|
+
d: {
|
|
70
|
+
readonly loc: {
|
|
71
|
+
readonly start: { line: number; column: number };
|
|
72
|
+
readonly end: { line: number; column: number };
|
|
73
|
+
};
|
|
74
|
+
} & (
|
|
75
|
+
| {
|
|
76
|
+
readonly messageId: string;
|
|
77
|
+
readonly data?: Readonly<Record<string, string | number>>;
|
|
78
|
+
}
|
|
79
|
+
| { readonly message: string }
|
|
80
|
+
),
|
|
81
|
+
): void;
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
export interface TexRuleModule {
|
|
@@ -110,6 +121,30 @@ function judgeClass(
|
|
|
110
121
|
}));
|
|
111
122
|
}
|
|
112
123
|
|
|
124
|
+
/** A candidate as the message names it: its line, or an empty `\documentclass{}`. */
|
|
125
|
+
const candidateLine = (c: ClassCandidate): string =>
|
|
126
|
+
c.kind === "class" ? documentClassLine(c) : "\\documentclass{}";
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Several class lines behind a TeX switch: satisfied when any candidate is the template's class
|
|
130
|
+
* with its options; otherwise one finding, at the first candidate, naming every one.
|
|
131
|
+
*/
|
|
132
|
+
function judgeCandidates(
|
|
133
|
+
candidates: readonly [ClassCandidate, ...ClassCandidate[]],
|
|
134
|
+
want: DocumentClass,
|
|
135
|
+
data: TemplateData,
|
|
136
|
+
): readonly Located[] {
|
|
137
|
+
const matches = candidates.some(
|
|
138
|
+
(c) => c.kind === "class" && judgeClass(c, want, data).length === 0,
|
|
139
|
+
);
|
|
140
|
+
if (matches) return [];
|
|
141
|
+
const named = candidates.map((c) => `\`${candidateLine(c)}\``).join(", ");
|
|
142
|
+
const found = { ...data, count: candidates.length, candidates: named };
|
|
143
|
+
return [
|
|
144
|
+
{ messageId: "noCandidate", data: found, at: spanOf(candidates[0].place) },
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
|
|
113
148
|
/** The paper's class line against a template that was read: present, naming a class, that class. */
|
|
114
149
|
function judgeClassLine(
|
|
115
150
|
line: ClassLine,
|
|
@@ -123,6 +158,8 @@ function judgeClassLine(
|
|
|
123
158
|
return [{ messageId: "emptyClass", data, at: spanOf(line.place) }];
|
|
124
159
|
case "class":
|
|
125
160
|
return judgeClass(line, want, data);
|
|
161
|
+
case "ambiguous":
|
|
162
|
+
return judgeCandidates(line.candidates, want, data);
|
|
126
163
|
}
|
|
127
164
|
}
|
|
128
165
|
|
|
@@ -252,7 +289,9 @@ export function judgeLeftover(
|
|
|
252
289
|
}
|
|
253
290
|
|
|
254
291
|
/** Where a rule's page lives: `docs/rules/tex/<name>.md` on the default branch. */
|
|
255
|
-
export const ruleDocsUrl = (
|
|
292
|
+
export const ruleDocsUrl = (
|
|
293
|
+
name: TexVenueRuleName | "claim-provenance" | "register",
|
|
294
|
+
): string =>
|
|
256
295
|
`https://github.com/zernie/paperlint/blob/main/docs/rules/tex/${name}.md`;
|
|
257
296
|
|
|
258
297
|
/** What a judge reads: the paper's source through the reader, its preset, the other venues. */
|
|
@@ -295,6 +334,8 @@ const RULES: Readonly<
|
|
|
295
334
|
"the class is `{{got}}`, and {{venue}} requires `{{template}}` — the page size, fonts and layout the venue checks come from the class",
|
|
296
335
|
missingOption:
|
|
297
336
|
"the class option `{{option}}` is missing: {{venue}} requires `{{template}}`",
|
|
337
|
+
noCandidate:
|
|
338
|
+
"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",
|
|
298
339
|
},
|
|
299
340
|
},
|
|
300
341
|
},
|
|
@@ -368,35 +409,25 @@ export interface TexVenueRuleDeps extends VenueRuleDeps {
|
|
|
368
409
|
}
|
|
369
410
|
|
|
370
411
|
/**
|
|
371
|
-
* What a judge reads for the paper whose `paper.tex` is `filename
|
|
372
|
-
* resolved preset (`pdf/measured` and
|
|
412
|
+
* What a judge reads for the paper whose `paper.tex` is `filename` — the paper with its includes
|
|
413
|
+
* spliced (`readPaper`) — or null when the paper has no resolved preset (`pdf/measured` and
|
|
414
|
+
* `pdf/profile` already say so).
|
|
373
415
|
*/
|
|
374
416
|
function readingOf(
|
|
375
417
|
filename: string,
|
|
376
418
|
src: string,
|
|
377
419
|
deps: TexVenueRuleDeps,
|
|
378
|
-
): Reading | null {
|
|
420
|
+
): { readonly reading: Reading; readonly paper: PaperSource } | null {
|
|
379
421
|
const dir = dirname(filename);
|
|
380
422
|
const p = paperPreset(dir, deps);
|
|
381
423
|
if (p.kind !== "resolved") return null;
|
|
382
424
|
const preset = p.preset;
|
|
383
425
|
const others = () => otherVenues(preset, join(dir, CONFIG_FILE), deps);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
context: TexRuleContext,
|
|
390
|
-
findings: readonly Located[],
|
|
391
|
-
): void {
|
|
392
|
-
const at = (i: number) => context.sourceCode.getLocFromIndex(i);
|
|
393
|
-
findings.forEach((f) => {
|
|
394
|
-
context.report({
|
|
395
|
-
loc: { start: at(f.at?.start ?? 0), end: at(f.at?.end ?? 0) },
|
|
396
|
-
messageId: f.messageId,
|
|
397
|
-
data: f.data,
|
|
398
|
-
});
|
|
399
|
-
});
|
|
426
|
+
const paper = readPaper(filename, src, deps);
|
|
427
|
+
return {
|
|
428
|
+
reading: { src: paper.text, latex: deps.latex, preset, others },
|
|
429
|
+
paper,
|
|
430
|
+
};
|
|
400
431
|
}
|
|
401
432
|
|
|
402
433
|
function rule(name: TexVenueRuleName, deps: TexVenueRuleDeps): TexRuleModule {
|
|
@@ -409,8 +440,9 @@ function rule(name: TexVenueRuleName, deps: TexVenueRuleDeps): TexRuleModule {
|
|
|
409
440
|
return {
|
|
410
441
|
root() {
|
|
411
442
|
const sc = context.sourceCode;
|
|
412
|
-
const
|
|
413
|
-
if (
|
|
443
|
+
const r = readingOf(context.filename, sc.raw ?? sc.text, deps);
|
|
444
|
+
if (r !== null)
|
|
445
|
+
reportInPaper(context, meta.messages, r.paper, judge(r.reading));
|
|
414
446
|
},
|
|
415
447
|
};
|
|
416
448
|
},
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
# Uncited-assertion fixture (markdown path)
|
|
2
|
-
|
|
3
|
-
Documents the `.md` behaviour of the uncited-assertion checker. Keep in the repo (it IS the contract).
|
|
4
|
-
<!-- Since 2026-08-27 the checker is the ESLint rule `paper/uncited-assertion`
|
|
5
|
-
(`eslint-rules/paper-claims.mjs`); this file is its test — see `eslint-rules/paper-claims.harness.mjs`. -->
|
|
6
|
-
|
|
7
|
-
Transformers outperform RNNs by 12%.
|
|
8
|
-
<!-- MUST FLAG: uncited empirical claim about prior work -->
|
|
9
|
-
|
|
10
|
-
Transformers outperform RNNs by 12% [@vaswani2017].
|
|
11
|
-
<!-- MUST NOT FLAG: pandoc [@key] citation attached -->
|
|
12
|
-
|
|
13
|
-
Prior work reports a large improvement over baselines.[^1]
|
|
14
|
-
<!-- MUST NOT FLAG: markdown footnote marker counts as a citation -->
|
|
15
|
-
|
|
16
|
-
The majority of detectors fail on adversarial inputs, per a recent survey ([link](https://example.org/survey)).
|
|
17
|
-
<!-- MUST NOT FLAG: markdown link/URL counts as a citation -->
|
|
18
|
-
|
|
19
|
-
Most prior detectors silently miss adversarial inputs.
|
|
20
|
-
<!-- MUST FLAG: bare fuzzy quantifier, no citation -->
|
|
21
|
-
|
|
22
|
-
We observe a 6% reduction in false positives.
|
|
23
|
-
<!-- MUST NOT FLAG: paper's own result (we-guard) -->
|
|
24
|
-
|
|
25
|
-
Transformers outperform RNNs by 12% [12].
|
|
26
|
-
<!-- MUST NOT FLAG (S6): numeric citation [12] counts as a cite -->
|
|
27
|
-
|
|
28
|
-
Prior detectors reduce error by 20% [3, 4].
|
|
29
|
-
<!-- MUST NOT FLAG (S6): citation list [3, 4] counts -->
|
|
30
|
-
|
|
31
|
-
US-based detectors reject 40% of benign inputs.
|
|
32
|
-
<!-- MUST FLAG (S5): bare country "US" is not the paper's own result -->
|
|
33
|
-
|
|
34
|
-
Our reading of the literature shows transformers outperform every prior baseline.
|
|
35
|
-
<!-- MUST FLAG (S5): claim about OTHERS, "our reading" is not an own result -->
|
|
36
|
-
|
|
37
|
-
Our method reduces false positives by 6% over the strongest baseline.
|
|
38
|
-
<!-- MUST NOT FLAG (S5): genuine own result, "our method" is the subject -->
|
|
39
|
-
|
|
40
|
-
2. Prior detectors silently miss most adversarial inputs.
|
|
41
|
-
<!-- MUST FLAG (S10): ordered-list marker "2." must not be a false boundary / stray number; the claim itself still flags -->
|
|
42
|
-
|
|
43
|
-
[^1]: Some prior study, 2021.
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
% fixtures/uncited-assertions-sample.tex
|
|
2
|
-
% Contract fixture for the uncited-assertion checker — documents exactly what MUST and MUST NOT flag.
|
|
3
|
-
% Since 2026-08-27 that checker is the ESLint rule `paper/uncited-assertion` (eslint-rules/paper-claims.mjs);
|
|
4
|
-
% this file is its test, run by eslint-rules/paper-claims.harness.mjs.
|
|
5
|
-
% Each numbered case is annotated with the expected verdict. Keep in the repo (it IS the contract).
|
|
6
|
-
\documentclass{article}
|
|
7
|
-
\begin{document}
|
|
8
|
-
|
|
9
|
-
% (1) MUST FLAG — uncited empirical claim about prior work (percentage + verb, no cite, no "we").
|
|
10
|
-
Transformers outperform RNNs by 12\%.
|
|
11
|
-
|
|
12
|
-
% (2) MUST NOT FLAG — same claim, now carrying a citation.
|
|
13
|
-
Transformers outperform RNNs by 12\% \cite{vaswani2017}.
|
|
14
|
-
|
|
15
|
-
% (3) MUST NOT FLAG — float pointer; the verb refers to a Table, not to prior work.
|
|
16
|
-
Table 2 shows the aggregated results across all runs.
|
|
17
|
-
|
|
18
|
-
% (4) MUST NOT FLAG — the only number is a year (year guard).
|
|
19
|
-
In 2026, the field continued to attract new benchmarks.
|
|
20
|
-
|
|
21
|
-
% (5) MUST NOT FLAG — the paper's OWN reported result (we/our guard); numbers allowed uncited.
|
|
22
|
-
We find a 6\% reduction in false positives (p<0.05) over the baseline.
|
|
23
|
-
|
|
24
|
-
% (6) MUST NOT FLAG — a definition sentence, even though it contains a number.
|
|
25
|
-
A transformer block is defined as a stack of 6 attention sub-layers.
|
|
26
|
-
|
|
27
|
-
% (7) MUST NOT FLAG — version-number guard (v2.1) and a math-mode number ($n = 512$) both skipped;
|
|
28
|
-
% no "we", so this isolates the version + math guards (no empirical signal survives).
|
|
29
|
-
The baseline model ships as release v2.1 with a hidden size of $n = 512$.
|
|
30
|
-
|
|
31
|
-
% (8) MUST FLAG — bare fuzzy quantifier making a claim about prior work, no cite.
|
|
32
|
-
Most prior detectors silently miss adversarial inputs.
|
|
33
|
-
|
|
34
|
-
% ── regression cases for the adversarial-review fixes (S5–S10) ──────────────────
|
|
35
|
-
|
|
36
|
-
% (S6) MUST NOT FLAG — numeric-style citation [12] is a citation (dominant CS style).
|
|
37
|
-
Transformers outperform RNNs by 12\% [12].
|
|
38
|
-
|
|
39
|
-
% (S6b) MUST NOT FLAG — citation range/list [3, 4] and [1]--[3] both count.
|
|
40
|
-
Prior detectors reduce error by 20\% [3, 4].
|
|
41
|
-
|
|
42
|
-
% (S7) MUST NOT FLAG — biblatex \footcite is a citation macro.
|
|
43
|
-
Prior work improves accuracy by 15\% \footcite{smith2020}.
|
|
44
|
-
|
|
45
|
-
% (S7b) MUST NOT FLAG — \citeauthor / \parencite are citation macros too.
|
|
46
|
-
\citeauthor{jones2019} shows a 9\% reduction over the baseline \parencite{jones2019}.
|
|
47
|
-
|
|
48
|
-
% (S8) MUST NOT FLAG — "cf." must NOT sever the trailing \cite into its own segment.
|
|
49
|
-
RNN baselines underperform on long sequences, cf. \cite{vaswani2017}.
|
|
50
|
-
|
|
51
|
-
% (S8b) MUST NOT FLAG — "e.g." before a capital must not break the sentence off its cite.
|
|
52
|
-
Several detectors miss adversarial inputs, e.g. Gradient attacks \cite{goodfellow2015}.
|
|
53
|
-
|
|
54
|
-
% (S9) MUST NOT FLAG — float pointer via \ref-macro (no literal digit); verb refers to a Table.
|
|
55
|
-
Table~\ref{tab:results} shows the aggregated numbers across all runs.
|
|
56
|
-
|
|
57
|
-
% (S9b) MUST NOT FLAG — \autoref / \Cref float pointers, verb-only.
|
|
58
|
-
\Cref{fig:overview} demonstrates the pipeline end to end.
|
|
59
|
-
|
|
60
|
-
% (S10) MUST NOT FLAG — tabular body: numeric rows are data, not a prose claim.
|
|
61
|
-
The results appear below.
|
|
62
|
-
\begin{tabular}{lll}
|
|
63
|
-
Model & Acc & F1 \\
|
|
64
|
-
BERT & 0.91 & 0.89 \\
|
|
65
|
-
GPT & 0.94 & 0.93 \\
|
|
66
|
-
\end{tabular}
|
|
67
|
-
|
|
68
|
-
% (S5) MUST FLAG — bare country "US" must NOT be read as the paper's own result ("us").
|
|
69
|
-
US-based detectors reject 40\% of benign inputs.
|
|
70
|
-
|
|
71
|
-
% (S5b) MUST FLAG — "Our reading of the literature" is a claim about OTHERS, not an own result.
|
|
72
|
-
Our reading of the literature shows transformers outperform every prior baseline.
|
|
73
|
-
|
|
74
|
-
% (S5c) MUST NOT FLAG — genuine own result: "our method" is the subject.
|
|
75
|
-
Our method reduces false positives by 6\% over the strongest baseline.
|
|
76
|
-
|
|
77
|
-
\end{document}
|