paperlint 6.3.0 → 7.1.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/CONTRIBUTING.md +4 -2
- package/README.md +20 -13
- package/dist/adapters/latex/conditionals.d.ts +21 -0
- package/dist/adapters/latex/conditionals.d.ts.map +1 -0
- package/dist/adapters/latex/conditionals.js +75 -0
- package/dist/adapters/latex/conditionals.js.map +1 -0
- package/dist/adapters/latex/headings.d.ts +10 -0
- package/dist/adapters/latex/headings.d.ts.map +1 -0
- package/dist/adapters/latex/headings.js +129 -0
- package/dist/adapters/latex/headings.js.map +1 -0
- package/dist/adapters/latex/index.d.ts +1 -0
- package/dist/adapters/latex/index.d.ts.map +1 -1
- package/dist/adapters/latex/index.js +3 -0
- package/dist/adapters/latex/index.js.map +1 -1
- package/dist/adapters/latex/parse.d.ts +1 -1
- package/dist/adapters/latex/parse.d.ts.map +1 -1
- package/dist/adapters/latex/parse.js +3 -2
- package/dist/adapters/latex/parse.js.map +1 -1
- package/dist/build.d.ts +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +1 -1
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +19 -5
- package/dist/cli.js.map +1 -1
- package/dist/domain/body-pages.d.ts +29 -0
- package/dist/domain/body-pages.d.ts.map +1 -1
- package/dist/domain/body-pages.js +25 -0
- package/dist/domain/body-pages.js.map +1 -1
- package/dist/domain/heading-case.d.ts +100 -0
- package/dist/domain/heading-case.d.ts.map +1 -0
- package/dist/domain/heading-case.js +365 -0
- package/dist/domain/heading-case.js.map +1 -0
- package/dist/domain/tex-document.d.ts +32 -0
- package/dist/domain/tex-document.d.ts.map +1 -1
- package/dist/domain/tex-document.js +11 -0
- package/dist/domain/tex-document.js.map +1 -1
- package/dist/eslint-rules/latex-language.d.ts.map +1 -1
- package/dist/eslint-rules/latex-language.js +16 -3
- package/dist/eslint-rules/latex-language.js.map +1 -1
- package/dist/facts-file.d.ts +3 -3
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +3 -3
- package/dist/facts-file.js.map +1 -1
- package/dist/folder-venue-rule.js +6 -6
- package/dist/folder-venue-rule.js.map +1 -1
- package/dist/heading-case.d.ts +48 -0
- package/dist/heading-case.d.ts.map +1 -0
- package/dist/heading-case.js +191 -0
- package/dist/heading-case.js.map +1 -0
- package/dist/paper-includes.d.ts +3 -0
- package/dist/paper-includes.d.ts.map +1 -1
- package/dist/paper-includes.js +17 -2
- package/dist/paper-includes.js.map +1 -1
- package/dist/ports/latex.d.ts +7 -1
- package/dist/ports/latex.d.ts.map +1 -1
- package/dist/preset-card-rule.d.ts +100 -0
- package/dist/preset-card-rule.d.ts.map +1 -0
- package/dist/preset-card-rule.js +221 -0
- package/dist/preset-card-rule.js.map +1 -0
- package/dist/presets.d.ts +29 -5
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +104 -68
- package/dist/presets.js.map +1 -1
- package/dist/submission.d.ts +8 -0
- package/dist/submission.d.ts.map +1 -1
- package/dist/submission.js +19 -7
- package/dist/submission.js.map +1 -1
- package/dist/tex-requirements.d.ts +42 -3
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +34 -5
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +6 -3
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +29 -10
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/venue-rules.d.ts +2 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +55 -7
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +13 -1
- package/docs/e2e.md +14 -12
- package/docs/optional-rules.md +4 -3
- package/docs/rules/format/page-limit.md +19 -2
- package/docs/rules/paper/folder-venue-leftover.md +1 -1
- package/docs/rules/talk/profile.md +1 -1
- package/docs/rules/tex/heading-case.md +424 -0
- package/docs/rules/tex/template.md +25 -5
- package/docs/rules/tex/venue-leftover.md +13 -0
- package/docs/rules.md +74 -57
- package/docs/submission.md +5 -1
- package/eslint-rules/latex-language.test.mjs +12 -0
- package/eslint-rules/latex-language.ts +17 -4
- package/eslint-rules/paper-typography.test.mjs +12 -0
- package/eslint.config.mjs +59 -1
- package/fixtures/build-e2e/acmart/paper.tex +1 -1
- package/fixtures/build-e2e/fallback/expect.json +3 -3
- package/fixtures/build-e2e/msr/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/msr/expect.json +18 -0
- package/fixtures/build-e2e/msr/paper.tex +49 -0
- package/fixtures/build-e2e/msr/paperlint.json +1 -0
- package/fixtures/build-e2e/msr-appendix-after/PIPELINE-STATUS.md +3 -0
- package/fixtures/build-e2e/msr-appendix-after/expect.json +20 -0
- package/fixtures/build-e2e/msr-appendix-after/paper.tex +48 -0
- package/fixtures/build-e2e/msr-appendix-after/paperlint.json +1 -0
- package/lib/skill-checks.mjs +31 -0
- package/package.json +7 -1
- package/presets/acm-sigconf.jsonc +5 -0
- package/presets/acm-sigconf.md +43 -0
- package/presets/agenticdev.jsonc +27 -6
- package/presets/agenticdev.md +151 -0
- package/presets/aidc.jsonc +6 -0
- package/presets/aidc.md +75 -0
- package/presets/aisec.jsonc +5 -1
- package/presets/aisec.md +117 -0
- package/presets/ieee-conference.jsonc +5 -0
- package/presets/ieee-conference.md +38 -0
- package/presets/msr.jsonc +71 -0
- package/presets/msr.md +94 -0
- package/presets/realm.jsonc +7 -2
- package/presets/realm.md +166 -0
- package/presets/tex/agenticdev.tex +1 -1
- package/presets/tex-base.jsonc +6 -0
- package/presets/tex-base.md +30 -0
- package/presets/venue-profile.schema.json +65 -1
- package/skills/camera-ready/SKILL.md +8 -1
- package/skills/camera-ready/SKILL.md.spec.ts +7 -0
- package/skills/find-venue/SKILL.md +6 -5
- package/skills/find-venue/SKILL.md.spec.ts +5 -4
- package/skills/paper-pipeline/SKILL.md +5 -6
- package/skills/paper-pipeline/SKILL.md.spec.ts +4 -5
- package/skills/paper-pipeline/references/acl-venue-rules.md +1 -1
- package/skills/study-accepted-papers/SKILL.md +3 -3
- package/skills/study-accepted-papers/SKILL.md.spec.ts +2 -2
- package/skills/submission-portal/SKILL.md +2 -2
- package/skills/submission-portal/SKILL.md.spec.ts +1 -1
- package/skills/submit-paper/SKILL.md +34 -22
- package/skills/submit-paper/SKILL.md.spec.ts +33 -21
- package/skills/submit-paper/references/publishers/acm.md +1 -1
- package/src/adapters/latex/conditionals.ts +130 -0
- package/src/adapters/latex/headings.ts +163 -0
- package/src/adapters/latex/index.test.ts +65 -0
- package/src/adapters/latex/index.ts +3 -0
- package/src/adapters/latex/parse.ts +3 -2
- package/src/build.ts +2 -2
- package/src/cli.test.ts +50 -7
- package/src/cli.ts +28 -6
- package/src/domain/body-pages.test.ts +144 -0
- package/src/domain/body-pages.ts +61 -1
- package/src/domain/heading-case.test.ts +660 -0
- package/src/domain/heading-case.ts +538 -0
- package/src/domain/tex-document.ts +36 -0
- package/src/domain/venue-name.test.ts +13 -7
- package/src/facts-file.test.ts +4 -4
- package/src/facts-file.ts +6 -6
- package/src/folder-venue-rule.test.ts +10 -10
- package/src/folder-venue-rule.ts +6 -6
- package/src/heading-case.test.ts +500 -0
- package/src/heading-case.ts +275 -0
- package/src/init.test.ts +1 -1
- package/src/new-paper.test.ts +37 -6
- package/src/paper-includes.test.ts +26 -0
- package/src/paper-includes.ts +20 -2
- package/src/paper-settings-commands.test.ts +26 -11
- package/src/ports/latex.ts +7 -0
- package/src/preset-card-rule.test.ts +267 -0
- package/src/preset-card-rule.ts +384 -0
- package/src/presets.test.ts +259 -41
- package/src/presets.ts +161 -75
- package/src/rule-cases.test.ts +136 -50
- package/src/submission.test.ts +44 -4
- package/src/submission.ts +28 -7
- package/src/talk-rules.test.ts +6 -3
- package/src/tex-requirements.harness.ts +25 -11
- package/src/tex-requirements.test.ts +127 -1
- package/src/tex-requirements.ts +91 -12
- package/src/tex-venue-rules.test.ts +196 -21
- package/src/tex-venue-rules.ts +47 -16
- package/src/venue-rules.test.ts +180 -8
- package/src/venue-rules.ts +71 -10
- package/templates/paper/paper.tex +1 -1
- package/skills/submit-paper/references/venues/agenticdev.md +0 -171
- package/skills/submit-paper/references/venues/aisec.md +0 -105
- package/skills/submit-paper/references/venues/realm.md +0 -155
package/src/rule-cases.test.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { z } from "zod";
|
|
|
18
18
|
import { run, rulePlugins } from "./cli.ts";
|
|
19
19
|
import { sha256Hex } from "./domain/sha256.ts";
|
|
20
20
|
import { bibHash } from "./references.ts";
|
|
21
|
-
import { useTempDir, writeTree } from "../test/support.ts";
|
|
21
|
+
import { useTempDir, venuePreset, writeTree } from "../test/support.ts";
|
|
22
22
|
|
|
23
23
|
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
24
24
|
const fixture = (p: string): string =>
|
|
@@ -39,19 +39,87 @@ const paper = (
|
|
|
39
39
|
...extra,
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
+
// ── the venues: presets of the tree's own, never the shipped ones ──────────────────────────────
|
|
43
|
+
//
|
|
44
|
+
// A case judges a RULE, so every number it is sized against is written here, beside the facts and
|
|
45
|
+
// the bodies that meet or miss it. A shipped venue preset changes when its call for papers does;
|
|
46
|
+
// its own tests resolve and judge it (presets.test.ts, venue-rules.test.ts, tex-venue-rules.test.ts,
|
|
47
|
+
// register-bands.test.ts, heading-case.test.ts). The one exception is the leftover rules, below:
|
|
48
|
+
// they name SHIPPED venues by design, so their reporting cases need one shipped venue's name.
|
|
49
|
+
// The venues below extend a template family only for what the schema asks of a chain's root (its
|
|
50
|
+
// `tex`), and set every key a case reads themselves.
|
|
51
|
+
|
|
52
|
+
/** A short-paper venue on the ACM template: the format the facts of `goodFacts` meet. */
|
|
53
|
+
const EXAMPLECONF = venuePreset("ExampleConf", {
|
|
54
|
+
extends: "paperlint:acm-sigconf",
|
|
55
|
+
format: {
|
|
56
|
+
page_w_in: 8.5,
|
|
57
|
+
page_h_in: 11,
|
|
58
|
+
columns: 2,
|
|
59
|
+
body_pt: 9,
|
|
60
|
+
body_pt_tol: 0.5,
|
|
61
|
+
ref_pt_min: 7,
|
|
62
|
+
ref_pt_max: 9,
|
|
63
|
+
fonts_text: "LinLibertine",
|
|
64
|
+
fonts_title: "LinBiolinum",
|
|
65
|
+
kinds: { short: { body_pages_max: 5, ref_pages_max: 2 } },
|
|
66
|
+
},
|
|
67
|
+
// chicago-headline for the title and every heading (tex/heading-case).
|
|
68
|
+
rules: {
|
|
69
|
+
"tex/heading-case": [
|
|
70
|
+
"error",
|
|
71
|
+
{ title: "chicago-headline", headings: "chicago-headline" },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
const IEEE = "\\documentclass[conference,compsoc]{IEEEtran}";
|
|
76
|
+
const STATEMENT_TITLE = "LLM Usage Statement";
|
|
77
|
+
/**
|
|
78
|
+
* A double-blind venue on the IEEE template: the class options it requires, the section it requires
|
|
79
|
+
* last, and one accepted paper of 10,000 words for the register bands. Its rates (contrast frames 4,
|
|
80
|
+
* claims in bold 4, relation markers 64 per 10,000 words), widened by two Poisson standard
|
|
81
|
+
* deviations, set the bands 0–8, 0–8 and 48–80.
|
|
82
|
+
*/
|
|
83
|
+
const BLINDCONF = venuePreset("BlindConf", {
|
|
84
|
+
extends: "paperlint:ieee-conference",
|
|
85
|
+
blind: true,
|
|
86
|
+
template: IEEE,
|
|
87
|
+
required_sections: [{ title: STATEMENT_TITLE, position: "last" }],
|
|
88
|
+
register: {
|
|
89
|
+
anchors: [
|
|
90
|
+
{
|
|
91
|
+
paper: "an-accepted-paper",
|
|
92
|
+
words: 10_000,
|
|
93
|
+
contrast_frames: 4,
|
|
94
|
+
claim_emphasis: 4,
|
|
95
|
+
relation_markers: 64,
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
/** The venue presets every built paper carries beside it, extended or not. */
|
|
101
|
+
const VENUES = {
|
|
102
|
+
[`${P}/exampleconf.jsonc`]: JSON.stringify(EXAMPLECONF),
|
|
103
|
+
[`${P}/blindconf.jsonc`]: JSON.stringify(BLINDCONF),
|
|
104
|
+
};
|
|
105
|
+
/** A paper folder on BlindConf: the preset beside its `paperlint.json`. */
|
|
106
|
+
const onBlindConf = (dir: string): Record<string, string> => ({
|
|
107
|
+
[`${dir}/blindconf.jsonc`]: JSON.stringify(BLINDCONF),
|
|
108
|
+
[`${dir}/paperlint.json`]: JSON.stringify({ extends: "./blindconf.jsonc" }),
|
|
109
|
+
});
|
|
110
|
+
|
|
42
111
|
// ── the venue rules' inputs: a PDF and the facts `paperlint build` would write about it ─────────
|
|
43
112
|
|
|
44
113
|
const PDF = "%PDF-1.5 a stand-in for the built PDF";
|
|
45
|
-
const
|
|
114
|
+
const EXAMPLE = { extends: "./exampleconf.jsonc", kind: "short" };
|
|
46
115
|
/** A double-blind venue's paper that declares who wrote it (`anonymity/*`). */
|
|
47
116
|
const BLIND = {
|
|
48
|
-
extends: "
|
|
49
|
-
kind: "regular",
|
|
117
|
+
extends: "./blindconf.jsonc",
|
|
50
118
|
identity: ["Ada Example", "adaexample"],
|
|
51
119
|
};
|
|
52
120
|
const ACM_TEX = tex("Text.", "\\documentclass[sigconf]{acmart}");
|
|
53
121
|
|
|
54
|
-
/** The facts of a short
|
|
122
|
+
/** The facts of a short ExampleConf paper that meets every number in its preset. */
|
|
55
123
|
const goodFacts = (): Record<string, unknown> => ({
|
|
56
124
|
schema: 3,
|
|
57
125
|
pages_text: [],
|
|
@@ -61,7 +129,7 @@ const goodFacts = (): Record<string, unknown> => ({
|
|
|
61
129
|
appendix_anchor_page: null,
|
|
62
130
|
pdf: "paper.pdf",
|
|
63
131
|
pdf_sha256: sha256Hex(new TextEncoder().encode(PDF)),
|
|
64
|
-
venue: "
|
|
132
|
+
venue: "exampleconf",
|
|
65
133
|
kind: "short",
|
|
66
134
|
npages: 7,
|
|
67
135
|
fonts_source: "pdfjs-drawn",
|
|
@@ -85,12 +153,13 @@ const goodFacts = (): Record<string, unknown> => ({
|
|
|
85
153
|
pages_by_type: { body: 5, bib: 2 },
|
|
86
154
|
});
|
|
87
155
|
|
|
88
|
-
/** An
|
|
156
|
+
/** An ExampleConf paper, built: its settings, the PDF, and facts with `patch` applied. */
|
|
89
157
|
const built = (
|
|
90
158
|
patch: Record<string, unknown> = {},
|
|
91
|
-
settings: Record<string, unknown> =
|
|
159
|
+
settings: Record<string, unknown> = EXAMPLE,
|
|
92
160
|
): Record<string, string> =>
|
|
93
161
|
paper(ACM_TEX, {
|
|
162
|
+
...VENUES,
|
|
94
163
|
[`${P}/paperlint.json`]: JSON.stringify(settings),
|
|
95
164
|
[`${P}/paper.pdf`]: PDF,
|
|
96
165
|
[`${P}/_build/paper.facts.json`]: JSON.stringify({
|
|
@@ -129,23 +198,26 @@ const checked = (exists: string, authors: string) => ({
|
|
|
129
198
|
entries: [{ key: "a", exists, authors }],
|
|
130
199
|
});
|
|
131
200
|
|
|
132
|
-
// ──
|
|
201
|
+
// ── a BlindConf paper, for the LaTeX venue rules ──────────────────────────────────────────────
|
|
133
202
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
203
|
+
const BLIND_PAPER = onBlindConf(P);
|
|
204
|
+
const ACM_CLASS = "\\documentclass[sigconf]{acmart}";
|
|
205
|
+
/** A paper on ExampleConf's preset: it turns `tex/heading-case` on, chicago-headline for the title and every heading. */
|
|
206
|
+
const ACM_PRESET = {
|
|
207
|
+
...VENUES,
|
|
208
|
+
[`${P}/paperlint.json`]: JSON.stringify(EXAMPLE),
|
|
139
209
|
};
|
|
140
|
-
const
|
|
141
|
-
const STATEMENT = "\\section*{LLM Usage Statement}\nNone.";
|
|
210
|
+
const STATEMENT = `\\section*{${STATEMENT_TITLE}}\nNone.`;
|
|
142
211
|
/** Two `\\documentclass` lines behind a switch: `article` for one venue, `second` for this one. */
|
|
143
212
|
const switched = (second: string): string =>
|
|
144
213
|
`\\def\\venue{2}\n\\if\\venue1\n\\documentclass{article}\n\\fi\n\\if\\venue2\n${second}\n\\fi`;
|
|
145
|
-
const
|
|
146
|
-
paper(
|
|
214
|
+
const blind = (body: string, cls = IEEE) =>
|
|
215
|
+
paper(
|
|
216
|
+
tex(`\\section{Introduction}\n${body}\n${STATEMENT}`, cls),
|
|
217
|
+
BLIND_PAPER,
|
|
218
|
+
);
|
|
147
219
|
|
|
148
|
-
// ── a paper that declares a talk: a preset of its own over
|
|
220
|
+
// ── a paper that declares a talk: a preset of its own over ExampleConf, and synthetic media ──────
|
|
149
221
|
|
|
150
222
|
const media = (name: string): Uint8Array =>
|
|
151
223
|
readFileSync(join(ROOT, "fixtures", "talk", name));
|
|
@@ -173,10 +245,13 @@ const talkPaper = (
|
|
|
173
245
|
};
|
|
174
246
|
return {
|
|
175
247
|
...paper(ACM_TEX, {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
talk
|
|
179
|
-
|
|
248
|
+
...VENUES,
|
|
249
|
+
[`${P}/talk-venue.jsonc`]: JSON.stringify(
|
|
250
|
+
venuePreset("talk-venue", {
|
|
251
|
+
extends: "./exampleconf.jsonc",
|
|
252
|
+
talk: { ...TALK_VENUE, ...venue },
|
|
253
|
+
}),
|
|
254
|
+
),
|
|
180
255
|
[`${P}/paperlint.json`]: JSON.stringify({
|
|
181
256
|
extends: "./talk-venue.jsonc",
|
|
182
257
|
kind: "short",
|
|
@@ -226,7 +301,7 @@ const FORMAL = Array.from(
|
|
|
226
301
|
/** FORMAL with `n` more sentences `s`: a body whose rate of one register measure is set. */
|
|
227
302
|
const formalWith = (n: number, s: string): string =>
|
|
228
303
|
`${FORMAL} ${Array.from({ length: n }, () => s).join(" ")}`;
|
|
229
|
-
/** FORMAL with
|
|
304
|
+
/** FORMAL with 20 relation markers, 68.7 per 10,000 words: inside BlindConf's band (48–80). */
|
|
230
305
|
const LINKED = formalWith(
|
|
231
306
|
10,
|
|
232
307
|
"Therefore, the rule counts the sentence because the body holds it.",
|
|
@@ -373,49 +448,60 @@ const CASES: Readonly<Record<string, RuleCases>> = {
|
|
|
373
448
|
// The class picked behind a TeX switch: two candidates, one of them the venue's or none.
|
|
374
449
|
"tex/template": {
|
|
375
450
|
reports: {
|
|
376
|
-
tree:
|
|
451
|
+
tree: blind("Text.", switched("\\documentclass[conference]{IEEEtran}")),
|
|
377
452
|
file: TEX_FILE,
|
|
378
453
|
severity: 2,
|
|
379
454
|
line: 3,
|
|
380
455
|
},
|
|
381
|
-
silent:
|
|
456
|
+
silent: blind("Text.", switched(IEEE)),
|
|
382
457
|
},
|
|
383
458
|
"tex/required-section": {
|
|
384
459
|
reports: {
|
|
385
|
-
tree: paper(tex("\\section{Introduction}\nText.", IEEE),
|
|
460
|
+
tree: paper(tex("\\section{Introduction}\nText.", IEEE), BLIND_PAPER),
|
|
386
461
|
file: TEX_FILE,
|
|
387
462
|
severity: 2,
|
|
388
463
|
line: 5,
|
|
389
464
|
},
|
|
390
|
-
silent:
|
|
465
|
+
silent: blind("Text."),
|
|
466
|
+
},
|
|
467
|
+
"tex/heading-case": {
|
|
468
|
+
reports: {
|
|
469
|
+
tree: paper(tex("\\section{Related work}", ACM_CLASS), ACM_PRESET),
|
|
470
|
+
file: TEX_FILE,
|
|
471
|
+
severity: 2,
|
|
472
|
+
line: 3,
|
|
473
|
+
},
|
|
474
|
+
silent: paper(tex("\\section{Related Work}", ACM_CLASS), ACM_PRESET),
|
|
391
475
|
},
|
|
392
476
|
"paper/folder-venue-leftover": {
|
|
393
477
|
reports: {
|
|
394
|
-
//
|
|
478
|
+
// A BlindConf paper in a folder named for EMNLP. The rule knows only the SHIPPED venues'
|
|
479
|
+
// names (REALM's preset calls it by its conference, EMNLP), so the other venue is one of them.
|
|
395
480
|
tree: {
|
|
396
|
-
"papers/
|
|
397
|
-
"papers/
|
|
398
|
-
"papers/
|
|
481
|
+
"papers/emnlp-2026/PIPELINE-STATUS.md": STATUS,
|
|
482
|
+
"papers/emnlp-2026/paper.tex": tex("x"),
|
|
483
|
+
...onBlindConf("papers/emnlp-2026"),
|
|
399
484
|
},
|
|
400
|
-
file: "papers/
|
|
485
|
+
file: "papers/emnlp-2026/PIPELINE-STATUS.md",
|
|
401
486
|
severity: 1,
|
|
402
487
|
line: 1,
|
|
403
488
|
},
|
|
404
489
|
// The folder names the paper's own venue.
|
|
405
490
|
silent: {
|
|
406
|
-
"papers/
|
|
407
|
-
"papers/
|
|
408
|
-
"papers/
|
|
491
|
+
"papers/blindconf-2026/PIPELINE-STATUS.md": STATUS,
|
|
492
|
+
"papers/blindconf-2026/paper.tex": tex("x"),
|
|
493
|
+
...onBlindConf("papers/blindconf-2026"),
|
|
409
494
|
},
|
|
410
495
|
},
|
|
496
|
+
// EMNLP, a shipped venue's name (REALM's conference), in a BlindConf paper's text.
|
|
411
497
|
"tex/venue-leftover": {
|
|
412
498
|
reports: {
|
|
413
|
-
tree:
|
|
499
|
+
tree: blind("First written for EMNLP."),
|
|
414
500
|
file: TEX_FILE,
|
|
415
501
|
severity: 1,
|
|
416
502
|
line: 4,
|
|
417
503
|
},
|
|
418
|
-
silent:
|
|
504
|
+
silent: blind("Text."),
|
|
419
505
|
},
|
|
420
506
|
"tex/claim-provenance": {
|
|
421
507
|
reports: {
|
|
@@ -439,31 +525,31 @@ const CASES: Readonly<Record<string, RuleCases>> = {
|
|
|
439
525
|
},
|
|
440
526
|
silent: paper(tex(FORMAL)),
|
|
441
527
|
},
|
|
442
|
-
// The three register bands judge a body against the
|
|
443
|
-
// line 3, right after the \\section, where the finding about the whole body stands.
|
|
528
|
+
// The three register bands judge a body against the anchor of BlindConf's preset; the body opens
|
|
529
|
+
// on line 3, right after the \\section, where the finding about the whole body stands.
|
|
444
530
|
"tex/contrast-frames": {
|
|
445
531
|
reports: {
|
|
446
|
-
tree:
|
|
532
|
+
tree: blind(formalWith(5, "It reads the word, not the operation.")),
|
|
447
533
|
file: TEX_FILE,
|
|
448
534
|
severity: 1,
|
|
449
535
|
line: 3,
|
|
450
536
|
},
|
|
451
|
-
silent:
|
|
537
|
+
silent: blind(LINKED),
|
|
452
538
|
},
|
|
453
539
|
"tex/claim-emphasis": {
|
|
454
540
|
reports: {
|
|
455
|
-
tree:
|
|
541
|
+
tree: blind(
|
|
456
542
|
formalWith(6, "We find that \\textbf{the rule counts 46 sentences}."),
|
|
457
543
|
),
|
|
458
544
|
file: TEX_FILE,
|
|
459
545
|
severity: 1,
|
|
460
546
|
line: 3,
|
|
461
547
|
},
|
|
462
|
-
silent:
|
|
548
|
+
silent: blind(LINKED),
|
|
463
549
|
},
|
|
464
550
|
"tex/relation-markers": {
|
|
465
|
-
reports: { tree:
|
|
466
|
-
silent:
|
|
551
|
+
reports: { tree: blind(FORMAL), file: TEX_FILE, severity: 1, line: 3 },
|
|
552
|
+
silent: blind(LINKED),
|
|
467
553
|
},
|
|
468
554
|
"bib/reachable-entry": {
|
|
469
555
|
reports: {
|
|
@@ -518,23 +604,23 @@ const CASES: Readonly<Record<string, RuleCases>> = {
|
|
|
518
604
|
},
|
|
519
605
|
"format/layout-override": {
|
|
520
606
|
reports: {
|
|
521
|
-
tree:
|
|
607
|
+
tree: blind("\\linespread{0.9}Text."),
|
|
522
608
|
file: TEX_FILE,
|
|
523
609
|
severity: 2,
|
|
524
610
|
line: 4,
|
|
525
611
|
},
|
|
526
|
-
silent:
|
|
612
|
+
silent: blind("\\vspace{-2mm}Text."),
|
|
527
613
|
},
|
|
528
614
|
"pdf/last-page-balance": {
|
|
529
615
|
reports: onPdf(
|
|
530
616
|
built(
|
|
531
617
|
{ last_page: { kind: "measured", columns_pt: [621.5, 264.8] } },
|
|
532
|
-
{ ...
|
|
618
|
+
{ ...EXAMPLE, rules: { "pdf/last-page-balance": "error" } },
|
|
533
619
|
),
|
|
534
620
|
),
|
|
535
621
|
silent: built(
|
|
536
622
|
{},
|
|
537
|
-
{ ...
|
|
623
|
+
{ ...EXAMPLE, rules: { "pdf/last-page-balance": "error" } },
|
|
538
624
|
),
|
|
539
625
|
},
|
|
540
626
|
"talk/profile": {
|
package/src/submission.test.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
usageProblem,
|
|
28
28
|
type SubmissionArgs,
|
|
29
29
|
} from "./submission.ts";
|
|
30
|
+
import { venuePreset } from "../test/support.ts";
|
|
30
31
|
|
|
31
32
|
const PAPER = "/w/papers/p";
|
|
32
33
|
const SITE = "http://127.0.0.1:9";
|
|
@@ -41,7 +42,7 @@ const withPresets = (fs: Files): Files => ({
|
|
|
41
42
|
});
|
|
42
43
|
|
|
43
44
|
const venue = (portal: unknown): string =>
|
|
44
|
-
JSON.stringify({ extends: "paperlint:aidc", portal });
|
|
45
|
+
JSON.stringify(venuePreset("venue", { extends: "paperlint:aidc", portal }));
|
|
45
46
|
const files = (o: {
|
|
46
47
|
readonly portal?: unknown;
|
|
47
48
|
readonly paper?: Readonly<Record<string, unknown>>;
|
|
@@ -116,6 +117,10 @@ const fakePortal = (answers: {
|
|
|
116
117
|
return { seen, portalFor };
|
|
117
118
|
};
|
|
118
119
|
|
|
120
|
+
/** The line `update` prints before it sends `pdf`, naming `call`. */
|
|
121
|
+
const CHECK = (pdf: string, call: string): string =>
|
|
122
|
+
`before you rely on it, open ${pdf} yourself and check it against ${call}: the page count and what counts toward the limit, the template and its class options, anonymity, and that every figure, table and reference renders — paperlint passing is not the venue accepting the format`;
|
|
123
|
+
|
|
119
124
|
const ARGS: SubmissionArgs = {
|
|
120
125
|
sub: "show",
|
|
121
126
|
paperDir: "papers/p",
|
|
@@ -286,6 +291,7 @@ test("update: a dry run by default — the paper's PDF sent, nothing saved, exit
|
|
|
286
291
|
],
|
|
287
292
|
]);
|
|
288
293
|
assert.deepEqual(r.out, [
|
|
294
|
+
CHECK("papers/p/paper.pdf", "the call for papers, https://example.org/cfp"),
|
|
289
295
|
"dry run — the portal checked the change and kept nothing; --save sends it for real",
|
|
290
296
|
"HTTP 200",
|
|
291
297
|
"valid yes",
|
|
@@ -321,7 +327,41 @@ test("update --save --submit --abstract --pdf: all of it sent, and said to be sa
|
|
|
321
327
|
{ save: true },
|
|
322
328
|
],
|
|
323
329
|
]);
|
|
324
|
-
|
|
330
|
+
// Guards: the PDF that is sent is the one named, and --save is told to check it too.
|
|
331
|
+
assert.deepEqual(r.out.slice(0, 2), [
|
|
332
|
+
CHECK("other.pdf", "the call for papers, https://example.org/cfp"),
|
|
333
|
+
"saved",
|
|
334
|
+
]);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("🔴 update: the check line names the venue's call from the preset's `url`; a preset that names no venue gets the generic line", async () => {
|
|
338
|
+
const family = memoryFiles({
|
|
339
|
+
"/w/package.json": "{}",
|
|
340
|
+
"/w/house.jsonc": JSON.stringify({
|
|
341
|
+
type: "family",
|
|
342
|
+
extends: "paperlint:ieee-conference",
|
|
343
|
+
portal: { kind: "hotcrp", url: `${SITE}/` },
|
|
344
|
+
}),
|
|
345
|
+
[`${PAPER}/paperlint.json`]: JSON.stringify({
|
|
346
|
+
extends: "../../house.jsonc",
|
|
347
|
+
submission: { id: 7 },
|
|
348
|
+
}),
|
|
349
|
+
[`${PAPER}/paper.pdf`]: PDF,
|
|
350
|
+
});
|
|
351
|
+
const r = await run({ sub: "update" }, { files: family });
|
|
352
|
+
assert.equal(r.code, 0);
|
|
353
|
+
assert.equal(
|
|
354
|
+
r.out[0],
|
|
355
|
+
CHECK("papers/p/paper.pdf", "the venue's call for papers"),
|
|
356
|
+
);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("show sends nothing, so it asks for no check", async () => {
|
|
360
|
+
const r = await run({});
|
|
361
|
+
assert.equal(
|
|
362
|
+
r.out.some((l) => l.includes("call for papers")),
|
|
363
|
+
false,
|
|
364
|
+
);
|
|
325
365
|
});
|
|
326
366
|
|
|
327
367
|
test("update: valid:false, or a non-2xx status, exits 1 with the portal's messages", async () => {
|
|
@@ -337,7 +377,7 @@ test("update: valid:false, or a non-2xx status, exits 1 with the portal's messag
|
|
|
337
377
|
{ portal: fakePortal({ update: invalid }) },
|
|
338
378
|
);
|
|
339
379
|
assert.equal(r1.code, 1);
|
|
340
|
-
assert.deepEqual(r1.out.slice(
|
|
380
|
+
assert.deepEqual(r1.out.slice(3), [
|
|
341
381
|
"valid NO",
|
|
342
382
|
"changes —",
|
|
343
383
|
`local PDF papers/p/paper.pdf sha2-${HEX}`,
|
|
@@ -379,7 +419,7 @@ test("update --save that the portal withheld says so", async () => {
|
|
|
379
419
|
{ sub: "update", save: true },
|
|
380
420
|
{ portal: fakePortal({ update: withheld }) },
|
|
381
421
|
);
|
|
382
|
-
assert.equal(r.out[
|
|
422
|
+
assert.equal(r.out[1], "SAVE WITHHELD by the portal — nothing changed");
|
|
383
423
|
});
|
|
384
424
|
|
|
385
425
|
test("update: no PDF to send, or no abstract file, is refused before the portal is asked", async () => {
|
package/src/submission.ts
CHANGED
|
@@ -80,6 +80,8 @@ export interface SubmissionTarget {
|
|
|
80
80
|
readonly id: number;
|
|
81
81
|
/** The paper's built PDF, absolute. */
|
|
82
82
|
readonly pdf: string;
|
|
83
|
+
/** The venue's call for papers (the preset's `url`); null when the preset names no venue. */
|
|
84
|
+
readonly call: string | null;
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
const configOf = (dir: string): string => join(dir, CONFIG_FILE);
|
|
@@ -109,7 +111,9 @@ export function submissionTarget(
|
|
|
109
111
|
const portal = supportedPortal(declared);
|
|
110
112
|
if (!portal.ok) return portal;
|
|
111
113
|
const pdf = join(paperDir, p.settings.pdf ?? "paper.pdf");
|
|
112
|
-
|
|
114
|
+
const identity = p.preset.identity;
|
|
115
|
+
const call = identity.type === "venue" ? identity.url : null;
|
|
116
|
+
return ok({ portal: portal.value, id: sub.id, pdf, call });
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
/** The token from the environment, or the line naming the variable and where to get one. */
|
|
@@ -182,6 +186,14 @@ export function showLines(
|
|
|
182
186
|
];
|
|
183
187
|
}
|
|
184
188
|
|
|
189
|
+
/**
|
|
190
|
+
* What `update` says before it sends a PDF, dry run or not: the author opens that file and checks it
|
|
191
|
+
* against the venue's call. paperlint's checks are an aid; the venue's reading of the PDF is what
|
|
192
|
+
* counts. Pure.
|
|
193
|
+
*/
|
|
194
|
+
export const checkPdfLine = (shown: string, call: string | null): string =>
|
|
195
|
+
`before you rely on it, open ${shown} yourself and check it against ${call === null ? "the venue's call for papers" : `the call for papers, ${call}`}: the page count and what counts toward the limit, the template and its class options, anonymity, and that every figure, table and reference renders — paperlint passing is not the venue accepting the format`;
|
|
196
|
+
|
|
185
197
|
/** `update`'s report. Pure. */
|
|
186
198
|
export function updateLines(
|
|
187
199
|
o: UpdateOutcome,
|
|
@@ -243,6 +255,17 @@ function abstractOf(
|
|
|
243
255
|
: ok(new TextDecoder().decode(bytes).trim());
|
|
244
256
|
}
|
|
245
257
|
|
|
258
|
+
/** What `update` sends: the PDF's bytes, the abstract's text, and whether to mark it submitted. */
|
|
259
|
+
const changeOf = (
|
|
260
|
+
a: SubmissionArgs & { readonly pdfPath: string },
|
|
261
|
+
bytes: Uint8Array,
|
|
262
|
+
abstract: string | null,
|
|
263
|
+
) => ({
|
|
264
|
+
pdf: { name: basename(a.pdfPath), bytes },
|
|
265
|
+
abstract,
|
|
266
|
+
submit: a.submit,
|
|
267
|
+
});
|
|
268
|
+
|
|
246
269
|
/** `update`: send the PDF (and the abstract, and "submitted" when asked); a dry run unless saving. */
|
|
247
270
|
async function runUpdate(
|
|
248
271
|
t: SubmissionTarget,
|
|
@@ -259,12 +282,10 @@ async function runUpdate(
|
|
|
259
282
|
);
|
|
260
283
|
const abstract = abstractOf(a, deps);
|
|
261
284
|
if (!abstract.ok) return (deps.err(abstract.error), 2);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
};
|
|
267
|
-
const r = await portal.update(t.id, change, { save: a.save });
|
|
285
|
+
deps.log(checkPdfLine(shown, t.call));
|
|
286
|
+
const r = await portal.update(t.id, changeOf(a, bytes, abstract.value), {
|
|
287
|
+
save: a.save,
|
|
288
|
+
});
|
|
268
289
|
if (!r.ok) return (deps.err(failureText(r.error)), 1);
|
|
269
290
|
const local = { shown, sha256: sha256Hex(bytes) };
|
|
270
291
|
updateLines(r.value, local, a.save).forEach((l) => {
|
package/src/talk-rules.test.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
talkRules,
|
|
24
24
|
type TalkRuleName,
|
|
25
25
|
} from "./talk-rules.ts";
|
|
26
|
-
import { useTempDir, writeTree } from "../test/support.ts";
|
|
26
|
+
import { useTempDir, venuePreset, writeTree } from "../test/support.ts";
|
|
27
27
|
|
|
28
28
|
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
29
29
|
const media = (name: string): Uint8Array =>
|
|
@@ -62,8 +62,11 @@ function run(
|
|
|
62
62
|
const dir = useTempDir("paperlint-talk-rules-");
|
|
63
63
|
const venue =
|
|
64
64
|
o.venue === null
|
|
65
|
-
? { extends: "paperlint:acm-sigconf" }
|
|
66
|
-
:
|
|
65
|
+
? { type: "family", extends: "paperlint:acm-sigconf" }
|
|
66
|
+
: venuePreset("venue", {
|
|
67
|
+
extends: "paperlint:agenticdev",
|
|
68
|
+
talk: { ...VENUE, ...o.venue },
|
|
69
|
+
});
|
|
67
70
|
writeTree(dir, {
|
|
68
71
|
"package.json": "{}",
|
|
69
72
|
"p/paper.tex": "x",
|
|
@@ -130,32 +130,40 @@ copyFileSync(join(VENUES, R.SCHEMA_FILE), join(tmp, R.SCHEMA_FILE));
|
|
|
130
130
|
const bad = (text: string) =>
|
|
131
131
|
throws(() => R.parseProfile(text, "bad.jsonc", tmp));
|
|
132
132
|
const BAD: [string, string, string][] = [
|
|
133
|
-
[
|
|
133
|
+
[
|
|
134
|
+
"no `tex` block at all",
|
|
135
|
+
`{ "type": "family", "template": "acmart" }`,
|
|
136
|
+
"tex",
|
|
137
|
+
],
|
|
134
138
|
// Guards: typo detection — `templat` would be accepted and the field silently unread.
|
|
135
139
|
[
|
|
136
140
|
"an unknown top-level key (a typo)",
|
|
137
|
-
`{ "tex": { "packages": { "a": ["a.sty"] } }, "templat": "x" }`,
|
|
141
|
+
`{ "type": "family", "tex": { "packages": { "a": ["a.sty"] } }, "templat": "x" }`,
|
|
138
142
|
"additional properties",
|
|
139
143
|
],
|
|
140
144
|
[
|
|
141
145
|
"a package with NO proof file",
|
|
142
|
-
`{ "tex": { "packages": { "acmart": [] } } }`,
|
|
146
|
+
`{ "type": "family", "tex": { "packages": { "acmart": [] } } }`,
|
|
143
147
|
".tex.packages['acmart']",
|
|
144
148
|
],
|
|
145
149
|
[
|
|
146
150
|
"a package name tlmgr would not know (upper case)",
|
|
147
|
-
`{ "tex": { "packages": { "Acmart": ["acmart.cls"] } } }`,
|
|
151
|
+
`{ "type": "family", "tex": { "packages": { "Acmart": ["acmart.cls"] } } }`,
|
|
148
152
|
"property name",
|
|
149
153
|
],
|
|
150
154
|
[
|
|
151
155
|
"a proof that is a path, not a file name",
|
|
152
|
-
`{ "tex": { "packages": { "a": ["tex/a.sty"] } } }`,
|
|
156
|
+
`{ "type": "family", "tex": { "packages": { "a": ["tex/a.sty"] } } }`,
|
|
153
157
|
"pattern",
|
|
154
158
|
],
|
|
155
|
-
[
|
|
159
|
+
[
|
|
160
|
+
"an empty packages map",
|
|
161
|
+
`{ "type": "family", "tex": { "packages": {} } }`,
|
|
162
|
+
"fewer than 1",
|
|
163
|
+
],
|
|
156
164
|
[
|
|
157
165
|
"an unknown key inside tex",
|
|
158
|
-
`{ "tex": { "packages": { "a": ["a.sty"] }, "pkgs": {} } }`,
|
|
166
|
+
`{ "type": "family", "tex": { "packages": { "a": ["a.sty"] }, "pkgs": {} } }`,
|
|
159
167
|
"additional properties",
|
|
160
168
|
],
|
|
161
169
|
];
|
|
@@ -174,7 +182,9 @@ check(
|
|
|
174
182
|
);
|
|
175
183
|
check(
|
|
176
184
|
"comments and trailing commas ARE JSONC",
|
|
177
|
-
bad(
|
|
185
|
+
bad(
|
|
186
|
+
`// quote\n{ "type": "family", "tex": { "packages": { "a": ["a.sty"], }, }, }`,
|
|
187
|
+
) === "",
|
|
178
188
|
);
|
|
179
189
|
rmSync(tmp, { recursive: true, force: true });
|
|
180
190
|
|
|
@@ -239,7 +249,7 @@ rmSync(tmp, { recursive: true, force: true });
|
|
|
239
249
|
// ── 5. the parser's tolerances: an optional block left out, and Ajv's optional fields ───
|
|
240
250
|
{
|
|
241
251
|
const bare = R.parsePreset(
|
|
242
|
-
'{ "extends": "paperlint:aisec" }',
|
|
252
|
+
'{ "type": "venue", "name": "X", "url": "https://x.org/", "extends": "paperlint:aisec" }',
|
|
243
253
|
"bare.jsonc",
|
|
244
254
|
VENUES,
|
|
245
255
|
);
|
|
@@ -254,9 +264,13 @@ rmSync(tmp, { recursive: true, force: true });
|
|
|
254
264
|
check(
|
|
255
265
|
"a tex block without `packages` is refused by the schema, before anything reads it",
|
|
256
266
|
/tex\.jsonc: \.tex should have required property 'packages'/.test(
|
|
257
|
-
throws(() =>
|
|
267
|
+
throws(() =>
|
|
268
|
+
R.parsePreset('{ "type": "family", "tex": {} }', "tex.jsonc", VENUES),
|
|
269
|
+
),
|
|
270
|
+
),
|
|
271
|
+
throws(() =>
|
|
272
|
+
R.parsePreset('{ "type": "family", "tex": {} }', "tex.jsonc", VENUES),
|
|
258
273
|
),
|
|
259
|
-
throws(() => R.parsePreset('{ "tex": {} }', "tex.jsonc", VENUES)),
|
|
260
274
|
);
|
|
261
275
|
check(
|
|
262
276
|
"violations: Ajv's null `errors` is no violation; a message Ajv left out is an empty one",
|