paperlint 6.2.3 → 6.3.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/README.md +8 -1
- package/dist/adapters/hotcrp/index.d.ts +7 -0
- package/dist/adapters/hotcrp/index.d.ts.map +1 -0
- package/dist/adapters/hotcrp/index.js +7 -0
- package/dist/adapters/hotcrp/index.js.map +1 -0
- package/dist/adapters/hotcrp/portal.io.d.ts +20 -0
- package/dist/adapters/hotcrp/portal.io.d.ts.map +1 -0
- package/dist/adapters/hotcrp/portal.io.js +90 -0
- package/dist/adapters/hotcrp/portal.io.js.map +1 -0
- package/dist/adapters/hotcrp/response.d.ts +13 -0
- package/dist/adapters/hotcrp/response.d.ts.map +1 -0
- package/dist/adapters/hotcrp/response.js +138 -0
- package/dist/adapters/hotcrp/response.js.map +1 -0
- package/dist/cli.d.ts +3 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +109 -34
- package/dist/cli.js.map +1 -1
- package/dist/domain/result.d.ts +4 -0
- package/dist/domain/result.d.ts.map +1 -1
- package/dist/domain/result.js +2 -0
- package/dist/domain/result.js.map +1 -1
- package/dist/domain/submission.d.ts +106 -0
- package/dist/domain/submission.d.ts.map +1 -0
- package/dist/domain/submission.js +39 -0
- package/dist/domain/submission.js.map +1 -0
- package/dist/domain/venue-name.d.ts +38 -0
- package/dist/domain/venue-name.d.ts.map +1 -0
- package/dist/domain/venue-name.js +71 -0
- package/dist/domain/venue-name.js.map +1 -0
- package/dist/folder-venue-rule.d.ts +51 -0
- package/dist/folder-venue-rule.d.ts.map +1 -0
- package/dist/folder-venue-rule.js +96 -0
- package/dist/folder-venue-rule.js.map +1 -0
- package/dist/init.d.ts +6 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +3 -3
- package/dist/init.js.map +1 -1
- package/dist/lib/paper-config.d.ts.map +1 -1
- package/dist/lib/paper-config.js +5 -1
- package/dist/lib/paper-config.js.map +1 -1
- package/dist/new-paper.d.ts +19 -5
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +23 -4
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-settings.d.ts +3 -0
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +20 -4
- package/dist/paper-settings.js.map +1 -1
- package/dist/ports/submission-portal.d.ts +19 -0
- package/dist/ports/submission-portal.d.ts.map +1 -0
- package/dist/ports/submission-portal.js +2 -0
- package/dist/ports/submission-portal.js.map +1 -0
- package/dist/presets.d.ts +24 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +35 -3
- package/dist/presets.js.map +1 -1
- package/dist/rules-config.js +1 -1
- package/dist/submission.d.ts +61 -0
- package/dist/submission.d.ts.map +1 -0
- package/dist/submission.js +211 -0
- package/dist/submission.js.map +1 -0
- package/dist/tex-requirements.d.ts +3 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +3 -0
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +6 -7
- package/dist/tex-venue-rules.d.ts.map +1 -1
- package/dist/tex-venue-rules.js +10 -14
- package/dist/tex-venue-rules.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/configuration.md +17 -10
- package/docs/rules/paper/folder-venue-leftover.md +104 -0
- package/docs/rules/talk/captions-cover.md +1 -1
- package/docs/rules/talk/duration-floor.md +1 -1
- package/docs/rules.md +44 -42
- package/docs/skills.md +5 -4
- package/docs/submission.md +64 -0
- package/docs/talk.md +1 -1
- package/lib/paper-config.ts +6 -1
- package/lib/skill-checks.d.mts +7 -0
- package/lib/skill-trigger-cases.mjs +11 -0
- package/package.json +2 -2
- package/presets/aidc.jsonc +6 -0
- package/presets/venue-profile.schema.json +17 -1
- package/skills/paper-pipeline/paper-pipeline.harness.mjs +4 -0
- package/skills/submission-portal/SKILL.md +69 -0
- package/skills/submission-portal/SKILL.md.spec.ts +71 -0
- package/skills/submission-portal/submission-portal.eval.mjs +15 -0
- package/skills/submission-portal/submission-portal.test.ts +81 -0
- package/skills/submit-paper/SKILL.md +2 -1
- package/skills/submit-paper/SKILL.md.spec.ts +1 -0
- package/src/adapters/hotcrp/cassette/README.md +24 -0
- package/src/adapters/hotcrp/cassette/get-noauth.json +10 -0
- package/src/adapters/hotcrp/cassette/get-paper.json +39 -0
- package/src/adapters/hotcrp/cassette/post-dot.json +21 -0
- package/src/adapters/hotcrp/cassette/post-ok.json +12 -0
- package/src/adapters/hotcrp/index.ts +11 -0
- package/src/adapters/hotcrp/portal.io.ts +130 -0
- package/src/adapters/hotcrp/portal.test.ts +503 -0
- package/src/adapters/hotcrp/response.ts +181 -0
- package/src/cli.test.ts +109 -0
- package/src/cli.ts +140 -21
- package/src/domain/result.test.ts +9 -1
- package/src/domain/result.ts +5 -0
- package/src/domain/submission.test.ts +45 -0
- package/src/domain/submission.ts +141 -0
- package/src/domain/venue-name.test.ts +115 -0
- package/src/domain/venue-name.ts +129 -0
- package/src/folder-venue-rule.test.ts +182 -0
- package/src/folder-venue-rule.ts +141 -0
- package/src/init.test.ts +22 -0
- package/src/init.ts +9 -2
- package/src/new-paper.harness.mjs +11 -9
- package/src/new-paper.test.ts +129 -8
- package/src/new-paper.ts +38 -7
- package/src/paper-settings.test.ts +12 -1
- package/src/paper-settings.ts +25 -5
- package/src/parse-args.test.ts +19 -0
- package/src/ports/submission-portal.ts +26 -0
- package/src/presets.test.ts +22 -0
- package/src/presets.ts +46 -3
- package/src/rule-cases.test.ts +19 -0
- package/src/rules-config.ts +1 -1
- package/src/submission.test.ts +496 -0
- package/src/submission.ts +311 -0
- package/src/tex-requirements.ts +8 -0
- package/src/tex-venue-rules.test.ts +1 -1
- package/src/tex-venue-rules.ts +12 -25
- package/src/types.ts +13 -0
|
@@ -75,22 +75,23 @@ try {
|
|
|
75
75
|
|
|
76
76
|
// ── names ───────────────────────────────────────────────────────────────────────────
|
|
77
77
|
for (const ok of ["demo", "aisec-2026", "v1.2_final"])
|
|
78
|
-
check(`\`${ok}\` is a valid name`, nameProblem(ok) === null);
|
|
78
|
+
check(`\`${ok}\` is a valid name`, nameProblem(ok, []) === null);
|
|
79
79
|
for (const bad of ["Demo", "my paper", "a/b", "../x", "", "ÿ"])
|
|
80
|
-
check(`\`${bad}\` is refused`, typeof nameProblem(bad) === "string");
|
|
80
|
+
check(`\`${bad}\` is refused`, typeof nameProblem(bad, []) === "string");
|
|
81
81
|
for (const dot of [".template", ".x", ".."])
|
|
82
82
|
check(
|
|
83
83
|
`\`${dot}\` is refused — discovery skips dot-directories, so it would never be linted`,
|
|
84
|
-
/starting with a dot/.test(nameProblem(dot) ?? ""),
|
|
84
|
+
/starting with a dot/.test(nameProblem(dot, []) ?? ""),
|
|
85
85
|
);
|
|
86
86
|
check(
|
|
87
87
|
"a refused name creates nothing",
|
|
88
|
-
newPaper(work, "Bad", "tex").ok === false &&
|
|
88
|
+
newPaper(work, "Bad", "tex", { venues: [] }).ok === false &&
|
|
89
|
+
!existsSync(join(work, "Bad")),
|
|
89
90
|
);
|
|
90
91
|
|
|
91
92
|
// ── a fresh LaTeX paper ─────────────────────────────────────────────────────────────
|
|
92
93
|
const papers = join(work, "papers");
|
|
93
|
-
const r = newPaper(papers, "demo", "tex");
|
|
94
|
+
const r = newPaper(papers, "demo", "tex", { venues: [] });
|
|
94
95
|
check(
|
|
95
96
|
"a fresh folder gets the scorecard, a .tex source and paperlint.json, all from the package",
|
|
96
97
|
r.ok &&
|
|
@@ -128,7 +129,7 @@ try {
|
|
|
128
129
|
|
|
129
130
|
// ── never overwrite; on an existing folder only the missing files ─────────────────────
|
|
130
131
|
writeFileSync(join(papers, "demo", STATUS_FILE), "mine\n");
|
|
131
|
-
const again = newPaper(papers, "demo", "md");
|
|
132
|
+
const again = newPaper(papers, "demo", "md", { venues: [] });
|
|
132
133
|
check(
|
|
133
134
|
"🔴 a second run overwrites nothing and adds no second source",
|
|
134
135
|
again.ok &&
|
|
@@ -139,7 +140,7 @@ try {
|
|
|
139
140
|
);
|
|
140
141
|
mkdirSync(join(papers, "old"), { recursive: true });
|
|
141
142
|
writeFileSync(join(papers, "old", "paper.md"), "# Old\n");
|
|
142
|
-
const migrated = newPaper(papers, "old", "tex");
|
|
143
|
+
const migrated = newPaper(papers, "old", "tex", { venues: [] });
|
|
143
144
|
check(
|
|
144
145
|
"an old folder with only paper.md gets ONLY the scorecard — that is the migration",
|
|
145
146
|
migrated.ok &&
|
|
@@ -157,7 +158,7 @@ try {
|
|
|
157
158
|
writeFileSync(join(papers, "afile"), "x");
|
|
158
159
|
check(
|
|
159
160
|
"a name taken by a FILE is refused, not written through",
|
|
160
|
-
newPaper(papers, "afile", "tex").ok === false,
|
|
161
|
+
newPaper(papers, "afile", "tex", { venues: [] }).ok === false,
|
|
161
162
|
);
|
|
162
163
|
|
|
163
164
|
// ── the project's template wins, file by file ─────────────────────────────────────────
|
|
@@ -166,7 +167,7 @@ try {
|
|
|
166
167
|
join(papers, OVERRIDE_DIR, STATUS_FILE),
|
|
167
168
|
"---\n---\n# House scorecard for {{name}}\n",
|
|
168
169
|
);
|
|
169
|
-
const own = newPaper(papers, "house", "md");
|
|
170
|
+
const own = newPaper(papers, "house", "md", { venues: [] });
|
|
170
171
|
// Guards: the override slot — `fromTemplate` in new-paper.ts reads `<papers>/.template/<file>`
|
|
171
172
|
// before the package's copy. Replace that lookup with the package's alone and this goes red.
|
|
172
173
|
check(
|
|
@@ -180,6 +181,7 @@ try {
|
|
|
180
181
|
|
|
181
182
|
// ── a missing template is a broken install, not an empty file ─────────────────────────
|
|
182
183
|
const broken = newPaper(join(work, "b"), "x", "tex", {
|
|
184
|
+
venues: [],
|
|
183
185
|
packageTemplates: join(work, "no-such-dir"),
|
|
184
186
|
});
|
|
185
187
|
check(
|
package/src/new-paper.test.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import {
|
|
7
7
|
mkdirSync,
|
|
8
8
|
mkdtempSync,
|
|
9
|
+
readdirSync,
|
|
9
10
|
readFileSync,
|
|
10
11
|
realpathSync,
|
|
11
12
|
rmSync,
|
|
@@ -40,7 +41,7 @@ const settingsOf = (dir: string) =>
|
|
|
40
41
|
describe("paperlint new — paperlint.json", () => {
|
|
41
42
|
it("is always written, from the package template: no venue chosen yet, and valid", () => {
|
|
42
43
|
const papers = join(tmp(), "papers");
|
|
43
|
-
const r = newPaper(papers, "demo", "tex");
|
|
44
|
+
const r = newPaper(papers, "demo", "tex", { venues: [] });
|
|
44
45
|
expect(r.ok && r.files.find((f) => f.file === "paperlint.json")).toEqual({
|
|
45
46
|
file: "paperlint.json",
|
|
46
47
|
status: "created",
|
|
@@ -58,6 +59,7 @@ describe("paperlint new — paperlint.json", () => {
|
|
|
58
59
|
rules: null,
|
|
59
60
|
identity: null,
|
|
60
61
|
talk: null,
|
|
62
|
+
submission: null,
|
|
61
63
|
},
|
|
62
64
|
});
|
|
63
65
|
});
|
|
@@ -69,7 +71,7 @@ describe("paperlint new — paperlint.json", () => {
|
|
|
69
71
|
join(papers, OVERRIDE_DIR, "paperlint.json"),
|
|
70
72
|
'{ "extends": "paperlint:aisec", "kind": "research" }\n',
|
|
71
73
|
);
|
|
72
|
-
const r = newPaper(papers, "house", "tex");
|
|
74
|
+
const r = newPaper(papers, "house", "tex", { venues: [] });
|
|
73
75
|
expect(r.ok && r.files.find((f) => f.file === "paperlint.json")?.from).toBe(
|
|
74
76
|
"project",
|
|
75
77
|
);
|
|
@@ -86,7 +88,7 @@ describe("paperlint new — paperlint.json", () => {
|
|
|
86
88
|
join(papers, "p", "paperlint.json"),
|
|
87
89
|
'{"extends":"paperlint:aisec"}',
|
|
88
90
|
);
|
|
89
|
-
const r = newPaper(papers, "p", "tex");
|
|
91
|
+
const r = newPaper(papers, "p", "tex", { venues: [] });
|
|
90
92
|
expect(
|
|
91
93
|
r.ok && r.files.find((f) => f.file === "paperlint.json")?.status,
|
|
92
94
|
).toBe("kept");
|
|
@@ -103,7 +105,7 @@ describe("paperlint lint — a paper with no venue preset chosen", () => {
|
|
|
103
105
|
join(root, "package.json"),
|
|
104
106
|
JSON.stringify({ name: "c", private: true }),
|
|
105
107
|
);
|
|
106
|
-
newPaper(join(root, "papers"), "demo", "tex");
|
|
108
|
+
newPaper(join(root, "papers"), "demo", "tex", { venues: [] });
|
|
107
109
|
if (extendsValue !== null)
|
|
108
110
|
writeFileSync(
|
|
109
111
|
join(root, "papers", "demo", "paperlint.json"),
|
|
@@ -140,7 +142,11 @@ describe("paperlint lint — a paper with no venue preset chosen", () => {
|
|
|
140
142
|
});
|
|
141
143
|
|
|
142
144
|
/** `paperlint new <args>` in a fresh project (not a terminal: vitest's stdin is not a TTY). */
|
|
143
|
-
async function newIn(
|
|
145
|
+
async function newIn(
|
|
146
|
+
args: string[],
|
|
147
|
+
files: Record<string, string> = {},
|
|
148
|
+
name = "demo",
|
|
149
|
+
) {
|
|
144
150
|
const root = tmp();
|
|
145
151
|
const all = {
|
|
146
152
|
"package.json": JSON.stringify({ name: "c", private: true }),
|
|
@@ -152,12 +158,12 @@ async function newIn(args: string[], files: Record<string, string> = {}) {
|
|
|
152
158
|
}
|
|
153
159
|
const out: string[] = [];
|
|
154
160
|
const err: string[] = [];
|
|
155
|
-
const code = await run(["new",
|
|
161
|
+
const code = await run(["new", name, ...args], {
|
|
156
162
|
cwd: root,
|
|
157
163
|
log: (s: string) => out.push(s),
|
|
158
164
|
err: (s: string) => err.push(s),
|
|
159
165
|
});
|
|
160
|
-
const dir = join(root, "papers",
|
|
166
|
+
const dir = join(root, "papers", name);
|
|
161
167
|
return {
|
|
162
168
|
code,
|
|
163
169
|
out: out.join("\n"),
|
|
@@ -286,6 +292,117 @@ describe("paperlint new --venue / --kind — refusals and defaults", () => {
|
|
|
286
292
|
});
|
|
287
293
|
});
|
|
288
294
|
|
|
295
|
+
describe("paperlint new — a name that names a venue is refused", () => {
|
|
296
|
+
it.each<[string, string, string[], string]>([
|
|
297
|
+
[
|
|
298
|
+
"a venue's label, with the work's name left to suggest",
|
|
299
|
+
"aisec-agent-drift",
|
|
300
|
+
[],
|
|
301
|
+
'`aisec-agent-drift` — the paper folder names a venue ("aisec"); venues change on resubmission, so name it after the work (e.g. "agent-drift")',
|
|
302
|
+
],
|
|
303
|
+
[
|
|
304
|
+
"nothing but a year left: no example",
|
|
305
|
+
"aisec-2026",
|
|
306
|
+
[],
|
|
307
|
+
'`aisec-2026` — the paper folder names a venue ("aisec"); venues change on resubmission, so name it after the work',
|
|
308
|
+
],
|
|
309
|
+
[
|
|
310
|
+
"its own venue too: right today, stale on the next resubmission",
|
|
311
|
+
"aidc-2026",
|
|
312
|
+
["--venue", "aidc"],
|
|
313
|
+
'`aidc-2026` — the paper folder names a venue ("aidc"); venues change on resubmission, so name it after the work',
|
|
314
|
+
],
|
|
315
|
+
[
|
|
316
|
+
"by an alias, glued to a year",
|
|
317
|
+
"acsac2026-rules",
|
|
318
|
+
[],
|
|
319
|
+
'`acsac2026-rules` — the paper folder names a venue ("ACSAC"); venues change on resubmission, so name it after the work',
|
|
320
|
+
],
|
|
321
|
+
[
|
|
322
|
+
"two venues",
|
|
323
|
+
"realm-aisec-rules",
|
|
324
|
+
[],
|
|
325
|
+
'`realm-aisec-rules` — the paper folder names a venue ("aisec", "realm"); venues change on resubmission, so name it after the work (e.g. "rules")',
|
|
326
|
+
],
|
|
327
|
+
])("%s: exit 2, nothing written", async (_, name, args, reason) => {
|
|
328
|
+
const r = await newIn(args, {}, name);
|
|
329
|
+
expect(r.code).toBe(2);
|
|
330
|
+
expect(r.out).toBe(
|
|
331
|
+
` ✗ ${reason}\n if the venue really belongs in the name, rerun \`paperlint new\` with --allow-venue-name`,
|
|
332
|
+
);
|
|
333
|
+
expect(existsSync(r.dir)).toBe(false);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("an existing folder that names a venue gets no missing files either", async () => {
|
|
337
|
+
const r = await newIn(
|
|
338
|
+
[],
|
|
339
|
+
{ "papers/aisec-2026/paper.tex": "x" },
|
|
340
|
+
"aisec-2026",
|
|
341
|
+
);
|
|
342
|
+
expect(r.code).toBe(2);
|
|
343
|
+
expect(readdirSync(r.dir)).toEqual(["paper.tex"]);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it.each(["agent-rule-drift", "overrealm-realms"])(
|
|
347
|
+
"%s names no venue: created, as before",
|
|
348
|
+
async (name) => {
|
|
349
|
+
const r = await newIn([], {}, name);
|
|
350
|
+
expect(r.out).not.toMatch(/names a venue/);
|
|
351
|
+
expect(existsSync(join(r.dir, "PIPELINE-STATUS.md"))).toBe(true);
|
|
352
|
+
expect(r.code).toBe(0);
|
|
353
|
+
},
|
|
354
|
+
);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe("paperlint new --allow-venue-name", () => {
|
|
358
|
+
it("the folder is created as any other, and nothing is said about the venue", async () => {
|
|
359
|
+
const r = await newIn(["--allow-venue-name"], {}, "realm-of-agents");
|
|
360
|
+
expect(r.code).toBe(0);
|
|
361
|
+
expect(r.out).not.toMatch(/venue really belongs|names a venue/);
|
|
362
|
+
expect(readdirSync(r.dir).sort()).toEqual([
|
|
363
|
+
"PIPELINE-STATUS.md",
|
|
364
|
+
"paper.tex",
|
|
365
|
+
"paperlint.json",
|
|
366
|
+
]);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it.each<{
|
|
370
|
+
readonly what: string;
|
|
371
|
+
readonly name: string;
|
|
372
|
+
readonly args: readonly string[];
|
|
373
|
+
readonly files: Record<string, string>;
|
|
374
|
+
readonly says: RegExp;
|
|
375
|
+
}>([
|
|
376
|
+
{
|
|
377
|
+
what: "an invalid name",
|
|
378
|
+
name: "Realm",
|
|
379
|
+
args: [],
|
|
380
|
+
files: {},
|
|
381
|
+
says: /may hold only a-z/,
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
what: "an unknown preset",
|
|
385
|
+
name: "realm-of-agents",
|
|
386
|
+
args: ["--venue", "nope"],
|
|
387
|
+
files: {},
|
|
388
|
+
says: /no such venue preset/,
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
what: "an existing paperlint.json with --venue",
|
|
392
|
+
name: "realm-of-agents",
|
|
393
|
+
args: ["--venue", "aisec"],
|
|
394
|
+
files: {
|
|
395
|
+
"papers/realm-of-agents/paperlint.json": '{ "extends": null }\n',
|
|
396
|
+
},
|
|
397
|
+
says: /already exists and is never overwritten/,
|
|
398
|
+
},
|
|
399
|
+
])("skips no other refusal: $what", async ({ name, args, files, says }) => {
|
|
400
|
+
const r = await newIn(["--allow-venue-name", ...args], files, name);
|
|
401
|
+
expect(r.code).toBe(2);
|
|
402
|
+
expect(`${r.out}\n${r.err}`).toMatch(says);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
289
406
|
describe("chooseVenue — on a terminal", () => {
|
|
290
407
|
const at = (answers: string[]) => {
|
|
291
408
|
const asked: string[] = [];
|
|
@@ -340,6 +457,7 @@ describe("a project's own template", () => {
|
|
|
340
457
|
const src = join(papers, OVERRIDE_DIR, "paperlint.json");
|
|
341
458
|
writeFileSync(src, '{\n // our house venue\n "extends": null\n}\n');
|
|
342
459
|
const r = newPaper(papers, "p", "tex", {
|
|
460
|
+
venues: [],
|
|
343
461
|
venue: { extends: "paperlint:aisec", kind: null },
|
|
344
462
|
});
|
|
345
463
|
expect(r).toEqual({
|
|
@@ -353,7 +471,10 @@ describe("a project's own template", () => {
|
|
|
353
471
|
const papers = join(tmp(), "papers");
|
|
354
472
|
mkdirSync(join(papers, OVERRIDE_DIR), { recursive: true });
|
|
355
473
|
writeFileSync(join(papers, OVERRIDE_DIR, "paperlint.json"), "{}\n");
|
|
356
|
-
const lines = reportNewPaper(
|
|
474
|
+
const lines = reportNewPaper(
|
|
475
|
+
newPaper(papers, "p", "tex", { venues: [] }),
|
|
476
|
+
(p) => p,
|
|
477
|
+
);
|
|
357
478
|
expect(lines).toContain(
|
|
358
479
|
" + paperlint.json (from the project's .template/ template)",
|
|
359
480
|
);
|
package/src/new-paper.ts
CHANGED
|
@@ -27,6 +27,11 @@ import { join } from "node:path";
|
|
|
27
27
|
import { fileURLToPath } from "node:url";
|
|
28
28
|
import { CONFIG_FILE } from "#lib/paper-config";
|
|
29
29
|
import { isRecord } from "./domain/record.ts";
|
|
30
|
+
import {
|
|
31
|
+
venuesNamedBy,
|
|
32
|
+
withoutVenues,
|
|
33
|
+
type NamedVenue,
|
|
34
|
+
} from "./domain/venue-name.ts";
|
|
30
35
|
|
|
31
36
|
export type PaperFormat = "tex" | "md";
|
|
32
37
|
export const FORMATS: readonly PaperFormat[] = ["tex", "md"];
|
|
@@ -50,6 +55,9 @@ export const SOURCE_FILE: Readonly<Record<PaperFormat, string>> = {
|
|
|
50
55
|
md: "paper.md",
|
|
51
56
|
};
|
|
52
57
|
|
|
58
|
+
/** The flag that lets `paperlint new` create a folder whose name names a venue. */
|
|
59
|
+
export const ALLOW_VENUE_NAME = "--allow-venue-name";
|
|
60
|
+
|
|
53
61
|
export const isFormat = (v: unknown): v is PaperFormat =>
|
|
54
62
|
FORMATS.some((f) => f === v);
|
|
55
63
|
|
|
@@ -57,13 +65,30 @@ export const isFormat = (v: unknown): v is PaperFormat =>
|
|
|
57
65
|
* Why a name is refused, or null. `[a-z0-9._-]+` is the charset; a LEADING dot is refused on top
|
|
58
66
|
* of it, because discovery skips dot-directories — a paper named `.x` would be created and then
|
|
59
67
|
* never linted, which is worse than a refusal.
|
|
68
|
+
*
|
|
69
|
+
* A name that names a venue — one of `venues`' names as whole words (`aisec-2026`) — is refused
|
|
70
|
+
* too, the paper's own venue included: a paper that is rejected goes to another venue and keeps its
|
|
71
|
+
* folder, so the name goes stale, and nothing at creation time knows the paper will not move. A
|
|
72
|
+
* folder made by hand, and one made with `--allow-venue-name` (a whole word can be the work's own:
|
|
73
|
+
* a paper about realms), is what `paper/folder-venue-leftover` judges once the venue changes. The
|
|
74
|
+
* flag passes no `venues`, so it skips this refusal and no other.
|
|
60
75
|
*/
|
|
61
|
-
export function nameProblem(
|
|
76
|
+
export function nameProblem(
|
|
77
|
+
name: string,
|
|
78
|
+
venues: readonly NamedVenue[],
|
|
79
|
+
): string | null {
|
|
62
80
|
if (!/^[a-z0-9._-]+$/.test(name))
|
|
63
81
|
return `\`${name}\` — a paper name may hold only a-z, 0-9, dot, underscore and hyphen (it becomes a path and a shell argument)`;
|
|
64
82
|
if (name.startsWith("."))
|
|
65
83
|
return `\`${name}\` — a name starting with a dot is skipped by paper discovery, so it would never be linted`;
|
|
66
|
-
|
|
84
|
+
const named = venuesNamedBy(name, venues);
|
|
85
|
+
if (named.length === 0) return null;
|
|
86
|
+
const which = named.map((n) => `"${n.name}"`).join(", ");
|
|
87
|
+
const instead = withoutVenues(name, venues);
|
|
88
|
+
return [
|
|
89
|
+
`\`${name}\` — the paper folder names a venue (${which}); venues change on resubmission, so name it after the work${instead === null ? "" : ` (e.g. "${instead}")`}`,
|
|
90
|
+
` if the venue really belongs in the name, rerun \`paperlint new\` with ${ALLOW_VENUE_NAME}`,
|
|
91
|
+
].join("\n");
|
|
67
92
|
}
|
|
68
93
|
|
|
69
94
|
export interface FileOutcome {
|
|
@@ -180,17 +205,23 @@ function fromTemplate(
|
|
|
180
205
|
: edited;
|
|
181
206
|
}
|
|
182
207
|
|
|
208
|
+
/** What `newPaper` is told besides the name: the venues a name may not name, and what to write. */
|
|
209
|
+
export interface NewPaperOptions {
|
|
210
|
+
readonly packageTemplates?: string;
|
|
211
|
+
readonly venue?: VenueSetting | null;
|
|
212
|
+
/** Every venue paperlint knows (`shippedVenueNames`): a name naming one is refused. */
|
|
213
|
+
readonly venues: readonly NamedVenue[];
|
|
214
|
+
}
|
|
215
|
+
|
|
183
216
|
// Documented in README.md#getting-started — update it when this changes.
|
|
184
217
|
export function newPaper(
|
|
185
218
|
papersRoot: string,
|
|
186
219
|
name: string,
|
|
187
220
|
format: PaperFormat,
|
|
188
|
-
|
|
189
|
-
packageTemplates = PACKAGE_TEMPLATES,
|
|
190
|
-
venue = null,
|
|
191
|
-
}: { packageTemplates?: string; venue?: VenueSetting | null } = {},
|
|
221
|
+
opts: NewPaperOptions,
|
|
192
222
|
): NewPaperResult {
|
|
193
|
-
const
|
|
223
|
+
const { packageTemplates = PACKAGE_TEMPLATES, venue = null } = opts;
|
|
224
|
+
const problem = nameProblem(name, opts.venues);
|
|
194
225
|
if (problem) return { ok: false, reason: problem };
|
|
195
226
|
const dir = join(papersRoot, name);
|
|
196
227
|
const fresh = !existsSync(dir);
|
|
@@ -32,6 +32,7 @@ describe("parsePaperSettings", () => {
|
|
|
32
32
|
rules: null,
|
|
33
33
|
identity: null,
|
|
34
34
|
talk: null,
|
|
35
|
+
submission: null,
|
|
35
36
|
},
|
|
36
37
|
});
|
|
37
38
|
expect(parsePaperSettings({})).toEqual({
|
|
@@ -43,6 +44,7 @@ describe("parsePaperSettings", () => {
|
|
|
43
44
|
rules: null,
|
|
44
45
|
identity: null,
|
|
45
46
|
talk: null,
|
|
47
|
+
submission: null,
|
|
46
48
|
},
|
|
47
49
|
});
|
|
48
50
|
});
|
|
@@ -57,6 +59,7 @@ describe("parsePaperSettings", () => {
|
|
|
57
59
|
rules: null,
|
|
58
60
|
identity: null,
|
|
59
61
|
talk: null,
|
|
62
|
+
submission: null,
|
|
60
63
|
},
|
|
61
64
|
});
|
|
62
65
|
});
|
|
@@ -155,7 +158,13 @@ describe("readPaperSettings — a root paperlint.json that cannot be used", () =
|
|
|
155
158
|
});
|
|
156
159
|
|
|
157
160
|
/** The fields a case does not set. */
|
|
158
|
-
const UNSET = {
|
|
161
|
+
const UNSET = {
|
|
162
|
+
pdf: null,
|
|
163
|
+
rules: null,
|
|
164
|
+
identity: null,
|
|
165
|
+
talk: null,
|
|
166
|
+
submission: null,
|
|
167
|
+
};
|
|
159
168
|
|
|
160
169
|
describe("readPaperSettings — the root paperlint.json's defaults, the paper's file over them", () => {
|
|
161
170
|
const ROOT = "/work";
|
|
@@ -210,6 +219,7 @@ describe("readPaperSettings — the root paperlint.json's defaults, the paper's
|
|
|
210
219
|
rules: { "pdf/fonts": "off" },
|
|
211
220
|
identity: null,
|
|
212
221
|
talk: null,
|
|
222
|
+
submission: null,
|
|
213
223
|
});
|
|
214
224
|
});
|
|
215
225
|
|
|
@@ -253,6 +263,7 @@ describe("paperRules — `rules` in paperlint.json", () => {
|
|
|
253
263
|
rules,
|
|
254
264
|
identity: null,
|
|
255
265
|
talk: null,
|
|
266
|
+
submission: null,
|
|
256
267
|
});
|
|
257
268
|
|
|
258
269
|
it("no rules: none", () => {
|
package/src/paper-settings.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* One file name at two levels, one schema (lib/paper-config.mjs → SETTINGS_KEYS):
|
|
5
5
|
*
|
|
6
6
|
* paperlint.json the project (optional): papersDir, rules, defaults
|
|
7
|
-
* <paper>/paperlint.json this paper: { extends, kind, pdf, rules }
|
|
7
|
+
* <paper>/paperlint.json this paper: { extends, kind, pdf, rules, identity, talk, submission }
|
|
8
8
|
* a venue preset `paperlint:<name>` (shipped) or `./x.jsonc` (src/presets.ts)
|
|
9
9
|
*
|
|
10
10
|
* The paper's `extends`, `kind` and `pdf` win over the root's; with no own value, the root's is
|
|
@@ -32,6 +32,10 @@ import {
|
|
|
32
32
|
} from "./rules-config.ts";
|
|
33
33
|
import { messageOf } from "./domain/text.ts";
|
|
34
34
|
import { parsePaperTalk, type PaperTalk } from "./domain/talk.ts";
|
|
35
|
+
import {
|
|
36
|
+
parsePaperSubmission,
|
|
37
|
+
type PaperSubmission,
|
|
38
|
+
} from "./domain/submission.ts";
|
|
35
39
|
|
|
36
40
|
/** What one paper declares. Every absent field is null. */
|
|
37
41
|
export interface PaperSettings {
|
|
@@ -54,6 +58,8 @@ export interface PaperSettings {
|
|
|
54
58
|
readonly identity: readonly string[] | null;
|
|
55
59
|
/** How this paper is presented (`talk/*`); null when it declares no talk. Never the root's. */
|
|
56
60
|
readonly talk: PaperTalk | null;
|
|
61
|
+
/** Which submission on the venue's portal is this paper's; null when it says none. Never the root's. */
|
|
62
|
+
readonly submission: PaperSubmission | null;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
/** Why a paper's settings cannot be read. */
|
|
@@ -112,17 +118,25 @@ export function identityField(
|
|
|
112
118
|
);
|
|
113
119
|
}
|
|
114
120
|
|
|
115
|
-
/** `rules` (checked for shape only) and `
|
|
121
|
+
/** `rules` (checked for shape only), `talk` and `submission`, each null when absent. */
|
|
116
122
|
function rulesAndTalk(
|
|
117
123
|
json: Readonly<Record<string, unknown>>,
|
|
118
|
-
): Result<Pick<PaperSettings, "rules" | "talk">, string> {
|
|
124
|
+
): Result<Pick<PaperSettings, "rules" | "talk" | "submission">, string> {
|
|
119
125
|
const rules = json["rules"];
|
|
120
126
|
if (rules !== undefined && !isObject(rules) && !Array.isArray(rules))
|
|
121
127
|
return err(
|
|
122
128
|
`"rules" must be an object of rule id → severity, or a list of ESLint blocks`,
|
|
123
129
|
);
|
|
124
130
|
const talk = parsePaperTalk(json["talk"]);
|
|
125
|
-
|
|
131
|
+
if (!talk.ok) return talk;
|
|
132
|
+
const submission = parsePaperSubmission(json["submission"]);
|
|
133
|
+
return submission.ok
|
|
134
|
+
? ok({
|
|
135
|
+
rules: rules ?? null,
|
|
136
|
+
talk: talk.value,
|
|
137
|
+
submission: submission.value,
|
|
138
|
+
})
|
|
139
|
+
: submission;
|
|
126
140
|
}
|
|
127
141
|
|
|
128
142
|
/** The parsed JSON of `paperlint.json` → the settings, or one line saying what is wrong. Pure. */
|
|
@@ -232,7 +246,12 @@ function merge(
|
|
|
232
246
|
paper: PaperSettings | null,
|
|
233
247
|
): PaperSettings | null {
|
|
234
248
|
const r = root ?? NO_DEFAULTS;
|
|
235
|
-
const p = paper ?? {
|
|
249
|
+
const p = paper ?? {
|
|
250
|
+
...NO_DEFAULTS,
|
|
251
|
+
rules: null,
|
|
252
|
+
talk: null,
|
|
253
|
+
submission: null,
|
|
254
|
+
};
|
|
236
255
|
const merged: PaperSettings = {
|
|
237
256
|
extends: p.extends ?? r.extends,
|
|
238
257
|
kind: p.kind ?? r.kind,
|
|
@@ -240,6 +259,7 @@ function merge(
|
|
|
240
259
|
rules: p.rules,
|
|
241
260
|
identity: joined(r.identity, p.identity),
|
|
242
261
|
talk: p.talk,
|
|
262
|
+
submission: p.submission,
|
|
243
263
|
};
|
|
244
264
|
return paper === null && Object.values(merged).every((v) => v === null)
|
|
245
265
|
? null
|
package/src/parse-args.test.ts
CHANGED
|
@@ -35,6 +35,25 @@ describe("parseArgs — `--flag=value` is the same as `--flag value`", () => {
|
|
|
35
35
|
});
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
describe("parseArgs — the switches, each turning one field on", () => {
|
|
39
|
+
it.each([
|
|
40
|
+
["--json", "json"],
|
|
41
|
+
["--fix", "fix"],
|
|
42
|
+
["--all", "all"],
|
|
43
|
+
["--dry-run", "dryRun"],
|
|
44
|
+
["--check", "check"],
|
|
45
|
+
["--yes", "yes"],
|
|
46
|
+
["-y", "yes"],
|
|
47
|
+
["--no-hooks", "noHooks"],
|
|
48
|
+
["--allow-venue-name", "allowVenueName"],
|
|
49
|
+
] as const)("%s → %s", (flag, key) => {
|
|
50
|
+
const off = parseArgs(["new", "x"]);
|
|
51
|
+
const on = parseArgs(["new", "x", flag]);
|
|
52
|
+
expect(off[key]).toBe(false);
|
|
53
|
+
expect(on).toEqual({ ...off, [key]: true });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
38
57
|
describe("an unknown flag is refused, not read as a path", () => {
|
|
39
58
|
it("exits 2 and names the flag", async () => {
|
|
40
59
|
const err: string[] = [];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A conference portal holding a paper's submission — the port `paperlint submission` talks through.
|
|
3
|
+
* Shaped by what the command needs (read one submission, send one change), not by any portal's API;
|
|
4
|
+
* one adapter per portal kind (`src/adapters/hotcrp/`).
|
|
5
|
+
*/
|
|
6
|
+
import type { Result } from "../domain/result.ts";
|
|
7
|
+
import type {
|
|
8
|
+
PortalFailure,
|
|
9
|
+
SubmissionChange,
|
|
10
|
+
SubmissionView,
|
|
11
|
+
UpdateOutcome,
|
|
12
|
+
} from "../domain/submission.ts";
|
|
13
|
+
|
|
14
|
+
export interface SubmissionPortal {
|
|
15
|
+
/** The submission as the portal holds it now. */
|
|
16
|
+
show(id: number): Promise<Result<SubmissionView, PortalFailure>>;
|
|
17
|
+
/**
|
|
18
|
+
* Send a change. Without `save` the portal only checks it (a dry run) and keeps nothing; an
|
|
19
|
+
* answer the portal gave — valid or not — is an outcome, not a failure.
|
|
20
|
+
*/
|
|
21
|
+
update(
|
|
22
|
+
id: number,
|
|
23
|
+
change: SubmissionChange,
|
|
24
|
+
o: { readonly save: boolean },
|
|
25
|
+
): Promise<Result<UpdateOutcome, PortalFailure>>;
|
|
26
|
+
}
|
package/src/presets.test.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
MAX_PRESET_DEPTH,
|
|
17
17
|
SHIPPED_PREFIX,
|
|
18
18
|
shippedPresets,
|
|
19
|
+
shippedVenueNames,
|
|
19
20
|
labelOf,
|
|
20
21
|
presetProblemText,
|
|
21
22
|
resolvePreset,
|
|
@@ -297,3 +298,24 @@ describe("labelOf — display only, never used to resolve", () => {
|
|
|
297
298
|
expect(labelOf(spec)).toBe(label);
|
|
298
299
|
});
|
|
299
300
|
});
|
|
301
|
+
|
|
302
|
+
describe("shippedVenueNames — what each shipped venue is called", () => {
|
|
303
|
+
it("every shipped preset, by its label and the names along its chain", () => {
|
|
304
|
+
const names = shippedVenueNames(deps());
|
|
305
|
+
expect(names.map((n) => n.label)).toEqual(shippedPresets(VENUES));
|
|
306
|
+
expect(names.find((n) => n.label === "aisec")?.aliases).toEqual(
|
|
307
|
+
expect.arrayContaining(["AISec", "ACM CCS"]),
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("a preset that does not resolve is left out", () => {
|
|
312
|
+
const files = memoryFiles(
|
|
313
|
+
Object.fromEntries(
|
|
314
|
+
Object.entries(shipped).filter(([f]) => !f.endsWith("realm.jsonc")),
|
|
315
|
+
),
|
|
316
|
+
);
|
|
317
|
+
expect(
|
|
318
|
+
shippedVenueNames({ files, venuesDir: VENUES }).map((n) => n.label),
|
|
319
|
+
).toEqual(shippedPresets(VENUES).filter((n) => n !== "realm"));
|
|
320
|
+
});
|
|
321
|
+
});
|