paperlint 4.0.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/.github/workflows/ci.yml +16 -23
- package/CLAUDE.md +55 -11
- package/CONTRIBUTING.md +30 -7
- package/README.md +11 -8
- package/dist/adapters/banal/invocation.d.ts +1 -1
- package/dist/adapters/latex/document-class.d.ts +23 -0
- package/dist/adapters/latex/document-class.d.ts.map +1 -0
- package/dist/adapters/latex/document-class.js +110 -0
- package/dist/adapters/latex/document-class.js.map +1 -0
- package/dist/adapters/latex/hidden.d.ts +16 -0
- package/dist/adapters/latex/hidden.d.ts.map +1 -0
- package/dist/adapters/latex/hidden.js +49 -0
- package/dist/adapters/latex/hidden.js.map +1 -0
- 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 +33 -0
- package/dist/adapters/latex/index.d.ts.map +1 -0
- package/dist/adapters/latex/index.js +25 -0
- package/dist/adapters/latex/index.js.map +1 -0
- package/dist/adapters/latex/nodes.d.ts +58 -0
- package/dist/adapters/latex/nodes.d.ts.map +1 -0
- package/dist/adapters/latex/nodes.js +97 -0
- package/dist/adapters/latex/nodes.js.map +1 -0
- package/dist/adapters/latex/outline.d.ts +10 -0
- package/dist/adapters/latex/outline.d.ts.map +1 -0
- package/dist/adapters/latex/outline.js +39 -0
- package/dist/adapters/latex/outline.js.map +1 -0
- package/dist/adapters/latex/parse.d.ts +42 -0
- package/dist/adapters/latex/parse.d.ts.map +1 -0
- package/dist/adapters/latex/parse.js +95 -0
- package/dist/adapters/latex/parse.js.map +1 -0
- 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/adapters/latex/rendered.d.ts +10 -0
- package/dist/adapters/latex/rendered.d.ts.map +1 -0
- package/dist/adapters/latex/rendered.js +39 -0
- package/dist/adapters/latex/rendered.js.map +1 -0
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
- package/dist/adapters/pdfjs/fill.js +12 -12
- package/dist/adapters/pdfjs/fill.js.map +1 -1
- package/dist/build.js +4 -4
- package/dist/build.js.map +1 -1
- 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 +49 -5
- 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 +119 -0
- package/dist/domain/tex-document.d.ts.map +1 -0
- package/dist/domain/tex-document.js +43 -0
- package/dist/domain/tex-document.js.map +1 -0
- 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/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +2 -2
- package/dist/facts-file.js.map +1 -1
- package/dist/new-paper.d.ts +5 -0
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +3 -0
- package/dist/new-paper.js.map +1 -1
- package/dist/package-dirs.d.ts +5 -0
- package/dist/package-dirs.d.ts.map +1 -0
- package/dist/package-dirs.js +14 -0
- package/dist/package-dirs.js.map +1 -0
- package/dist/ports/latex.d.ts +35 -0
- package/dist/ports/latex.d.ts.map +1 -0
- package/dist/ports/latex.js +2 -0
- package/dist/ports/latex.js.map +1 -0
- package/dist/presets.d.ts +15 -2
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +19 -6
- package/dist/presets.js.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-requirements.d.ts +11 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +14 -8
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +88 -0
- package/dist/tex-venue-rules.d.ts.map +1 -0
- package/dist/tex-venue-rules.js +268 -0
- package/dist/tex-venue-rules.js.map +1 -0
- package/dist/venue-rules.d.ts +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/docs/configuration.md +3 -2
- package/docs/e2e.md +69 -40
- package/docs/install.md +1 -1
- package/docs/prior-art/README.md +1 -1
- package/docs/prior-art/package-location.md +5 -5
- package/docs/prior-art/repro/README.md +1 -1
- package/docs/prior-art/test-tooling.md +21 -10
- 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 +75 -0
- package/docs/rules/tex/template.md +74 -0
- package/docs/rules/tex/venue-leftover.md +73 -0
- package/docs/rules.md +73 -48
- package/docs/testing.md +17 -4
- package/docs/texlive-install-decision.md +1 -1
- package/docs/toolchain.md +2 -2
- package/eslint-rules/paper-typography.test.mjs +48 -1
- package/eslint-rules/paper-typography.ts +72 -1
- package/eslint.config.mjs +156 -15
- package/fixtures/build-e2e/acmart/expect.json +26 -0
- package/fixtures/build-e2e/aidc/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/aidc/expect.json +14 -0
- package/fixtures/build-e2e/aidc/paper.tex +46 -0
- package/fixtures/build-e2e/aidc/paperlint.json +1 -0
- package/fixtures/build-e2e/broken/expect.json +11 -0
- package/fixtures/build-e2e/cite/expect.json +27 -0
- package/fixtures/build-e2e/empty/expect.json +12 -0
- package/fixtures/build-e2e/fallback/expect.json +53 -0
- package/fixtures/build-e2e/guards/expect.json +6 -0
- package/fixtures/build-e2e/no-source/expect.json +10 -0
- package/fixtures/build-e2e/unbalanced/expect.json +60 -0
- package/fixtures/pdf-facts/README.md +1 -1
- package/fixtures/real-markdown-paper/baseline.ts +8 -7
- package/fixtures/render-paper/build-clean.sh +3 -5
- package/package.json +14 -2
- package/{skills/submit-paper/references/venues → presets}/acm-sigconf.jsonc +4 -1
- package/{skills/submit-paper/references/venues → presets}/agenticdev.jsonc +4 -0
- package/presets/aidc.jsonc +73 -0
- package/{skills/submit-paper/references/venues → presets}/aisec.jsonc +4 -0
- package/presets/ieee-conference.jsonc +66 -0
- package/{skills/submit-paper/references/venues → presets}/realm.jsonc +8 -1
- package/{skills/submit-paper/references/venues → presets/tex}/paper-guards.tex +1 -1
- package/{skills/submit-paper/references/venues → presets}/venue-profile.schema.json +30 -2
- package/scripts/check.harness.ts +64 -10
- package/scripts/check.test.ts +60 -13
- package/scripts/check.ts +83 -30
- package/scripts/test.test.ts +1 -1
- package/scripts/test.ts +1 -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/PIPELINE-MAP.md +2 -2
- package/skills/paper-pipeline/SKILL.md +4 -4
- package/skills/paper-pipeline/SKILL.md.spec.ts +3 -3
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +6 -6
- package/skills/paper-pipeline/references/writing-craft.md +53 -18
- package/skills/paper-pipeline/scripts/consumer.d.mts +0 -2
- package/skills/paper-pipeline/scripts/consumer.mjs +1 -15
- package/skills/pc-panel-review/SKILL.md +36 -11
- package/skills/pc-panel-review/SKILL.md.spec.ts +35 -10
- package/skills/render-paper/SKILL.md +2 -2
- package/skills/render-paper/SKILL.md.spec.ts +1 -1
- package/skills/render-paper/check-render.sh +16 -63
- package/skills/render-paper/render-paper.harness.mjs +17 -59
- package/src/adapters/banal/index.test.ts +1 -1
- package/src/adapters/banal/invocation.ts +1 -1
- package/src/adapters/banal/output.test.ts +1 -1
- package/src/adapters/banal/run.test.ts +1 -1
- package/src/adapters/banal/xml.test.ts +1 -1
- package/src/adapters/latex/document-class.ts +155 -0
- package/src/adapters/latex/hidden.ts +70 -0
- package/src/adapters/latex/includes.ts +66 -0
- package/src/adapters/latex/index.test.ts +535 -0
- package/src/adapters/latex/index.ts +65 -0
- package/src/adapters/latex/nodes.ts +150 -0
- package/src/adapters/latex/outline.ts +64 -0
- package/src/adapters/latex/parse.ts +137 -0
- package/src/adapters/latex/prose.ts +484 -0
- package/src/adapters/latex/rendered.ts +59 -0
- package/src/adapters/pdfjs/fill.test.ts +1 -1
- package/src/adapters/pdfjs/fill.ts +18 -10
- package/src/build.harness.mjs +5 -7
- package/src/build.ts +4 -4
- package/src/claim-provenance.test.ts +352 -0
- package/src/claim-provenance.ts +217 -0
- package/src/cli.harness.mjs +1 -1
- package/src/cli.test.ts +2 -8
- package/src/cli.ts +53 -8
- 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.test.ts +80 -0
- package/src/domain/tex-document.ts +143 -0
- package/src/engine.harness.mjs +1 -1
- package/src/facts-file.test.ts +4 -7
- package/src/facts-file.ts +2 -2
- package/src/latex-loop.harness.ts +1 -1
- package/src/new-paper.test.ts +28 -3
- package/src/new-paper.ts +8 -0
- package/src/package-dirs.ts +17 -0
- package/src/paper-settings-commands.test.ts +6 -1
- package/src/ports/latex.ts +42 -0
- package/src/presets.test.ts +65 -4
- package/src/presets.ts +38 -15
- package/src/register.test.ts +165 -0
- package/src/register.ts +177 -0
- package/src/rule-cases.test.ts +524 -0
- package/src/rule-docs.test.ts +159 -0
- package/src/tex-paper.test.ts +131 -0
- package/src/tex-paper.ts +152 -0
- package/src/tex-requirements.harness.ts +13 -23
- package/src/tex-requirements.ts +35 -8
- package/src/tex-venue-rules.test.ts +741 -0
- package/src/tex-venue-rules.ts +461 -0
- package/src/toolchain.harness.mjs +1 -1
- package/src/venue-rules.test.ts +96 -4
- package/src/venue-rules.ts +1 -1
- package/templates/paper/paper.tex +2 -1
- package/skills/harden-paper/fixtures/uncited-assertions-sample.md +0 -43
- package/skills/harden-paper/fixtures/uncited-assertions-sample.tex +0 -77
- /package/{skills/submit-paper/references/venues → presets/tex}/agenticdev.tex +0 -0
- /package/{skills/submit-paper/references/venues → presets}/tex-base.jsonc +0 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE VENUE-CONFORMANCE RULES OVER THE SOURCE — `paper.tex` judged against what its venue preset
|
|
3
|
+
* requires of the LaTeX itself, before any build:
|
|
4
|
+
*
|
|
5
|
+
* tex/template error the `\documentclass` is the preset's class, with every option it names
|
|
6
|
+
* tex/required-section error each section the preset requires is there, titled exactly, and
|
|
7
|
+
* where the preset says (`last`: after every section of the body)
|
|
8
|
+
* tex/venue-leftover warn the text a reader sees names ANOTHER shipped venue — its `name`
|
|
9
|
+
* or one of its `aliases` — than the one the paper extends
|
|
10
|
+
*
|
|
11
|
+
* ── WHY THEY LIVE HERE AND NOT IN `eslint-rules/` ────────────────────────────────
|
|
12
|
+
* Like the `pdf/*` venue rules (`venue-rules.ts`), they need the paper's resolved preset —
|
|
13
|
+
* `paperPreset`, the one answer to "which venue is this paper judged against" — and read it through
|
|
14
|
+
* the `Files` port. What they read of the source comes through the `LatexReader` port
|
|
15
|
+
* (`src/ports/latex.ts`, implemented over unified-latex in `src/adapters/latex/`): the parse tree of
|
|
16
|
+
* `sourceCode.raw`, never a search of the text.
|
|
17
|
+
*
|
|
18
|
+
* ── WHO SPEAKS WHEN THERE IS NOTHING TO JUDGE ───────────────────────────────────
|
|
19
|
+
* No `paperlint.json`, no `extends`, or a preset that does not resolve: silent. `pdf/measured` and
|
|
20
|
+
* `pdf/profile` already say so, once. A preset that names no template: silent — there is nothing to
|
|
21
|
+
* compare, and a template-family preset is where it is named.
|
|
22
|
+
*/
|
|
23
|
+
import { basename, dirname, join } from "node:path";
|
|
24
|
+
import { CONFIG_FILE } from "#lib/paper-config";
|
|
25
|
+
import {
|
|
26
|
+
collapse,
|
|
27
|
+
documentClassLine,
|
|
28
|
+
missingOptions,
|
|
29
|
+
runText,
|
|
30
|
+
spanIn,
|
|
31
|
+
type ClassCandidate,
|
|
32
|
+
type ClassLine,
|
|
33
|
+
type DocumentClass,
|
|
34
|
+
type Heading,
|
|
35
|
+
type Outline,
|
|
36
|
+
type Place,
|
|
37
|
+
type Span,
|
|
38
|
+
type TextRun,
|
|
39
|
+
} from "./domain/tex-document.ts";
|
|
40
|
+
import type { LatexReader } from "./ports/latex.ts";
|
|
41
|
+
import {
|
|
42
|
+
paperPreset,
|
|
43
|
+
resolvePreset,
|
|
44
|
+
SHIPPED_PREFIX,
|
|
45
|
+
shippedPresets,
|
|
46
|
+
type Preset,
|
|
47
|
+
} from "./presets.ts";
|
|
48
|
+
import type { PaperSource } from "./domain/paper-source.ts";
|
|
49
|
+
import { readPaper, reportInPaper } from "./tex-paper.ts";
|
|
50
|
+
import type { RequiredSection } from "./tex-requirements.ts";
|
|
51
|
+
import type { Finding, VenueRuleDeps } from "./venue-rules.ts";
|
|
52
|
+
|
|
53
|
+
/** A finding and where it points; null points at the top of the file. */
|
|
54
|
+
export interface Located extends Finding {
|
|
55
|
+
readonly at: Span | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The slice of ESLint's rule context these rules use. `raw` exists on the `.tex` language's. */
|
|
59
|
+
export interface TexRuleContext {
|
|
60
|
+
readonly filename: string;
|
|
61
|
+
/** The rule's options from the config, as ESLint validated them against its schema. */
|
|
62
|
+
readonly options?: readonly unknown[];
|
|
63
|
+
readonly sourceCode: {
|
|
64
|
+
readonly text: string;
|
|
65
|
+
readonly raw?: string;
|
|
66
|
+
getLocFromIndex(index: number): Readonly<{ line: number; column: number }>;
|
|
67
|
+
};
|
|
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;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface TexRuleModule {
|
|
85
|
+
readonly meta: {
|
|
86
|
+
readonly type: "problem" | "suggestion";
|
|
87
|
+
readonly docs: { readonly description: string; readonly url: string };
|
|
88
|
+
readonly schema: readonly object[];
|
|
89
|
+
readonly messages: Readonly<Record<string, string>>;
|
|
90
|
+
};
|
|
91
|
+
create(context: TexRuleContext): { root?: () => void };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── the judges: what the source says + a resolved preset → findings. Pure. ─────────
|
|
95
|
+
|
|
96
|
+
/** A place as a finding points at it: its span, or null — the top of the file — when unplaced. */
|
|
97
|
+
const spanOf = (p: Place): Span | null => (p.kind === "at" ? p.span : null);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* What every template finding names: the venue and the class line it requires. A type alias, not an
|
|
101
|
+
* interface: a finding's `data` is a string record, and only an alias is assignable to one.
|
|
102
|
+
*/
|
|
103
|
+
type TemplateData = {
|
|
104
|
+
readonly venue: string;
|
|
105
|
+
readonly template: string;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** A class line that names a class, against the template's: the same class, with every option. */
|
|
109
|
+
function judgeClass(
|
|
110
|
+
line: Extract<ClassLine, { readonly kind: "class" }>,
|
|
111
|
+
want: DocumentClass,
|
|
112
|
+
data: TemplateData,
|
|
113
|
+
): readonly Located[] {
|
|
114
|
+
const at = spanOf(line.place);
|
|
115
|
+
if (line.cls !== want.cls)
|
|
116
|
+
return [{ messageId: "wrongClass", data: { ...data, got: line.cls }, at }];
|
|
117
|
+
return missingOptions(line, want).map((option) => ({
|
|
118
|
+
messageId: "missingOption",
|
|
119
|
+
data: { ...data, option },
|
|
120
|
+
at,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
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
|
+
|
|
148
|
+
/** The paper's class line against a template that was read: present, naming a class, that class. */
|
|
149
|
+
function judgeClassLine(
|
|
150
|
+
line: ClassLine,
|
|
151
|
+
want: DocumentClass,
|
|
152
|
+
data: TemplateData,
|
|
153
|
+
): readonly Located[] {
|
|
154
|
+
switch (line.kind) {
|
|
155
|
+
case "missing":
|
|
156
|
+
return [{ messageId: "noClass", data, at: null }];
|
|
157
|
+
case "empty":
|
|
158
|
+
return [{ messageId: "emptyClass", data, at: spanOf(line.place) }];
|
|
159
|
+
case "class":
|
|
160
|
+
return judgeClass(line, want, data);
|
|
161
|
+
case "ambiguous":
|
|
162
|
+
return judgeCandidates(line.candidates, want, data);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The paper's class line against the preset's template: a template the reader cannot read is said
|
|
168
|
+
* once; then the class must be there, name a class, be the template's class, and carry every
|
|
169
|
+
* option the template names.
|
|
170
|
+
*/
|
|
171
|
+
export function judgeTemplate(
|
|
172
|
+
line: ClassLine,
|
|
173
|
+
preset: Preset,
|
|
174
|
+
latex: Pick<LatexReader, "template">,
|
|
175
|
+
): readonly Located[] {
|
|
176
|
+
const t = preset.template;
|
|
177
|
+
if (t === null) return [];
|
|
178
|
+
const want = latex.template(t.text);
|
|
179
|
+
if (want === null) {
|
|
180
|
+
const data = { file: t.file, text: JSON.stringify(t.text) };
|
|
181
|
+
return [{ messageId: "badTemplate", data, at: null }];
|
|
182
|
+
}
|
|
183
|
+
const data = { venue: preset.label, template: documentClassLine(want) };
|
|
184
|
+
return judgeClassLine(line, want, data);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Where a heading starts, or null when the parser gave it no place. */
|
|
188
|
+
const startOf = (p: Place): number | null =>
|
|
189
|
+
p.kind === "at" ? p.span.start : null;
|
|
190
|
+
|
|
191
|
+
/** A required section that is absent, reported where the document ends — where it would go. */
|
|
192
|
+
const missingSection = (
|
|
193
|
+
outline: Outline,
|
|
194
|
+
data: { readonly venue: string; readonly title: string },
|
|
195
|
+
): Located => ({
|
|
196
|
+
messageId: "missing",
|
|
197
|
+
data,
|
|
198
|
+
at: outline.end === null ? null : { start: outline.end, end: outline.end },
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
/** The first section of the body that starts after `start` and is not titled `title`. */
|
|
202
|
+
function bodySectionAfter(
|
|
203
|
+
outline: Outline,
|
|
204
|
+
start: number,
|
|
205
|
+
title: string,
|
|
206
|
+
): Heading | undefined {
|
|
207
|
+
const backMatter = outline.backMatter ?? Number.POSITIVE_INFINITY;
|
|
208
|
+
return outline.sections.find((h) => {
|
|
209
|
+
const s = startOf(h.place);
|
|
210
|
+
return s !== null && s > start && s < backMatter && h.title !== title;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* One required section against the outline: missing (reported where the document ends, where it
|
|
216
|
+
* would go), or — for `last` — standing before a section of the body. The body is every section
|
|
217
|
+
* before the appendix and the bibliography, so the required one may follow the bibliography. A
|
|
218
|
+
* heading without a place cannot be ordered, so it is never reported as out of order.
|
|
219
|
+
*/
|
|
220
|
+
function judgeSection(
|
|
221
|
+
outline: Outline,
|
|
222
|
+
want: RequiredSection,
|
|
223
|
+
venue: string,
|
|
224
|
+
): readonly Located[] {
|
|
225
|
+
const title = collapse(want.title);
|
|
226
|
+
const last = outline.sections.filter((h) => h.title === title).at(-1);
|
|
227
|
+
const data = { venue, title };
|
|
228
|
+
if (last === undefined) return [missingSection(outline, data)];
|
|
229
|
+
const lastStart = startOf(last.place);
|
|
230
|
+
if (want.position !== "last" || lastStart === null) return [];
|
|
231
|
+
const after = bodySectionAfter(outline, lastStart, title);
|
|
232
|
+
if (after === undefined) return [];
|
|
233
|
+
const notLast = { ...data, after: after.title };
|
|
234
|
+
return [{ messageId: "notLast", data: notLast, at: spanOf(last.place) }];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Every section the preset requires, against the paper's outline. */
|
|
238
|
+
export function judgeRequiredSections(
|
|
239
|
+
outline: () => Outline,
|
|
240
|
+
preset: Preset,
|
|
241
|
+
): readonly Located[] {
|
|
242
|
+
if (preset.requiredSections.length === 0) return [];
|
|
243
|
+
const o = outline();
|
|
244
|
+
return preset.requiredSections.flatMap((r) =>
|
|
245
|
+
judgeSection(o, r, preset.label),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Another shipped venue: the word for it, and what it is called in a paper's text. */
|
|
250
|
+
export interface OtherVenue {
|
|
251
|
+
readonly label: string;
|
|
252
|
+
readonly aliases: readonly string[];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** `s` as a pattern that matches it literally. */
|
|
256
|
+
const literal = (s: string): string =>
|
|
257
|
+
s.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Each place the rendered text names another venue — one of its aliases as a whole word, case
|
|
261
|
+
* and all — unless that alias is also this paper's venue's own (a shared parent conference).
|
|
262
|
+
*/
|
|
263
|
+
export function judgeLeftover(
|
|
264
|
+
runs: readonly TextRun[],
|
|
265
|
+
preset: Preset,
|
|
266
|
+
others: readonly OtherVenue[],
|
|
267
|
+
): readonly Located[] {
|
|
268
|
+
const own = new Set(preset.aliases);
|
|
269
|
+
const names = others.flatMap((o) =>
|
|
270
|
+
o.aliases
|
|
271
|
+
.filter((a) => !own.has(a))
|
|
272
|
+
.map((alias) => ({
|
|
273
|
+
other: o.label,
|
|
274
|
+
re: new RegExp(
|
|
275
|
+
`(?<![\\p{L}\\p{N}])${literal(alias)}(?![\\p{L}\\p{N}])`,
|
|
276
|
+
"gu",
|
|
277
|
+
),
|
|
278
|
+
})),
|
|
279
|
+
);
|
|
280
|
+
return runs.flatMap((run) =>
|
|
281
|
+
names.flatMap(({ other, re }) =>
|
|
282
|
+
[...runText(run).matchAll(re)].map((m) => ({
|
|
283
|
+
messageId: "leftover",
|
|
284
|
+
data: { name: m[0], other, venue: preset.label },
|
|
285
|
+
at: spanIn(run, m.index, m.index + m[0].length),
|
|
286
|
+
})),
|
|
287
|
+
),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Where a rule's page lives: `docs/rules/tex/<name>.md` on the default branch. */
|
|
292
|
+
export const ruleDocsUrl = (
|
|
293
|
+
name: TexVenueRuleName | "claim-provenance" | "register",
|
|
294
|
+
): string =>
|
|
295
|
+
`https://github.com/zernie/paperlint/blob/main/docs/rules/tex/${name}.md`;
|
|
296
|
+
|
|
297
|
+
/** What a judge reads: the paper's source through the reader, its preset, the other venues. */
|
|
298
|
+
interface Reading {
|
|
299
|
+
readonly src: string;
|
|
300
|
+
readonly latex: LatexReader;
|
|
301
|
+
readonly preset: Preset;
|
|
302
|
+
readonly others: () => readonly OtherVenue[];
|
|
303
|
+
}
|
|
304
|
+
type Judge = (r: Reading) => readonly Located[];
|
|
305
|
+
|
|
306
|
+
export type TexVenueRuleName =
|
|
307
|
+
"template" | "required-section" | "venue-leftover";
|
|
308
|
+
|
|
309
|
+
const RULES: Readonly<
|
|
310
|
+
Record<
|
|
311
|
+
TexVenueRuleName,
|
|
312
|
+
{ readonly judge: Judge; readonly meta: TexRuleModule["meta"] }
|
|
313
|
+
>
|
|
314
|
+
> = {
|
|
315
|
+
template: {
|
|
316
|
+
judge: (r) =>
|
|
317
|
+
judgeTemplate(r.latex.documentClass(r.src), r.preset, r.latex),
|
|
318
|
+
meta: {
|
|
319
|
+
type: "problem",
|
|
320
|
+
docs: {
|
|
321
|
+
description:
|
|
322
|
+
"the \\documentclass is the venue preset's class, with every option the preset names",
|
|
323
|
+
url: ruleDocsUrl("template"),
|
|
324
|
+
},
|
|
325
|
+
schema: [],
|
|
326
|
+
messages: {
|
|
327
|
+
noClass:
|
|
328
|
+
"paper.tex has no \\documentclass; {{venue}} requires `{{template}}`",
|
|
329
|
+
emptyClass:
|
|
330
|
+
"the \\documentclass names no class; {{venue}} requires `{{template}}`",
|
|
331
|
+
badTemplate:
|
|
332
|
+
'{{file}}: the preset\'s "template" {{text}} is neither a \\documentclass line nor a class name, so the class cannot be checked',
|
|
333
|
+
wrongClass:
|
|
334
|
+
"the class is `{{got}}`, and {{venue}} requires `{{template}}` — the page size, fonts and layout the venue checks come from the class",
|
|
335
|
+
missingOption:
|
|
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",
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
"required-section": {
|
|
343
|
+
judge: (r) => judgeRequiredSections(() => r.latex.outline(r.src), r.preset),
|
|
344
|
+
meta: {
|
|
345
|
+
type: "problem",
|
|
346
|
+
docs: {
|
|
347
|
+
description:
|
|
348
|
+
"every section the venue preset requires is there, titled exactly, and where the preset says",
|
|
349
|
+
url: ruleDocsUrl("required-section"),
|
|
350
|
+
},
|
|
351
|
+
schema: [],
|
|
352
|
+
messages: {
|
|
353
|
+
missing:
|
|
354
|
+
"{{venue}} requires a section titled «{{title}}» and there is none — add `\\section*{{{title}}}` (the title exactly; a bold paragraph does not count)",
|
|
355
|
+
notLast:
|
|
356
|
+
"«{{title}}» must close the paper at {{venue}}, and the section «{{after}}» comes after it — move it after the last section of the body (after the bibliography is fine)",
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
"venue-leftover": {
|
|
361
|
+
judge: (r) =>
|
|
362
|
+
judgeLeftover(r.latex.renderedRuns(r.src), r.preset, r.others()),
|
|
363
|
+
meta: {
|
|
364
|
+
type: "suggestion",
|
|
365
|
+
docs: {
|
|
366
|
+
description:
|
|
367
|
+
"the text names another shipped venue than the one the paper extends — a leftover from an earlier submission",
|
|
368
|
+
url: ruleDocsUrl("venue-leftover"),
|
|
369
|
+
},
|
|
370
|
+
schema: [],
|
|
371
|
+
messages: {
|
|
372
|
+
leftover:
|
|
373
|
+
"«{{name}}» names {{other}}, and this paper extends {{venue}} — a leftover from an earlier submission? A reviewer reads it before the abstract. Comments and citation keys are not reported; a sentence that names the other venue on purpose can keep it with a disable directive",
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
/** The level each rule is on at in paperlint's own config, for every `paper.tex`. */
|
|
380
|
+
export const TEX_VENUE_RULE_LEVELS: Readonly<
|
|
381
|
+
Record<`tex/${TexVenueRuleName}`, "error" | "warn">
|
|
382
|
+
> = {
|
|
383
|
+
"tex/template": "error",
|
|
384
|
+
"tex/required-section": "error",
|
|
385
|
+
"tex/venue-leftover": "warn",
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Every shipped venue but the ones on this paper's own chain, each resolved the way a paper
|
|
390
|
+
* extending it would be. A preset that does not resolve is skipped: `pdf/profile` owns that.
|
|
391
|
+
*/
|
|
392
|
+
export function otherVenues(
|
|
393
|
+
preset: Preset,
|
|
394
|
+
fromFile: string,
|
|
395
|
+
deps: VenueRuleDeps,
|
|
396
|
+
): readonly OtherVenue[] {
|
|
397
|
+
return shippedPresets(deps.venuesDir).flatMap((name) => {
|
|
398
|
+
const r = resolvePreset(`${SHIPPED_PREFIX}${name}`, fromFile, deps);
|
|
399
|
+
// A preset all of whose files are on this paper's chain is this venue or one it extends.
|
|
400
|
+
return r.ok && !r.value.chain.every((f) => preset.chain.includes(f))
|
|
401
|
+
? [{ label: r.value.label, aliases: r.value.aliases }]
|
|
402
|
+
: [];
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** What the venue-conformance rules are built with: the preset store, and the LaTeX reader. */
|
|
407
|
+
export interface TexVenueRuleDeps extends VenueRuleDeps {
|
|
408
|
+
readonly latex: LatexReader;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
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).
|
|
415
|
+
*/
|
|
416
|
+
function readingOf(
|
|
417
|
+
filename: string,
|
|
418
|
+
src: string,
|
|
419
|
+
deps: TexVenueRuleDeps,
|
|
420
|
+
): { readonly reading: Reading; readonly paper: PaperSource } | null {
|
|
421
|
+
const dir = dirname(filename);
|
|
422
|
+
const p = paperPreset(dir, deps);
|
|
423
|
+
if (p.kind !== "resolved") return null;
|
|
424
|
+
const preset = p.preset;
|
|
425
|
+
const others = () => otherVenues(preset, join(dir, CONFIG_FILE), deps);
|
|
426
|
+
const paper = readPaper(filename, src, deps);
|
|
427
|
+
return {
|
|
428
|
+
reading: { src: paper.text, latex: deps.latex, preset, others },
|
|
429
|
+
paper,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function rule(name: TexVenueRuleName, deps: TexVenueRuleDeps): TexRuleModule {
|
|
434
|
+
const { judge, meta } = RULES[name];
|
|
435
|
+
return {
|
|
436
|
+
meta,
|
|
437
|
+
create(context) {
|
|
438
|
+
// Judged once per paper, on its paper.tex: the paper directory is the file's directory.
|
|
439
|
+
if (basename(context.filename) !== "paper.tex") return {};
|
|
440
|
+
return {
|
|
441
|
+
root() {
|
|
442
|
+
const sc = context.sourceCode;
|
|
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));
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** The venue-conformance rules, as rules of the `tex` plugin (beside `tex/future-promise`). */
|
|
453
|
+
export function texVenueRules(
|
|
454
|
+
deps: TexVenueRuleDeps,
|
|
455
|
+
): Readonly<Record<TexVenueRuleName, TexRuleModule>> {
|
|
456
|
+
return {
|
|
457
|
+
template: rule("template", deps),
|
|
458
|
+
"required-section": rule("required-section", deps),
|
|
459
|
+
"venue-leftover": rule("venue-leftover", deps),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* inside is `fixtures/toolchain-mirror/install-tl`, which lays out a tree whose `kpsewhich` and
|
|
7
7
|
* `tlmgr` answer from two text files. So what runs is paperlint's own download, fallback, unpack,
|
|
8
8
|
* install and verify logic — the part that decides success — and what is faked is only TeX.
|
|
9
|
-
* The real TeX Live half is `test/e2e/toolchain.ts`.
|
|
9
|
+
* The real TeX Live half is `test/e2e/tex/toolchain.e2e.ts`.
|
|
10
10
|
*
|
|
11
11
|
* Tables, in order:
|
|
12
12
|
* 1. the pure pieces (year, profile, tlmgr's unknown names, cache location);
|
package/src/venue-rules.test.ts
CHANGED
|
@@ -12,12 +12,12 @@ import { join } from "node:path";
|
|
|
12
12
|
import { describe, expect, it } from "vitest";
|
|
13
13
|
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
14
14
|
import { sha256Hex } from "./domain/sha256.ts";
|
|
15
|
-
import {
|
|
15
|
+
import { presetsDir } from "./package-dirs.ts";
|
|
16
16
|
import { VENUE_RULE_LEVELS, venueRules } from "./venue-rules.ts";
|
|
17
17
|
import { buildConfig, OPTIONAL_RULES, SHIPPED_RULES } from "./cli.ts";
|
|
18
18
|
import { present } from "../test/support.ts";
|
|
19
19
|
|
|
20
|
-
const VENUES =
|
|
20
|
+
const VENUES = presetsDir();
|
|
21
21
|
const PAPER = "/work/papers/p";
|
|
22
22
|
const PDF_BYTES = new TextEncoder().encode(
|
|
23
23
|
"%PDF-1.5 a stand-in for the built PDF",
|
|
@@ -213,13 +213,13 @@ describe("pdf/profile — the declaration must resolve, or nothing is judged", (
|
|
|
213
213
|
"an unknown venue (a typo would silently disable every check)",
|
|
214
214
|
{ extends: "paperlint:agentic-dev", kind: "short" },
|
|
215
215
|
"preset",
|
|
216
|
-
/agenticdev, aisec, realm/,
|
|
216
|
+
/agenticdev, aidc, aisec, ieee-conference, realm/,
|
|
217
217
|
],
|
|
218
218
|
[
|
|
219
219
|
"the base TeX set is not a venue",
|
|
220
220
|
{ extends: "paperlint:tex-base" },
|
|
221
221
|
"preset",
|
|
222
|
-
/agenticdev, aisec, realm/,
|
|
222
|
+
/agenticdev, aidc, aisec, ieee-conference, realm/,
|
|
223
223
|
],
|
|
224
224
|
[
|
|
225
225
|
"paperlint.json that is not JSON",
|
|
@@ -560,6 +560,98 @@ describe("paperlint's own config turns the venue rules on for every paper.tex",
|
|
|
560
560
|
});
|
|
561
561
|
});
|
|
562
562
|
|
|
563
|
+
/**
|
|
564
|
+
* The facts of the 13-page `[conference,compsoc]` IEEEtran paper measured for ieee-conference.jsonc
|
|
565
|
+
* (2026-09-27): banal's page, columns and sizes, pdf.js's fonts.
|
|
566
|
+
*/
|
|
567
|
+
/** Embedded Type 1 fonts of these names. */
|
|
568
|
+
const type1 = (names: readonly string[]): Font[] =>
|
|
569
|
+
names.map((name) => ({
|
|
570
|
+
name,
|
|
571
|
+
type: "Type 1",
|
|
572
|
+
embedded: true,
|
|
573
|
+
program: "Type1",
|
|
574
|
+
}));
|
|
575
|
+
|
|
576
|
+
const ieeeFacts = (patch: Patch = () => {}): Facts =>
|
|
577
|
+
withFacts((f) => {
|
|
578
|
+
f.page_w_in = 8.5;
|
|
579
|
+
f.page_h_in = 11;
|
|
580
|
+
f.columns = 2;
|
|
581
|
+
f.body_pt = 10.3;
|
|
582
|
+
f.ref_pt = 8.3;
|
|
583
|
+
f.body_pages = 12;
|
|
584
|
+
f.ref_pages = 1;
|
|
585
|
+
f.fonts = type1([
|
|
586
|
+
"NimbusRomNo9L-Medi",
|
|
587
|
+
"NimbusRomNo9L-Regu",
|
|
588
|
+
"NimbusRomNo9L-ReguItal",
|
|
589
|
+
"NimbusRomNo9L-MediItal",
|
|
590
|
+
]);
|
|
591
|
+
patch(f);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
describe("the IEEE conference family and AIDC", () => {
|
|
595
|
+
const FAMILY = { extends: "paperlint:ieee-conference" };
|
|
596
|
+
const AIDC = { extends: "paperlint:aidc", kind: "regular" };
|
|
597
|
+
|
|
598
|
+
it.each([
|
|
599
|
+
["the family", FAMILY],
|
|
600
|
+
["aidc/regular", AIDC],
|
|
601
|
+
["aidc/short", { extends: "paperlint:aidc", kind: "short" }],
|
|
602
|
+
])("the measured IEEEtran build passes %s", (_, venue) => {
|
|
603
|
+
expect(lint({ venue, facts: ieeeFacts() })).toEqual([]);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it.each([
|
|
607
|
+
["the family", FAMILY],
|
|
608
|
+
["aidc", AIDC],
|
|
609
|
+
])("the planted A4 build fails %s on both page dimensions", (_, venue) => {
|
|
610
|
+
const facts = ieeeFacts(
|
|
611
|
+
(f) => ((f.page_w_in = 8.264), (f.page_h_in = 11.694)),
|
|
612
|
+
);
|
|
613
|
+
expect(ids(lint({ venue, facts }))).toEqual([
|
|
614
|
+
"pdf/geometry:dim",
|
|
615
|
+
"pdf/geometry:dim",
|
|
616
|
+
]);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it.each([
|
|
620
|
+
["the family", FAMILY],
|
|
621
|
+
["aidc", AIDC],
|
|
622
|
+
])("the planted Computer Modern build fails %s: no Times", (_, venue) => {
|
|
623
|
+
const facts = ieeeFacts(
|
|
624
|
+
(f) => (f.fonts = type1(["CMBX12", "CMR10", "CMTI10"])),
|
|
625
|
+
);
|
|
626
|
+
const fs = lint({ venue, facts });
|
|
627
|
+
expect(ids(fs)).toEqual(["pdf/fonts:noFamily"]);
|
|
628
|
+
expect(fs[0]?.message).toMatch(/NimbusRomNo9L/);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it("a body size off the family's 10 pt fails pdf/body-size", () => {
|
|
632
|
+
expect(
|
|
633
|
+
ids(lint({ venue: AIDC, facts: ieeeFacts((f) => (f.body_pt = 9.3)) })),
|
|
634
|
+
).toEqual(["pdf/body-size:body"]);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it("🔴 AIDC's page limit is not gated: banal's body count of 13 — a correct paper with its LLM Usage Statement before the references — is not a finding", () => {
|
|
638
|
+
expect(
|
|
639
|
+
lint({
|
|
640
|
+
venue: AIDC,
|
|
641
|
+
facts: ieeeFacts((f) => ((f.body_pages = 13), (f.ref_pages = 0))),
|
|
642
|
+
}),
|
|
643
|
+
).toEqual([]);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it("an AIDC kind that does not exist names the two that do", () => {
|
|
647
|
+
const fs = lint({ venue: { extends: "paperlint:aidc", kind: "wip" } });
|
|
648
|
+
expect(ids(fs)).toContain("pdf/profile:kindUnknown");
|
|
649
|
+
expect(fs.find((f) => f.rule === "pdf/profile")?.message).toMatch(
|
|
650
|
+
/regular, short/,
|
|
651
|
+
);
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
563
655
|
describe("a project's own preset, and the facts' other spellings", () => {
|
|
564
656
|
// A standalone preset (no `extends`, so it carries a `tex` block) that sets a body size with no
|
|
565
657
|
// tolerance, a reference range, two columns — and no page size and no font families.
|
package/src/venue-rules.ts
CHANGED
|
@@ -89,7 +89,7 @@ export type Assessment =
|
|
|
89
89
|
|
|
90
90
|
export interface VenueRuleDeps {
|
|
91
91
|
readonly files: Files;
|
|
92
|
-
/** The package's
|
|
92
|
+
/** The package's presets directory (`presetsDir()`): the shipped presets and their schema. */
|
|
93
93
|
readonly venuesDir: string;
|
|
94
94
|
}
|
|
95
95
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
% Created by `paperlint new`. A stub: replace all of it. The only thing `paperlint lint` needs is that
|
|
2
|
-
% this file exists
|
|
2
|
+
% this file exists. With `--venue`, the class below is the venue's (`tex/template` checks it);
|
|
3
|
+
% without, it is yours. `paperlint build` compiles it to paper.pdf.
|
|
3
4
|
\documentclass{article}
|
|
4
5
|
\title{Working title for {{name}}}
|
|
5
6
|
\begin{document}
|
|
@@ -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.
|