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/presets.test.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* resolving shows here.
|
|
8
8
|
*/
|
|
9
9
|
import { readdirSync, readFileSync } from "node:fs";
|
|
10
|
-
import { join } from "node:path";
|
|
10
|
+
import { join, relative } from "node:path";
|
|
11
11
|
import { describe, expect, it } from "vitest";
|
|
12
12
|
import { memoryFiles } from "./adapters/memory/index.ts";
|
|
13
13
|
import { presetsDir } from "./package-dirs.ts";
|
|
@@ -35,6 +35,17 @@ const deps = (extra: Record<string, string> = {}) => ({
|
|
|
35
35
|
});
|
|
36
36
|
const resolve = (spec: string, extra: Record<string, string> = {}) =>
|
|
37
37
|
resolvePreset(spec, PAPER_FILE, deps(extra));
|
|
38
|
+
/** A venue preset of a project's own: the identity every venue declares, then `body`. */
|
|
39
|
+
const venue = (body: Record<string, unknown>, name = "Our Workshop"): string =>
|
|
40
|
+
JSON.stringify({
|
|
41
|
+
type: "venue",
|
|
42
|
+
name,
|
|
43
|
+
url: "https://example.org/cfp",
|
|
44
|
+
...body,
|
|
45
|
+
});
|
|
46
|
+
/** A template family of a project's own. */
|
|
47
|
+
const family = (body: Record<string, unknown>): string =>
|
|
48
|
+
JSON.stringify({ type: "family", ...body });
|
|
38
49
|
|
|
39
50
|
describe("shipped presets", () => {
|
|
40
51
|
it("paperlint:agenticdev → agenticdev over acm-sigconf: the family's format, the venue's kinds", () => {
|
|
@@ -54,7 +65,15 @@ describe("shipped presets", () => {
|
|
|
54
65
|
file: join(VENUES, "acm-sigconf.jsonc"),
|
|
55
66
|
});
|
|
56
67
|
expect("acmart" in r.value.tex.packages).toBe(true);
|
|
57
|
-
|
|
68
|
+
// The label is the venue's own name, from its file's `name`; the id stays the file name — the
|
|
69
|
+
// stable key the facts file and the build plan carry.
|
|
70
|
+
expect(r.value.label).toBe("AgenticDev");
|
|
71
|
+
expect(r.value.id).toBe("agenticdev");
|
|
72
|
+
expect(r.value.identity).toEqual({
|
|
73
|
+
type: "venue",
|
|
74
|
+
name: "AgenticDev",
|
|
75
|
+
url: "https://conf.researchr.org/home/ase-2026/agenticdev-2026",
|
|
76
|
+
});
|
|
58
77
|
});
|
|
59
78
|
|
|
60
79
|
it("a paper may extend a family directly: format and fonts, and no kinds", () => {
|
|
@@ -78,19 +97,72 @@ describe("shipped presets", () => {
|
|
|
78
97
|
});
|
|
79
98
|
});
|
|
80
99
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
// Each shipped preset, parsed: its file name and what it extends. The families are DERIVED — a
|
|
101
|
+
// shipped preset another shipped preset extends — so a new family is checked the day it ships.
|
|
102
|
+
const PARSED = shippedPresets(VENUES).map((name) => {
|
|
103
|
+
const file = join(VENUES, `${name}.jsonc`);
|
|
104
|
+
return {
|
|
105
|
+
name,
|
|
106
|
+
preset: parsePreset(readFileSync(file, "utf8"), file, VENUES),
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
const FAMILIES = PARSED.filter((p) =>
|
|
110
|
+
PARSED.some((q) => q.preset.extends === `${SHIPPED_PREFIX}${p.name}`),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
describe("what each shipped preset declares itself to be", () => {
|
|
114
|
+
it("every shipped preset loads and declares what it is; a venue links its call", () => {
|
|
115
|
+
const declared = Object.fromEntries(
|
|
116
|
+
readdirSync(VENUES)
|
|
117
|
+
.filter((f) => f.endsWith(".jsonc"))
|
|
118
|
+
.map((f) => [
|
|
119
|
+
f,
|
|
120
|
+
parsePreset(readFileSync(join(VENUES, f), "utf8"), f, VENUES)
|
|
121
|
+
.identity,
|
|
122
|
+
]),
|
|
123
|
+
);
|
|
124
|
+
expect(declared).toEqual({
|
|
125
|
+
"acm-sigconf.jsonc": { type: "family" },
|
|
126
|
+
"agenticdev.jsonc": {
|
|
127
|
+
type: "venue",
|
|
128
|
+
name: "AgenticDev",
|
|
129
|
+
url: "https://conf.researchr.org/home/ase-2026/agenticdev-2026",
|
|
130
|
+
},
|
|
131
|
+
"aidc.jsonc": {
|
|
132
|
+
type: "venue",
|
|
133
|
+
name: "AIDC",
|
|
134
|
+
url: "https://aidcworkshop.github.io/",
|
|
135
|
+
},
|
|
136
|
+
"aisec.jsonc": {
|
|
137
|
+
type: "venue",
|
|
138
|
+
name: "AISec",
|
|
139
|
+
url: "https://aisec.cc/",
|
|
140
|
+
},
|
|
141
|
+
"ieee-conference.jsonc": { type: "family" },
|
|
142
|
+
"realm.jsonc": {
|
|
143
|
+
type: "venue",
|
|
144
|
+
name: "REALM",
|
|
145
|
+
url: "https://realm-workshop.github.io/call_for_papers",
|
|
146
|
+
},
|
|
147
|
+
"tex-base.jsonc": { type: "base" },
|
|
148
|
+
});
|
|
90
149
|
});
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
150
|
+
|
|
151
|
+
it("the families another preset extends are exactly the ones that declare `family`", () => {
|
|
152
|
+
expect(
|
|
153
|
+
PARSED.filter((p) => p.preset.identity.type === "family").map(
|
|
154
|
+
(p) => p.name,
|
|
155
|
+
),
|
|
156
|
+
).toEqual(FAMILIES.map((f) => f.name));
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("the naming convention of shipped presets (docs/rules.md)", () => {
|
|
161
|
+
const families = FAMILIES;
|
|
162
|
+
const presets = PARSED.map((p) => ({
|
|
163
|
+
name: p.name,
|
|
164
|
+
extends: p.preset.extends,
|
|
165
|
+
}));
|
|
94
166
|
const publishers = families.map((f) => f.name.slice(0, f.name.indexOf("-")));
|
|
95
167
|
|
|
96
168
|
it("there are families to check (else the checks below see nothing)", () => {
|
|
@@ -118,16 +190,18 @@ describe("the naming convention of shipped presets (docs/rules.md)", () => {
|
|
|
118
190
|
});
|
|
119
191
|
|
|
120
192
|
describe("a project's own preset, by relative path", () => {
|
|
121
|
-
const usenix =
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
193
|
+
const usenix = venue(
|
|
194
|
+
{
|
|
195
|
+
extends: "paperlint:acm-sigconf",
|
|
196
|
+
format: {
|
|
197
|
+
columns: 1,
|
|
198
|
+
kinds: { full: { body_pages_max: 13, ref_pages_max: 0 } },
|
|
199
|
+
},
|
|
200
|
+
tex: { packages: { usenix: ["usenix.sty"] } },
|
|
201
|
+
rules: { "pdf/body-size": "off" },
|
|
127
202
|
},
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
});
|
|
203
|
+
"USENIX Security",
|
|
204
|
+
);
|
|
131
205
|
|
|
132
206
|
it("./ is relative to the file that says it; merged per block, child wins", () => {
|
|
133
207
|
const r = resolve("../../venues/usenix-sec.jsonc", {
|
|
@@ -147,18 +221,21 @@ describe("a project's own preset, by relative path", () => {
|
|
|
147
221
|
expect(r.value.label).toBe("USENIX Security");
|
|
148
222
|
});
|
|
149
223
|
|
|
150
|
-
it("
|
|
224
|
+
it("a family names no venue: its label is the file name without its extension", () => {
|
|
151
225
|
const r = resolve("./venues/usenix-sec.jsonc", {
|
|
152
|
-
"/work/papers/p/venues/usenix-sec.jsonc":
|
|
226
|
+
"/work/papers/p/venues/usenix-sec.jsonc": family({
|
|
153
227
|
extends: "paperlint:acm-sigconf",
|
|
154
228
|
}),
|
|
155
229
|
});
|
|
156
230
|
expect(r.ok && r.value.label).toBe("usenix-sec");
|
|
231
|
+
expect(r.ok && r.value.identity).toEqual({ type: "family" });
|
|
157
232
|
});
|
|
233
|
+
});
|
|
158
234
|
|
|
235
|
+
describe("a project's own preset: what its children replace", () => {
|
|
159
236
|
it("a child kind replaces that kind wholesale; other kinds are kept", () => {
|
|
160
237
|
const r = resolve("./mine.jsonc", {
|
|
161
|
-
"/work/papers/p/mine.jsonc":
|
|
238
|
+
"/work/papers/p/mine.jsonc": venue({
|
|
162
239
|
extends: "paperlint:agenticdev",
|
|
163
240
|
format: { kinds: { short: { body_pages_max: 6 } } },
|
|
164
241
|
}),
|
|
@@ -174,13 +251,40 @@ describe("a project's own preset, by relative path", () => {
|
|
|
174
251
|
|
|
175
252
|
it("rules: the leaf wins over its parent, per rule id", () => {
|
|
176
253
|
const r = resolve("./mine.jsonc", {
|
|
177
|
-
"/work/papers/p/mine.jsonc":
|
|
254
|
+
"/work/papers/p/mine.jsonc": venue({
|
|
178
255
|
extends: "paperlint:agenticdev",
|
|
179
256
|
rules: { "pdf/last-page-balance": "off" },
|
|
180
257
|
}),
|
|
181
258
|
});
|
|
182
259
|
expect(r.ok && r.value.rules["pdf/last-page-balance"]).toBe("off");
|
|
183
260
|
});
|
|
261
|
+
|
|
262
|
+
it("ruleOrigins: each rule's entry names the file of the chain that set it", () => {
|
|
263
|
+
const r = resolve("./mine.jsonc", {
|
|
264
|
+
"/work/papers/p/mine.jsonc": venue({
|
|
265
|
+
extends: "paperlint:agenticdev",
|
|
266
|
+
rules: { "pdf/body-size": "off" },
|
|
267
|
+
}),
|
|
268
|
+
});
|
|
269
|
+
// Guards: an own rule is the leaf's, an inherited one names the parent that set it.
|
|
270
|
+
expect(r.ok && r.value.ruleOrigins).toEqual({
|
|
271
|
+
"pdf/last-page-balance": join(VENUES, "agenticdev.jsonc"),
|
|
272
|
+
"tex/heading-case": join(VENUES, "agenticdev.jsonc"),
|
|
273
|
+
"pdf/body-size": "/work/papers/p/mine.jsonc",
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("ruleOrigins: a rule the leaf sets again is the leaf's, not its parent's", () => {
|
|
278
|
+
const r = resolve("./mine.jsonc", {
|
|
279
|
+
"/work/papers/p/mine.jsonc": venue({
|
|
280
|
+
extends: "paperlint:agenticdev",
|
|
281
|
+
rules: { "tex/heading-case": "off" },
|
|
282
|
+
}),
|
|
283
|
+
});
|
|
284
|
+
expect(r.ok && r.value.ruleOrigins["tex/heading-case"]).toBe(
|
|
285
|
+
"/work/papers/p/mine.jsonc",
|
|
286
|
+
);
|
|
287
|
+
});
|
|
184
288
|
});
|
|
185
289
|
|
|
186
290
|
describe("what does not resolve, and says why", () => {
|
|
@@ -217,8 +321,8 @@ describe("what does not resolve, and says why", () => {
|
|
|
217
321
|
describe("chains that do not resolve", () => {
|
|
218
322
|
it("a cycle is refused, naming the chain", () => {
|
|
219
323
|
const r = resolve("./a.jsonc", {
|
|
220
|
-
"/work/papers/p/a.jsonc":
|
|
221
|
-
"/work/papers/p/b.jsonc":
|
|
324
|
+
"/work/papers/p/a.jsonc": family({ extends: "./b.jsonc" }),
|
|
325
|
+
"/work/papers/p/b.jsonc": family({ extends: "./a.jsonc" }),
|
|
222
326
|
});
|
|
223
327
|
expect(r.ok).toBe(false);
|
|
224
328
|
if (r.ok) return;
|
|
@@ -231,7 +335,7 @@ describe("chains that do not resolve", () => {
|
|
|
231
335
|
it(`a chain longer than ${String(MAX_PRESET_DEPTH)} is refused, printing it`, () => {
|
|
232
336
|
const files: Record<string, string> = {};
|
|
233
337
|
for (let i = 1; i <= 5; i++)
|
|
234
|
-
files[`/work/papers/p/p${String(i)}.jsonc`] =
|
|
338
|
+
files[`/work/papers/p/p${String(i)}.jsonc`] = venue({
|
|
235
339
|
extends: i < 5 ? `./p${String(i + 1)}.jsonc` : "paperlint:acm-sigconf",
|
|
236
340
|
});
|
|
237
341
|
const r = resolve("./p1.jsonc", files);
|
|
@@ -243,17 +347,17 @@ describe("chains that do not resolve", () => {
|
|
|
243
347
|
|
|
244
348
|
it("a chain of exactly the limit resolves", () => {
|
|
245
349
|
const r = resolve("./p1.jsonc", {
|
|
246
|
-
"/work/papers/p/p1.jsonc":
|
|
247
|
-
"/work/papers/p/p2.jsonc":
|
|
248
|
-
extends: "paperlint:agenticdev",
|
|
249
|
-
}),
|
|
350
|
+
"/work/papers/p/p1.jsonc": venue({ extends: "./p2.jsonc" }),
|
|
351
|
+
"/work/papers/p/p2.jsonc": venue({ extends: "paperlint:agenticdev" }),
|
|
250
352
|
});
|
|
251
353
|
expect(r.ok && r.value.chain.length).toBe(MAX_PRESET_DEPTH);
|
|
252
354
|
});
|
|
355
|
+
});
|
|
253
356
|
|
|
357
|
+
describe("chains that do not resolve: the files themselves", () => {
|
|
254
358
|
it("a preset that fails the schema is named with its file", () => {
|
|
255
359
|
const r = resolve("./bad.jsonc", {
|
|
256
|
-
"/work/papers/p/bad.jsonc":
|
|
360
|
+
"/work/papers/p/bad.jsonc": family({
|
|
257
361
|
extends: "paperlint:acm-sigconf",
|
|
258
362
|
colums: 2,
|
|
259
363
|
}),
|
|
@@ -266,16 +370,51 @@ describe("chains that do not resolve", () => {
|
|
|
266
370
|
|
|
267
371
|
it("a preset with no `extends` must carry `tex` (it is a root)", () => {
|
|
268
372
|
const r = resolve("./root.jsonc", {
|
|
269
|
-
"/work/papers/p/root.jsonc":
|
|
373
|
+
"/work/papers/p/root.jsonc": family({ format: { columns: 1 } }),
|
|
374
|
+
});
|
|
375
|
+
expect(r.ok).toBe(false);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("a family that extends a venue is refused, naming both files", () => {
|
|
379
|
+
const r = resolve("./fam.jsonc", {
|
|
380
|
+
"/work/papers/p/fam.jsonc": family({ extends: "paperlint:aisec" }),
|
|
381
|
+
});
|
|
382
|
+
expect(r.ok).toBe(false);
|
|
383
|
+
if (r.ok) return;
|
|
384
|
+
expect(presetProblemText(r.error)).toBe(
|
|
385
|
+
`the preset /work/papers/p/fam.jsonc is a template family and extends ${join(VENUES, "aisec.jsonc")}, a venue — a family builds only on a family; a venue extends a family or another venue`,
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("nothing extends the TeX base set, even by a relative path", () => {
|
|
390
|
+
const r = resolve("./mine.jsonc", {
|
|
391
|
+
"/work/papers/p/mine.jsonc": venue({
|
|
392
|
+
extends: relative("/work/papers/p", join(VENUES, "tex-base.jsonc")),
|
|
393
|
+
}),
|
|
270
394
|
});
|
|
271
395
|
expect(r.ok).toBe(false);
|
|
396
|
+
if (r.ok) return;
|
|
397
|
+
expect(presetProblemText(r.error)).toBe(
|
|
398
|
+
`the preset /work/papers/p/mine.jsonc extends ${join(VENUES, "tex-base.jsonc")}, the TeX base set — every paper gets that set already, and no preset extends it`,
|
|
399
|
+
);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("a venue may extend a venue, and the resolved identity is the leaf's", () => {
|
|
403
|
+
const r = resolve("./mine.jsonc", {
|
|
404
|
+
"/work/papers/p/mine.jsonc": venue({ extends: "paperlint:aisec" }),
|
|
405
|
+
});
|
|
406
|
+
expect(r.ok && r.value.identity).toEqual({
|
|
407
|
+
type: "venue",
|
|
408
|
+
name: "Our Workshop",
|
|
409
|
+
url: "https://example.org/cfp",
|
|
410
|
+
});
|
|
272
411
|
});
|
|
273
412
|
});
|
|
274
413
|
|
|
275
414
|
describe("a preset's template: kept as its file spells it, with that file", () => {
|
|
276
415
|
it("a child's template replaces its parent's, and names its own file", () => {
|
|
277
416
|
const r = resolve("./own.jsonc", {
|
|
278
|
-
"/work/papers/p/own.jsonc":
|
|
417
|
+
"/work/papers/p/own.jsonc": venue({
|
|
279
418
|
extends: "paperlint:aidc",
|
|
280
419
|
template: "\\documentclass[conference]{IEEEtran}",
|
|
281
420
|
}),
|
|
@@ -302,8 +441,16 @@ describe("labelOf — display only, never used to resolve", () => {
|
|
|
302
441
|
describe("shippedVenueNames — what each shipped venue is called", () => {
|
|
303
442
|
it("every shipped preset, by its label and the names along its chain", () => {
|
|
304
443
|
const names = shippedVenueNames(deps());
|
|
305
|
-
|
|
306
|
-
expect(names.
|
|
444
|
+
// A venue is labelled by its `name`, a family by its file name.
|
|
445
|
+
expect(names.map((n) => n.label)).toEqual([
|
|
446
|
+
"acm-sigconf",
|
|
447
|
+
"AgenticDev",
|
|
448
|
+
"AIDC",
|
|
449
|
+
"AISec",
|
|
450
|
+
"ieee-conference",
|
|
451
|
+
"REALM",
|
|
452
|
+
]);
|
|
453
|
+
expect(names.find((n) => n.label === "AISec")?.aliases).toEqual(
|
|
307
454
|
expect.arrayContaining(["AISec", "ACM CCS"]),
|
|
308
455
|
);
|
|
309
456
|
});
|
|
@@ -316,6 +463,12 @@ describe("shippedVenueNames — what each shipped venue is called", () => {
|
|
|
316
463
|
);
|
|
317
464
|
expect(
|
|
318
465
|
shippedVenueNames({ files, venuesDir: VENUES }).map((n) => n.label),
|
|
319
|
-
).toEqual(
|
|
466
|
+
).toEqual([
|
|
467
|
+
"acm-sigconf",
|
|
468
|
+
"AgenticDev",
|
|
469
|
+
"AIDC",
|
|
470
|
+
"AISec",
|
|
471
|
+
"ieee-conference",
|
|
472
|
+
]);
|
|
320
473
|
});
|
|
321
474
|
});
|
package/src/presets.ts
CHANGED
|
@@ -12,20 +12,27 @@
|
|
|
12
12
|
* Anything else — an npm package name, a bare name — is refused by name. npm presets are not
|
|
13
13
|
* supported yet; a bare name is almost always a shipped name missing its prefix, and the message says so.
|
|
14
14
|
*
|
|
15
|
+
* ── WHAT EACH FILE IS ─────────────────────────────────────────────────────────────
|
|
16
|
+
* Every preset declares its `type` (`venue-profile.schema.json`): a `venue` names itself and links
|
|
17
|
+
* its call for papers, a template `family` names no venue, and the TeX `base` set is extended by
|
|
18
|
+
* nothing. A chain must read top-down as template → venue: a family extends only a family, and
|
|
19
|
+
* nothing extends the base set. A chain that does not is refused naming both files.
|
|
20
|
+
*
|
|
15
21
|
* ── THE CHAIN ─────────────────────────────────────────────────────────────────────
|
|
16
22
|
* At most `MAX_PRESET_DEPTH` files, root to leaf. A cycle (relative paths can form one) is refused
|
|
17
23
|
* naming the chain. The chain is merged block by block:
|
|
18
24
|
*
|
|
19
25
|
* tex union — a child never removes a package its parent needs
|
|
20
26
|
* format per key, the child wins; `kinds` by kind name, a child's kind replaces that kind
|
|
21
|
-
* rules per rule id, the child wins
|
|
27
|
+
* rules per rule id, the child wins; `ruleOrigins` keeps which file set each
|
|
22
28
|
* template the child wins; so does `name`, and `required_sections`, `register`, `talk` and
|
|
23
29
|
* `portal` (each whole)
|
|
24
30
|
* aliases union, with every `name` — what the venue is called along the chain
|
|
25
31
|
*
|
|
26
|
-
* ── THE LABEL
|
|
27
|
-
*
|
|
28
|
-
*
|
|
32
|
+
* ── THE ID AND THE LABEL ──────────────────────────────────────────────────────────
|
|
33
|
+
* The facts file and the build plan carry the preset's `id`: the spec's file name without its
|
|
34
|
+
* extension, a stable key consumers match against preset ids. Messages say the `label`: the most
|
|
35
|
+
* derived venue's `name` when the chain has a venue, else the id
|
|
29
36
|
* (`paperlint:agenticdev` → `agenticdev`, `./venues/usenix-sec.jsonc` → `usenix-sec`). Display
|
|
30
37
|
* only: nothing is ever resolved by it.
|
|
31
38
|
*/
|
|
@@ -38,6 +45,7 @@ import {
|
|
|
38
45
|
profileFileOf,
|
|
39
46
|
venueNames,
|
|
40
47
|
type PresetFile,
|
|
48
|
+
type PresetIdentity,
|
|
41
49
|
type RequiredSection,
|
|
42
50
|
type TexRequirements,
|
|
43
51
|
type VenueFormat,
|
|
@@ -75,10 +83,17 @@ export interface PresetTemplate {
|
|
|
75
83
|
|
|
76
84
|
/** A resolved chain, merged. */
|
|
77
85
|
export interface Preset {
|
|
78
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* The preset's id: the spec's file name without its extension (`agenticdev`, `usenix-sec`). The
|
|
88
|
+
* stable key a machine reads — the facts file and the build plan carry it — whatever `name` says.
|
|
89
|
+
*/
|
|
90
|
+
readonly id: string;
|
|
91
|
+
/** The word for this venue in messages: the venue's `name`, else the id. */
|
|
79
92
|
readonly label: string;
|
|
80
93
|
/** Every file of the chain, root first. */
|
|
81
94
|
readonly chain: readonly string[];
|
|
95
|
+
/** What the spec names — the leaf file of the chain: a venue (with its call), or a family. */
|
|
96
|
+
readonly identity: PresetIdentity;
|
|
82
97
|
/**
|
|
83
98
|
* The `\\documentclass` the venue's template uses, as the preset spells it, and the file that
|
|
84
99
|
* spells it; null when no file of the chain names one. Read by the rules and `paperlint new`,
|
|
@@ -93,6 +108,8 @@ export interface Preset {
|
|
|
93
108
|
readonly tex: TexRequirements;
|
|
94
109
|
readonly format: VenueFormat;
|
|
95
110
|
readonly rules: Readonly<Record<string, unknown>>;
|
|
111
|
+
/** Rule id → the file of the chain that set its entry in `rules` (the last one to name it). */
|
|
112
|
+
readonly ruleOrigins: Readonly<Record<string, string>>;
|
|
96
113
|
/** The accepted papers the register rules measure a body against; empty when no file names any. */
|
|
97
114
|
readonly registerAnchors: readonly RegisterAnchor[];
|
|
98
115
|
/** What the venue asks a presenter to send (`talk/*`); null when no file of the chain says. */
|
|
@@ -112,6 +129,14 @@ export type PresetProblem =
|
|
|
112
129
|
}
|
|
113
130
|
| { readonly kind: "cycle"; readonly chain: readonly string[] }
|
|
114
131
|
| { readonly kind: "too-deep"; readonly chain: readonly string[] }
|
|
132
|
+
/** `file` extends `parent`, which a preset of its type may not extend. */
|
|
133
|
+
| {
|
|
134
|
+
readonly kind: "wrong-parent";
|
|
135
|
+
readonly file: string;
|
|
136
|
+
readonly parent: string;
|
|
137
|
+
/** A family extending a venue, or anything extending the base set. */
|
|
138
|
+
readonly parentType: "venue" | "base";
|
|
139
|
+
}
|
|
115
140
|
| { readonly kind: "broken"; readonly file: string; readonly why: string };
|
|
116
141
|
|
|
117
142
|
/** The display label of a spec: the file name without its extension. Pure. */
|
|
@@ -166,27 +191,47 @@ function readPreset(
|
|
|
166
191
|
}
|
|
167
192
|
}
|
|
168
193
|
|
|
169
|
-
/**
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
194
|
+
/** One file of a chain: where it is, and what it says. */
|
|
195
|
+
interface Link {
|
|
196
|
+
readonly file: string;
|
|
197
|
+
readonly preset: PresetFile;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The chain from `child` up, leaf first: `below` is every file read so far, `child` the last of them.
|
|
202
|
+
* Each parent is refused when it closes a cycle, makes the chain too long, or is of a type `child`
|
|
203
|
+
* may not extend.
|
|
204
|
+
*/
|
|
205
|
+
function chainUp(
|
|
206
|
+
below: readonly Link[],
|
|
207
|
+
child: Link,
|
|
173
208
|
deps: PresetDeps,
|
|
174
|
-
): Result<
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
209
|
+
): Result<readonly Link[], PresetProblem> {
|
|
210
|
+
const up = child.preset.extends;
|
|
211
|
+
if (up === null) return ok(below);
|
|
212
|
+
const r = readPreset(up, child.file, deps);
|
|
213
|
+
if (!r.ok) return r;
|
|
214
|
+
const files = below.map((x) => x.file);
|
|
215
|
+
if (files.includes(r.value.file))
|
|
216
|
+
return err({ kind: "cycle", chain: [...files, r.value.file] });
|
|
217
|
+
if (below.length === MAX_PRESET_DEPTH)
|
|
218
|
+
return err({ kind: "too-deep", chain: [...files, r.value.file] });
|
|
219
|
+
const parentType = r.value.preset.identity.type;
|
|
220
|
+
if (parentType === "venue" && child.preset.identity.type === "family")
|
|
221
|
+
return err({
|
|
222
|
+
kind: "wrong-parent",
|
|
223
|
+
file: child.file,
|
|
224
|
+
parent: r.value.file,
|
|
225
|
+
parentType,
|
|
226
|
+
});
|
|
227
|
+
if (parentType === "base")
|
|
228
|
+
return err({
|
|
229
|
+
kind: "wrong-parent",
|
|
230
|
+
file: child.file,
|
|
231
|
+
parent: r.value.file,
|
|
232
|
+
parentType,
|
|
233
|
+
});
|
|
234
|
+
return chainUp([...below, r.value], r.value, deps);
|
|
190
235
|
}
|
|
191
236
|
|
|
192
237
|
/** A child's format over its parent's: per key the child wins, kinds by name. Pure. */
|
|
@@ -213,52 +258,68 @@ export function mergeFormat(
|
|
|
213
258
|
};
|
|
214
259
|
}
|
|
215
260
|
|
|
261
|
+
/** A venue's name; a family and the base set have none. Pure. */
|
|
262
|
+
const nameOf = (i: PresetIdentity): string | null =>
|
|
263
|
+
i.type === "venue" ? i.name : null;
|
|
264
|
+
|
|
265
|
+
/** A venue's name as a list, empty for a family and the base set. Pure. */
|
|
266
|
+
const namesOf = (i: PresetIdentity): readonly string[] =>
|
|
267
|
+
i.type === "venue" ? [i.name] : [];
|
|
268
|
+
|
|
269
|
+
/** What the merge carries from file to file: the preset without what only the whole chain knows. */
|
|
270
|
+
type Merging = Omit<Preset, "id" | "label" | "chain" | "identity"> & {
|
|
271
|
+
readonly name: string | null;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const NOTHING_MERGED: Merging = {
|
|
275
|
+
name: null,
|
|
276
|
+
template: null,
|
|
277
|
+
aliases: [],
|
|
278
|
+
blind: false,
|
|
279
|
+
requiredSections: [],
|
|
280
|
+
tex: NO_REQUIREMENTS,
|
|
281
|
+
format: NO_FORMAT,
|
|
282
|
+
rules: {},
|
|
283
|
+
ruleOrigins: {},
|
|
284
|
+
registerAnchors: [],
|
|
285
|
+
talk: null,
|
|
286
|
+
portal: null,
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
/** One file of the chain, `file`, merged over what its parents gave. Pure. */
|
|
290
|
+
const mergeOne = (
|
|
291
|
+
acc: Merging,
|
|
292
|
+
{ preset: p, file }: { readonly preset: PresetFile; readonly file: string },
|
|
293
|
+
): Merging => ({
|
|
294
|
+
name: nameOf(p.identity) ?? acc.name,
|
|
295
|
+
template: p.template === null ? acc.template : { text: p.template, file },
|
|
296
|
+
aliases: [...new Set([...acc.aliases, ...namesOf(p.identity), ...p.aliases])],
|
|
297
|
+
blind: p.blind ?? acc.blind,
|
|
298
|
+
requiredSections: p.requiredSections ?? acc.requiredSections,
|
|
299
|
+
tex: p.tex ? mergeRequirements(acc.tex, p.tex) : acc.tex,
|
|
300
|
+
format: mergeFormat(acc.format, p.format),
|
|
301
|
+
rules: { ...acc.rules, ...p.rules },
|
|
302
|
+
ruleOrigins: {
|
|
303
|
+
...acc.ruleOrigins,
|
|
304
|
+
...Object.fromEntries(Object.keys(p.rules).map((id) => [id, file])),
|
|
305
|
+
},
|
|
306
|
+
registerAnchors: p.registerAnchors ?? acc.registerAnchors,
|
|
307
|
+
talk: p.talk ?? acc.talk,
|
|
308
|
+
portal: p.portal ?? acc.portal,
|
|
309
|
+
});
|
|
310
|
+
|
|
216
311
|
/** The chain, root first, merged into one preset. Pure. */
|
|
217
312
|
function merged(
|
|
218
313
|
spec: string,
|
|
219
314
|
rootFirst: readonly { readonly preset: PresetFile; readonly file: string }[],
|
|
315
|
+
leaf: { readonly preset: PresetFile },
|
|
220
316
|
): Preset {
|
|
221
|
-
const
|
|
222
|
-
readonly name: string | null;
|
|
223
|
-
} = {
|
|
224
|
-
name: null,
|
|
225
|
-
template: null,
|
|
226
|
-
aliases: [],
|
|
227
|
-
blind: false,
|
|
228
|
-
requiredSections: [],
|
|
229
|
-
tex: NO_REQUIREMENTS,
|
|
230
|
-
format: NO_FORMAT,
|
|
231
|
-
rules: {},
|
|
232
|
-
registerAnchors: [],
|
|
233
|
-
talk: null,
|
|
234
|
-
portal: null,
|
|
235
|
-
};
|
|
236
|
-
const m = rootFirst.reduce(
|
|
237
|
-
(acc, { preset: p, file }) => ({
|
|
238
|
-
name: p.name ?? acc.name,
|
|
239
|
-
template: p.template === null ? acc.template : { text: p.template, file },
|
|
240
|
-
aliases: [
|
|
241
|
-
...new Set([
|
|
242
|
-
...acc.aliases,
|
|
243
|
-
...(p.name === null ? [] : [p.name]),
|
|
244
|
-
...p.aliases,
|
|
245
|
-
]),
|
|
246
|
-
],
|
|
247
|
-
blind: p.blind ?? acc.blind,
|
|
248
|
-
requiredSections: p.requiredSections ?? acc.requiredSections,
|
|
249
|
-
tex: p.tex ? mergeRequirements(acc.tex, p.tex) : acc.tex,
|
|
250
|
-
format: mergeFormat(acc.format, p.format),
|
|
251
|
-
rules: { ...acc.rules, ...p.rules },
|
|
252
|
-
registerAnchors: p.registerAnchors ?? acc.registerAnchors,
|
|
253
|
-
talk: p.talk ?? acc.talk,
|
|
254
|
-
portal: p.portal ?? acc.portal,
|
|
255
|
-
}),
|
|
256
|
-
base,
|
|
257
|
-
);
|
|
258
|
-
const { name, ...rest } = m;
|
|
317
|
+
const { name, ...rest } = rootFirst.reduce(mergeOne, NOTHING_MERGED);
|
|
259
318
|
return {
|
|
319
|
+
id: labelOf(spec),
|
|
260
320
|
label: name ?? labelOf(spec),
|
|
261
321
|
chain: rootFirst.map((x) => x.file),
|
|
322
|
+
identity: leaf.preset.identity,
|
|
262
323
|
...rest,
|
|
263
324
|
};
|
|
264
325
|
}
|
|
@@ -272,13 +333,17 @@ export function resolvePreset(
|
|
|
272
333
|
fromFile: string,
|
|
273
334
|
deps: PresetDeps,
|
|
274
335
|
): Result<Preset, PresetProblem> {
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
336
|
+
const refused = (p: PresetProblem): Result<Preset, PresetProblem> =>
|
|
337
|
+
err(
|
|
338
|
+
p.kind === "not-found"
|
|
339
|
+
? { ...p, shipped: shippedPresets(deps.venuesDir) }
|
|
340
|
+
: p,
|
|
341
|
+
);
|
|
342
|
+
const leaf = readPreset(spec, fromFile, deps);
|
|
343
|
+
if (!leaf.ok) return refused(leaf.error);
|
|
344
|
+
const chain = chainUp([leaf.value], leaf.value, deps);
|
|
345
|
+
if (!chain.ok) return refused(chain.error);
|
|
346
|
+
return ok(merged(spec, [...chain.value].reverse(), leaf.value));
|
|
282
347
|
}
|
|
283
348
|
|
|
284
349
|
/** The shipped presets' names (each is spelled `paperlint:<name>`). */
|
|
@@ -336,6 +401,10 @@ export function presetProblemText(p: PresetProblem): string {
|
|
|
336
401
|
return `the preset chain is longer than ${String(MAX_PRESET_DEPTH)}: ${p.chain.join(" → ")}`;
|
|
337
402
|
case "broken":
|
|
338
403
|
return `the preset ${p.file} does not parse: ${p.why}`;
|
|
404
|
+
case "wrong-parent":
|
|
405
|
+
return p.parentType === "base"
|
|
406
|
+
? `the preset ${p.file} extends ${p.parent}, the TeX base set — every paper gets that set already, and no preset extends it`
|
|
407
|
+
: `the preset ${p.file} is a template family and extends ${p.parent}, a venue — a family builds only on a family; a venue extends a family or another venue`;
|
|
339
408
|
}
|
|
340
409
|
}
|
|
341
410
|
|