paperlint 6.2.2 → 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/skills/verify-citations/SKILL.md +8 -4
- package/skills/verify-citations/SKILL.md.spec.ts +7 -3
- package/skills/verify-citations/scripts/verify-cites.mjs +23 -5
- package/skills/verify-citations/scripts/verify-cites.test.mjs +76 -5
- 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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `paper/folder-venue-leftover` — a paper folder named after a venue the paper no longer targets.
|
|
3
|
+
*
|
|
4
|
+
* A folder named `aisec-2026` reads as "the AISec paper" for as long as it exists; once the paper is
|
|
5
|
+
* rejected and goes to another venue, the name is wrong and nothing says so. paperlint already knows
|
|
6
|
+
* the venue a paper targets — its `paperlint.json` `extends` — and the names of every venue it ships
|
|
7
|
+
* a preset for, so the stale case costs one comparison. The sibling of `tex/venue-leftover`, which
|
|
8
|
+
* does the same for the text of the paper.
|
|
9
|
+
*
|
|
10
|
+
* ── WHAT IT READS ────────────────────────────────────────────────────────────────
|
|
11
|
+
* The folder's name, as the path module gives it (the directory of the linted file), its words
|
|
12
|
+
* against each shipped venue's label and `aliases` (`src/domain/venue-name.ts`). The venues are
|
|
13
|
+
* `otherVenues` — the presets not on this paper's own chain, the same list `tex/venue-leftover`
|
|
14
|
+
* reads — and the paper's own label and aliases are never reported.
|
|
15
|
+
*
|
|
16
|
+
* ── WHICH FILE IT REPORTS ON ─────────────────────────────────────────────────────
|
|
17
|
+
* The folder itself is not a file ESLint lints. `PIPELINE-STATUS.md` is: every paper folder has one
|
|
18
|
+
* (`paperlint lint` requires it, `paperlint new` writes it), whatever the paper's source format, so
|
|
19
|
+
* the finding is made once per paper, on that file's first line, where a disable directive can
|
|
20
|
+
* also sit.
|
|
21
|
+
*
|
|
22
|
+
* ── WHO SPEAKS WHEN THERE IS NOTHING TO JUDGE ───────────────────────────────────
|
|
23
|
+
* No `paperlint.json`, no `extends`, or one that does not resolve: silent. With no venue chosen
|
|
24
|
+
* there is no "other" venue, and `pdf/measured` and `pdf/profile` already speak about the rest. An
|
|
25
|
+
* `extends` that names only a template family is silent too (`isFamily`).
|
|
26
|
+
*/
|
|
27
|
+
import { basename, dirname } from "node:path";
|
|
28
|
+
import { venuesNamedBy, type NamedIn } from "./domain/venue-name.ts";
|
|
29
|
+
import { paperPreset, type Preset } from "./presets.ts";
|
|
30
|
+
import { otherVenues, type OtherVenue } from "./tex-venue-rules.ts";
|
|
31
|
+
import type { Finding, VenueRuleDeps } from "./venue-rules.ts";
|
|
32
|
+
|
|
33
|
+
/** The file the rule reports on: one per paper folder. */
|
|
34
|
+
const STATUS_FILE = "PIPELINE-STATUS.md";
|
|
35
|
+
|
|
36
|
+
/** The slice of ESLint's rule context this rule uses: the file, its first line, and `report`. */
|
|
37
|
+
export interface FolderRuleContext {
|
|
38
|
+
readonly filename: string;
|
|
39
|
+
readonly sourceCode: {
|
|
40
|
+
getLocFromIndex(index: number): Readonly<{ line: number; column: number }>;
|
|
41
|
+
};
|
|
42
|
+
report(d: {
|
|
43
|
+
readonly loc: {
|
|
44
|
+
readonly start: { line: number; column: number };
|
|
45
|
+
readonly end: { line: number; column: number };
|
|
46
|
+
};
|
|
47
|
+
readonly messageId: string;
|
|
48
|
+
readonly data: Readonly<Record<string, string | number>>;
|
|
49
|
+
}): void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface FolderRuleModule {
|
|
53
|
+
readonly meta: {
|
|
54
|
+
readonly type: "suggestion";
|
|
55
|
+
readonly docs: { readonly description: string; readonly url: string };
|
|
56
|
+
readonly schema: readonly object[];
|
|
57
|
+
readonly messages: Readonly<Record<string, string>>;
|
|
58
|
+
};
|
|
59
|
+
create(context: FolderRuleContext): { root?: () => void };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A venue as the message names it: its label, and the alias when the folder used another name. */
|
|
63
|
+
const shown = (n: NamedIn): string =>
|
|
64
|
+
n.name.toLowerCase() === n.venue.toLowerCase()
|
|
65
|
+
? n.venue
|
|
66
|
+
: `${n.venue} («${n.name}»)`;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Each other venue the folder name names, as a finding. Pure: the folder's name, the paper's
|
|
70
|
+
* resolved preset, and the other shipped venues.
|
|
71
|
+
*/
|
|
72
|
+
export function judgeFolder(
|
|
73
|
+
folder: string,
|
|
74
|
+
preset: Pick<Preset, "label" | "aliases">,
|
|
75
|
+
others: readonly OtherVenue[],
|
|
76
|
+
): readonly Finding[] {
|
|
77
|
+
return venuesNamedBy(folder, others, [preset.label, ...preset.aliases]).map(
|
|
78
|
+
(n) => ({
|
|
79
|
+
messageId: "leftover",
|
|
80
|
+
data: { folder, other: shown(n), venue: preset.label },
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A template family (`acm-sigconf`, `ieee-conference`): a preset with no kinds, which a paper for a
|
|
87
|
+
* venue paperlint has no preset for extends directly (`src/venue-rules.ts` reads it the same way).
|
|
88
|
+
* Such a paper declares a format, not a venue, so a venue in its folder name may be its own: the
|
|
89
|
+
* accepted ACSAC papers in `fixtures/accepted-papers/` extend `ieee-conference` from folders named
|
|
90
|
+
* `…-acsac24`, and ACSAC is the parent conference `aidc` names as an alias.
|
|
91
|
+
*/
|
|
92
|
+
const isFamily = (preset: Preset): boolean => preset.format.kinds.size === 0;
|
|
93
|
+
|
|
94
|
+
/** The findings for the paper folder `dir`; none when it has no resolved venue, or only a family. */
|
|
95
|
+
function findingsFor(dir: string, deps: VenueRuleDeps): readonly Finding[] {
|
|
96
|
+
const p = paperPreset(dir, deps);
|
|
97
|
+
if (p.kind !== "resolved" || isFamily(p.preset)) return [];
|
|
98
|
+
const others = otherVenues(p.preset, deps);
|
|
99
|
+
return judgeFolder(basename(dir), p.preset, others);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const META: FolderRuleModule["meta"] = {
|
|
103
|
+
type: "suggestion",
|
|
104
|
+
docs: {
|
|
105
|
+
description:
|
|
106
|
+
"the paper folder's name names a shipped venue other than the one the paper extends — a name left from an earlier submission",
|
|
107
|
+
url: "https://github.com/zernie/paperlint/blob/main/docs/rules/paper/folder-venue-leftover.md",
|
|
108
|
+
},
|
|
109
|
+
schema: [],
|
|
110
|
+
messages: {
|
|
111
|
+
leftover:
|
|
112
|
+
"the folder name «{{folder}}» names the venue {{other}}, and this paper extends {{venue}} — the name went stale when the venue changed, and venues change on every resubmission. Name the folder after the work (what the paper shows), not the venue",
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** The rule, reading the paper's settings and the shipped presets through `deps`. */
|
|
117
|
+
export function folderVenueRules(
|
|
118
|
+
deps: VenueRuleDeps,
|
|
119
|
+
): Readonly<Record<"folder-venue-leftover", FolderRuleModule>> {
|
|
120
|
+
return {
|
|
121
|
+
"folder-venue-leftover": {
|
|
122
|
+
meta: META,
|
|
123
|
+
create(context) {
|
|
124
|
+
if (basename(context.filename) !== STATUS_FILE) return {};
|
|
125
|
+
return {
|
|
126
|
+
root() {
|
|
127
|
+
const at = context.sourceCode.getLocFromIndex(0);
|
|
128
|
+
findingsFor(dirname(context.filename), deps).forEach((f) => {
|
|
129
|
+
context.report({ loc: { start: at, end: at }, ...f });
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The level it is on at in paperlint's own config, for every paper's `PIPELINE-STATUS.md`. */
|
|
139
|
+
export const FOLDER_VENUE_RULE_LEVELS: Readonly<
|
|
140
|
+
Record<"paper/folder-venue-leftover", "warn">
|
|
141
|
+
> = { "paper/folder-venue-leftover": "warn" };
|
package/src/init.test.ts
CHANGED
|
@@ -37,6 +37,9 @@ import {
|
|
|
37
37
|
workflowYaml,
|
|
38
38
|
} from "./init.ts";
|
|
39
39
|
import { runNode } from "../test/support.ts";
|
|
40
|
+
import { nodeFiles } from "./adapters/node/index.ts";
|
|
41
|
+
import { presetsDir } from "./package-dirs.ts";
|
|
42
|
+
import { shippedVenueNames } from "./presets.ts";
|
|
40
43
|
|
|
41
44
|
const dirs: string[] = [];
|
|
42
45
|
afterEach(() => {
|
|
@@ -515,6 +518,25 @@ describe("paperlint init — a first paper", () => {
|
|
|
515
518
|
});
|
|
516
519
|
});
|
|
517
520
|
|
|
521
|
+
describe("paperlint init — a first paper named after a venue", () => {
|
|
522
|
+
it("--paper aisec-2026 is refused before anything is asked, and createPaper is not called", async () => {
|
|
523
|
+
const made: string[] = [];
|
|
524
|
+
const asked: string[] = [];
|
|
525
|
+
const r = await initRun(tree({}), {
|
|
526
|
+
interactive: true,
|
|
527
|
+
paper: "aisec-2026",
|
|
528
|
+
venues: shippedVenueNames({ files: nodeFiles, venuesDir: presetsDir() }),
|
|
529
|
+
ask: (q) => Promise.resolve((asked.push(q), "")),
|
|
530
|
+
createPaper: (_root, name) => Promise.resolve((made.push(name), 0)),
|
|
531
|
+
});
|
|
532
|
+
expect(made).toEqual([]);
|
|
533
|
+
expect(asked.filter((q) => /format/.test(q))).toEqual([]);
|
|
534
|
+
expect(r.out[r.out.indexOf("first paper") + 1]).toBe(
|
|
535
|
+
' ✗ no paper created — `aisec-2026` — the paper folder names a venue ("aisec"); venues change on resubmission, so name it after the work\n if the venue really belongs in the name, rerun `paperlint new` with --allow-venue-name',
|
|
536
|
+
);
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
|
|
518
540
|
describe("paperlint init — paths are shown relative to where it was run", () => {
|
|
519
541
|
it("a path that IS the working directory is shown whole, not as an empty string", async () => {
|
|
520
542
|
const root = tree({ "papers/a/paper.md": "# a\n" });
|
package/src/init.ts
CHANGED
|
@@ -79,6 +79,7 @@ import {
|
|
|
79
79
|
} from "#lib/paper-config";
|
|
80
80
|
import { messageOf } from "./domain/text.ts";
|
|
81
81
|
import { isRecord } from "./domain/record.ts";
|
|
82
|
+
import type { NamedVenue } from "./domain/venue-name.ts";
|
|
82
83
|
|
|
83
84
|
/** How the papers directory was arrived at. Printed, because a guess must not read as a fact. */
|
|
84
85
|
export type PapersHow =
|
|
@@ -582,6 +583,11 @@ export interface InitOptions {
|
|
|
582
583
|
name: string,
|
|
583
584
|
format: PaperFormat,
|
|
584
585
|
) => Promise<number>;
|
|
586
|
+
/**
|
|
587
|
+
* The venues a paper name may not name (`shippedVenueNames`), so `--paper aisec-2026` is refused
|
|
588
|
+
* before anything is asked. `createPaper` refuses it again whatever this holds.
|
|
589
|
+
*/
|
|
590
|
+
venues?: readonly NamedVenue[];
|
|
585
591
|
run?: ProgramProbe;
|
|
586
592
|
/**
|
|
587
593
|
* What `paperlint lint` would resolve from the declaration, asked of the CLI's OWN reader. A second
|
|
@@ -713,6 +719,7 @@ export async function init(
|
|
|
713
719
|
paper = null,
|
|
714
720
|
format = null,
|
|
715
721
|
createPaper,
|
|
722
|
+
venues = [],
|
|
716
723
|
run = spawnSync,
|
|
717
724
|
resolveCliPapers,
|
|
718
725
|
link = (r: string) => linkSkills(r),
|
|
@@ -816,7 +823,7 @@ export async function init(
|
|
|
816
823
|
wanted = answer ? answer : null;
|
|
817
824
|
}
|
|
818
825
|
if (wanted !== null && createPaper) {
|
|
819
|
-
const problem = nameProblem(wanted);
|
|
826
|
+
const problem = nameProblem(wanted, venues);
|
|
820
827
|
let fmt: PaperFormat = format ?? DEFAULT_FORMAT;
|
|
821
828
|
if (!problem && format === null && interactive) {
|
|
822
829
|
const f = (
|
|
@@ -825,7 +832,7 @@ export async function init(
|
|
|
825
832
|
if (isFormat(f)) fmt = f;
|
|
826
833
|
}
|
|
827
834
|
if (problem) {
|
|
828
|
-
log(` ✗
|
|
835
|
+
log(` ✗ no paper created — ${problem}`);
|
|
829
836
|
paperCode = 2;
|
|
830
837
|
} else {
|
|
831
838
|
const code = await createPaper(papersAbs, wanted, fmt);
|
|
@@ -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", () => {
|