paperlint 6.3.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +4 -2
- package/README.md +2 -2
- 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 +17 -3
- package/dist/cli.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/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 +19 -2
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +97 -62
- package/dist/presets.js.map +1 -1
- package/dist/tex-requirements.d.ts +33 -3
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +27 -2
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +1 -1
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js.map +1 -1
- package/docs/configuration.md +13 -1
- package/docs/optional-rules.md +4 -3
- package/docs/rules/format/page-limit.md +1 -1
- 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.md +62 -53
- 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/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/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 +40 -1
- 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/submit-paper/SKILL.md +26 -22
- package/skills/submit-paper/SKILL.md.spec.ts +25 -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 +21 -4
- 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 +10 -6
- package/src/paper-settings-commands.test.ts +25 -10
- package/src/ports/latex.ts +7 -0
- package/src/preset-card-rule.test.ts +266 -0
- package/src/preset-card-rule.ts +384 -0
- package/src/presets.test.ts +193 -40
- package/src/presets.ts +139 -70
- package/src/rule-cases.test.ts +136 -50
- package/src/submission.test.ts +2 -1
- 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 +70 -8
- package/src/tex-venue-rules.test.ts +35 -21
- package/src/tex-venue-rules.ts +2 -1
- package/src/venue-rules.test.ts +7 -6
- 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>>;
|
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",
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { test } from "vitest";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
identityHint,
|
|
5
|
+
identityHints,
|
|
6
|
+
loadTypescript,
|
|
7
|
+
parsePreset,
|
|
8
|
+
} from "./tex-requirements.ts";
|
|
4
9
|
|
|
5
10
|
test("loadTypescript refuses a module without the two functions a profile is read with", () => {
|
|
6
11
|
assert.throws(
|
|
@@ -19,3 +24,124 @@ test("loadTypescript hands back what `require` loaded when it has them", () => {
|
|
|
19
24
|
ts,
|
|
20
25
|
);
|
|
21
26
|
});
|
|
27
|
+
|
|
28
|
+
// ── what a preset is: the identity its `type` declares ─────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/** What `parsePreset` throws for `preset`, as text; "" when it parses. */
|
|
31
|
+
function refusalOf(name: string, preset: object): string {
|
|
32
|
+
try {
|
|
33
|
+
parsePreset(JSON.stringify(preset), name);
|
|
34
|
+
return "";
|
|
35
|
+
} catch (e) {
|
|
36
|
+
return e instanceof Error ? e.message : String(e);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test("a venue without the link to its call for papers is refused at load, saying what to add", () => {
|
|
41
|
+
// Guards: the wrong state the identity exists to make unrepresentable — a venue nobody can check.
|
|
42
|
+
assert.equal(
|
|
43
|
+
refusalOf("v.jsonc", {
|
|
44
|
+
type: "venue",
|
|
45
|
+
name: "X",
|
|
46
|
+
extends: "paperlint:acm-sigconf",
|
|
47
|
+
}),
|
|
48
|
+
[
|
|
49
|
+
"v.jsonc does not match venue-profile.schema.json:",
|
|
50
|
+
' a venue preset names its venue and links its call for papers: "name": "<the venue, as its call spells it>", "url": "https://<the call for papers>"',
|
|
51
|
+
" v.jsonc: (top level) should have required property 'url'",
|
|
52
|
+
' v.jsonc: (top level) should match "then" schema',
|
|
53
|
+
].join("\n"),
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("a template family with a venue's name and url is refused, naming each key", () => {
|
|
58
|
+
assert.equal(
|
|
59
|
+
refusalOf("f.jsonc", {
|
|
60
|
+
type: "family",
|
|
61
|
+
name: "ACM",
|
|
62
|
+
url: "https://acm.org/",
|
|
63
|
+
extends: "paperlint:acm-sigconf",
|
|
64
|
+
}),
|
|
65
|
+
[
|
|
66
|
+
"f.jsonc does not match venue-profile.schema.json:",
|
|
67
|
+
' a template family names no venue: drop "name" — "name" and "url" belong to the venue presets that extend it',
|
|
68
|
+
' a template family names no venue: drop "url" — "name" and "url" belong to the venue presets that extend it',
|
|
69
|
+
" f.jsonc: .name boolean schema is false",
|
|
70
|
+
" f.jsonc: .url boolean schema is false",
|
|
71
|
+
' f.jsonc: (top level) should match "then" schema',
|
|
72
|
+
].join("\n"),
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("a preset that does not say what it is is refused, with the three answers", () => {
|
|
77
|
+
assert.equal(
|
|
78
|
+
refusalOf("n.jsonc", { extends: "paperlint:acm-sigconf" }),
|
|
79
|
+
[
|
|
80
|
+
"n.jsonc does not match venue-profile.schema.json:",
|
|
81
|
+
' say what this preset is: "type": "venue" (one venue\'s call for papers), "family" (a publisher\'s template that venues extend) or "base" (the TeX base set)',
|
|
82
|
+
" n.jsonc: (top level) should have required property 'type'",
|
|
83
|
+
].join("\n"),
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("the TeX base set holds its packages and nothing else", () => {
|
|
88
|
+
assert.equal(
|
|
89
|
+
refusalOf("b.jsonc", {
|
|
90
|
+
type: "base",
|
|
91
|
+
tex: { packages: { a: ["a.sty"] } },
|
|
92
|
+
format: { columns: 2 },
|
|
93
|
+
}),
|
|
94
|
+
[
|
|
95
|
+
"b.jsonc does not match venue-profile.schema.json:",
|
|
96
|
+
' the TeX base set holds "type" and "tex" only — a venue\'s or a family\'s settings go in its own preset',
|
|
97
|
+
" b.jsonc: (top level) should NOT have more than 2 properties",
|
|
98
|
+
' b.jsonc: (top level) should match "then" schema',
|
|
99
|
+
].join("\n"),
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("a venue is parsed into its identity; a family and the base set carry none of a venue's fields", () => {
|
|
104
|
+
const read = (p: object) =>
|
|
105
|
+
parsePreset(JSON.stringify(p), "p.jsonc").identity;
|
|
106
|
+
assert.deepEqual(
|
|
107
|
+
[
|
|
108
|
+
read({
|
|
109
|
+
type: "venue",
|
|
110
|
+
name: "X",
|
|
111
|
+
url: "https://x.org/cfp",
|
|
112
|
+
extends: "paperlint:acm-sigconf",
|
|
113
|
+
}),
|
|
114
|
+
read({ type: "family", extends: "paperlint:acm-sigconf" }),
|
|
115
|
+
read({ type: "base", tex: { packages: { a: ["a.sty"] } } }),
|
|
116
|
+
],
|
|
117
|
+
[
|
|
118
|
+
{ type: "venue", name: "X", url: "https://x.org/cfp" },
|
|
119
|
+
{ type: "family" },
|
|
120
|
+
{ type: "base" },
|
|
121
|
+
],
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("identityHint has nothing to add to a violation that is not about the identity", () => {
|
|
126
|
+
assert.equal(
|
|
127
|
+
identityHint({
|
|
128
|
+
keyword: "additionalProperties",
|
|
129
|
+
dataPath: "",
|
|
130
|
+
params: { additionalProperty: "colums" },
|
|
131
|
+
}),
|
|
132
|
+
null,
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("identityHints: Ajv's null errors give none; a hint two violations share is given once", () => {
|
|
137
|
+
assert.deepEqual(identityHints({ errors: null }), []);
|
|
138
|
+
const missing = {
|
|
139
|
+
keyword: "required",
|
|
140
|
+
dataPath: "",
|
|
141
|
+
schemaPath: "#/required",
|
|
142
|
+
params: { missingProperty: "type" },
|
|
143
|
+
};
|
|
144
|
+
const hints = identityHints({ errors: [missing, missing] });
|
|
145
|
+
assert.equal(hints.length, 1);
|
|
146
|
+
assert.deepEqual(hints, [identityHint(missing)]);
|
|
147
|
+
});
|