paperlint 6.2.3 → 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 +10 -3
- 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/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 +4 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +126 -37
- 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/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/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/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/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.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/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/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/latex.d.ts +7 -1
- package/dist/ports/latex.d.ts.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/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 +43 -2
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +130 -63
- 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 +36 -3
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +30 -2
- package/dist/tex-requirements.js.map +1 -1
- package/dist/tex-venue-rules.d.ts +7 -8
- 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 +30 -11
- package/docs/optional-rules.md +4 -3
- package/docs/rules/format/page-limit.md +1 -1
- 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/talk/profile.md +1 -1
- package/docs/rules/tex/heading-case.md +424 -0
- package/docs/rules.md +64 -53
- package/docs/skills.md +5 -4
- package/docs/submission.md +64 -0
- package/docs/talk.md +1 -1
- package/eslint-rules/latex-language.test.mjs +12 -0
- package/eslint-rules/latex-language.ts +17 -4
- package/eslint-rules/paper-typography.test.mjs +12 -0
- package/eslint.config.mjs +59 -1
- package/fixtures/build-e2e/acmart/paper.tex +1 -1
- package/fixtures/build-e2e/fallback/expect.json +3 -3
- package/lib/paper-config.ts +6 -1
- package/lib/skill-checks.d.mts +7 -0
- package/lib/skill-checks.mjs +31 -0
- package/lib/skill-trigger-cases.mjs +11 -0
- package/package.json +8 -2
- 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 +12 -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 +57 -2
- 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/paper-pipeline.harness.mjs +4 -0
- package/skills/paper-pipeline/references/acl-venue-rules.md +1 -1
- package/skills/study-accepted-papers/SKILL.md +3 -3
- package/skills/study-accepted-papers/SKILL.md.spec.ts +2 -2
- package/skills/submission-portal/SKILL.md +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 +27 -22
- package/skills/submit-paper/SKILL.md.spec.ts +26 -21
- package/skills/submit-paper/references/publishers/acm.md +1 -1
- 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/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 +155 -3
- package/src/cli.ts +161 -25
- package/src/domain/heading-case.test.ts +660 -0
- package/src/domain/heading-case.ts +538 -0
- 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/tex-document.ts +36 -0
- package/src/domain/venue-name.test.ts +121 -0
- package/src/domain/venue-name.ts +129 -0
- package/src/facts-file.test.ts +4 -4
- package/src/facts-file.ts +6 -6
- package/src/folder-venue-rule.test.ts +182 -0
- package/src/folder-venue-rule.ts +141 -0
- package/src/heading-case.test.ts +500 -0
- package/src/heading-case.ts +275 -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 +135 -10
- package/src/new-paper.ts +38 -7
- package/src/paper-settings-commands.test.ts +25 -10
- 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/latex.ts +7 -0
- package/src/ports/submission-portal.ts +26 -0
- package/src/preset-card-rule.test.ts +266 -0
- package/src/preset-card-rule.ts +384 -0
- package/src/presets.test.ts +212 -37
- package/src/presets.ts +183 -71
- package/src/rule-cases.test.ts +147 -42
- package/src/rules-config.ts +1 -1
- package/src/submission.test.ts +497 -0
- package/src/submission.ts +311 -0
- 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 +78 -8
- package/src/tex-venue-rules.test.ts +36 -22
- package/src/tex-venue-rules.ts +14 -26
- package/src/types.ts +13 -0
- 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
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE RULE THAT JUDGES THE CASE OF TITLES — `tex/heading-case`, over the paper's `\title` and the
|
|
3
|
+
* titles of `\section`, `\subsection`, `\subsubsection`, `\paragraph` and `\subparagraph`
|
|
4
|
+
* (starred or not, and each optional short title).
|
|
5
|
+
*
|
|
6
|
+
* tex/heading-case off unless turned on every word of a title is in the case its scope asks:
|
|
7
|
+
* { title, headings, levels? }, each a CaseStyle
|
|
8
|
+
*
|
|
9
|
+
* ── WHY IT IS AN OPTION AND NOT A DEFAULT ────────────────────────────────────────
|
|
10
|
+
* Which style a venue asks is the venue's, and venues name different authorities: Conference
|
|
11
|
+
* Publishing (AgenticDev's proceedings) quotes the Chicago Manual of Style, ACL links APA title case,
|
|
12
|
+
* IEEE's manual has its own list. So the rule is optional, like `pdf/last-page-balance`: a venue
|
|
13
|
+
* preset whose author kit was read turns it on in its `rules` — today only `presets/agenticdev.jsonc`
|
|
14
|
+
* — and a paper may turn it on in its own `paperlint.json`.
|
|
15
|
+
*
|
|
16
|
+
* ── THE OPTIONS ARE PARSED ONCE, WHEN ESLINT LOADS THE RULE ──────────────────────
|
|
17
|
+
* ESLint checks the SHAPE against `meta.schema`: `title` and `headings` required, no other key, no
|
|
18
|
+
* unknown level. The VALUES are judged by `parseOptions`, because ajv's enum message («should be
|
|
19
|
+
* equal to one of the allowed values») names none of them: a misspelt style, the retired words
|
|
20
|
+
* `headline` and `off`, and `any` for both scopes each throw from `create`, which ESLint reports as
|
|
21
|
+
* «Error while loading rule» and stops the run. The judge then gets `HeadingCaseOptions` and never
|
|
22
|
+
* looks at the raw object again. A bare severity (`"error"`, no options) passes ESLint's schema —
|
|
23
|
+
* it validates the options it is given, not their count — so it is reported once, at the top of the
|
|
24
|
+
* file, rather than passing as a rule that checked nothing.
|
|
25
|
+
*
|
|
26
|
+
* ── ONE FILE AT A TIME ───────────────────────────────────────────────────────────
|
|
27
|
+
* The rule reads the file ESLint hands it, like `paper/section-word`: it runs on `paper.tex` and on
|
|
28
|
+
* every file `paper.tex` includes from its body (the `**` block of `src/cli.ts`), so a heading in
|
|
29
|
+
* `sections/results.tex` is reported at that file and line, and `--fix` edits that file. The
|
|
30
|
+
* preamble is read too: that is where `\title` stands.
|
|
31
|
+
*
|
|
32
|
+
* What a word is asked is decided in `src/domain/heading-case.ts`.
|
|
33
|
+
*/
|
|
34
|
+
import {
|
|
35
|
+
CASE_STYLES,
|
|
36
|
+
HEADING_LEVELS,
|
|
37
|
+
judgeHeadingCase,
|
|
38
|
+
type CaseFinding,
|
|
39
|
+
type CaseStyle,
|
|
40
|
+
type HeadingCaseOptions,
|
|
41
|
+
} from "./domain/heading-case.ts";
|
|
42
|
+
import { fieldOf, isRecord } from "./domain/record.ts";
|
|
43
|
+
import type { HeadingLevel } from "./domain/tex-document.ts";
|
|
44
|
+
import type { LatexReader } from "./ports/latex.ts";
|
|
45
|
+
import { ruleDocsUrl } from "./tex-venue-rules.ts";
|
|
46
|
+
|
|
47
|
+
/** What the rule reads with: the LaTeX reader. */
|
|
48
|
+
export interface HeadingCaseDeps {
|
|
49
|
+
readonly latex: LatexReader;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type Loc = { readonly line: number; readonly column: number };
|
|
53
|
+
|
|
54
|
+
/** The slice of ESLint's rule context this rule uses. `raw` exists on the `.tex` language's. */
|
|
55
|
+
export interface HeadingCaseContext {
|
|
56
|
+
readonly options: readonly unknown[];
|
|
57
|
+
readonly sourceCode: {
|
|
58
|
+
readonly text: string;
|
|
59
|
+
readonly raw?: string;
|
|
60
|
+
getLocFromIndex(index: number): Loc;
|
|
61
|
+
};
|
|
62
|
+
report(d: {
|
|
63
|
+
readonly loc: { readonly start: Loc; readonly end: Loc };
|
|
64
|
+
readonly messageId: string;
|
|
65
|
+
readonly data?: Readonly<Record<string, string>>;
|
|
66
|
+
readonly fix?: (fixer: {
|
|
67
|
+
replaceTextRange(range: readonly [number, number], text: string): unknown;
|
|
68
|
+
}) => unknown;
|
|
69
|
+
}): void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface HeadingCaseRule {
|
|
73
|
+
readonly meta: {
|
|
74
|
+
readonly type: "suggestion";
|
|
75
|
+
readonly fixable: "code";
|
|
76
|
+
readonly docs: { readonly description: string; readonly url: string };
|
|
77
|
+
readonly schema: readonly object[];
|
|
78
|
+
readonly messages: Readonly<Record<string, string>>;
|
|
79
|
+
};
|
|
80
|
+
create(context: HeadingCaseContext): { root?: () => void };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const DOCS_URL = ruleDocsUrl("heading-case");
|
|
84
|
+
|
|
85
|
+
/** The rule's options as ESLint gave them: a configuration, none at all, or one it refuses. */
|
|
86
|
+
type Asked =
|
|
87
|
+
| { readonly kind: "options"; readonly options: HeadingCaseOptions }
|
|
88
|
+
| { readonly kind: "none" }
|
|
89
|
+
| { readonly kind: "refused"; readonly reason: string };
|
|
90
|
+
|
|
91
|
+
const refused = (reason: string): Asked => ({
|
|
92
|
+
kind: "refused",
|
|
93
|
+
reason: `${reason} — ${DOCS_URL}`,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const quoted = (xs: readonly string[]): string =>
|
|
97
|
+
xs.map((x) => JSON.stringify(x)).join(", ");
|
|
98
|
+
|
|
99
|
+
/** One style value, or why it is none; `key` is how the message names where it stands. */
|
|
100
|
+
function styleOf(
|
|
101
|
+
key: string,
|
|
102
|
+
v: unknown,
|
|
103
|
+
): CaseStyle | { readonly why: string } {
|
|
104
|
+
const style = CASE_STYLES.find((s) => s === v);
|
|
105
|
+
if (style !== undefined) return style;
|
|
106
|
+
if (v === "headline")
|
|
107
|
+
return {
|
|
108
|
+
why: `"${key}" is "headline", which names no single style: write "chicago-headline" for the Chicago Manual of Style headline style. Other headline variants (APA title case, IEEE, Springer) are not implemented yet, and they disagree with Chicago on words such as With, Between and From`,
|
|
109
|
+
};
|
|
110
|
+
if (v === "off")
|
|
111
|
+
return {
|
|
112
|
+
why: `"${key}" is "off", which is not a style: write "any" to leave this scope unconstrained, or turn the whole rule off with "tex/heading-case": "off"`,
|
|
113
|
+
};
|
|
114
|
+
const shown = v === undefined ? "missing" : JSON.stringify(v);
|
|
115
|
+
return {
|
|
116
|
+
why: `"${key}" is ${shown}, which is not a style: one of ${quoted(CASE_STYLES)}`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
type Levels = Readonly<Partial<Record<HeadingLevel, CaseStyle>>>;
|
|
121
|
+
|
|
122
|
+
/** The `levels` object: the levels it names, each with its style, or why it is refused. */
|
|
123
|
+
function levelsOf(v: unknown): Levels | { readonly why: string } {
|
|
124
|
+
if (v === undefined) return {};
|
|
125
|
+
if (!isRecord(v))
|
|
126
|
+
return {
|
|
127
|
+
why: `"levels" is ${JSON.stringify(v)}, not an object such as {"paragraph": "sentence"}`,
|
|
128
|
+
};
|
|
129
|
+
return HEADING_LEVELS.reduce<Levels | { readonly why: string }>(
|
|
130
|
+
(acc, level) => {
|
|
131
|
+
if ("why" in acc || !(level in v)) return acc;
|
|
132
|
+
const style = styleOf(`levels.${level}`, v[level]);
|
|
133
|
+
return typeof style === "string" ? { ...acc, [level]: style } : style;
|
|
134
|
+
},
|
|
135
|
+
{},
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Whether every scope is `any`: the rule turned off, said in a second way. */
|
|
140
|
+
const asksNothing = (
|
|
141
|
+
o: HeadingCaseOptions & { readonly levels: Levels },
|
|
142
|
+
): boolean =>
|
|
143
|
+
o.title === "any" &&
|
|
144
|
+
o.headings === "any" &&
|
|
145
|
+
Object.values(o.levels).every((s) => s === "any");
|
|
146
|
+
|
|
147
|
+
/** ESLint's options for the rule → what it is asked, parsed once. */
|
|
148
|
+
function parseOptions(options: readonly unknown[]): Asked {
|
|
149
|
+
const [raw] = options;
|
|
150
|
+
if (raw === undefined) return { kind: "none" };
|
|
151
|
+
if (!isRecord(raw))
|
|
152
|
+
return refused(
|
|
153
|
+
`the options are ${JSON.stringify(raw)}, not an object such as {"title": "chicago-headline", "headings": "chicago-headline"}`,
|
|
154
|
+
);
|
|
155
|
+
const title = styleOf("title", fieldOf(raw, "title"));
|
|
156
|
+
if (typeof title !== "string") return refused(title.why);
|
|
157
|
+
const headings = styleOf("headings", fieldOf(raw, "headings"));
|
|
158
|
+
if (typeof headings !== "string") return refused(headings.why);
|
|
159
|
+
const levels = levelsOf(fieldOf(raw, "levels"));
|
|
160
|
+
if ("why" in levels) return refused(levels.why);
|
|
161
|
+
const parsed = { title, headings, levels };
|
|
162
|
+
return asksNothing(parsed)
|
|
163
|
+
? refused(
|
|
164
|
+
`"title" and "headings" are both "any" and no level asks for more: that is the rule turned off — remove it from "rules", or set it to "off"`,
|
|
165
|
+
)
|
|
166
|
+
: { kind: "options", options: parsed };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const MESSAGES: Readonly<Record<string, string>> = {
|
|
170
|
+
capitalize:
|
|
171
|
+
"{{where}}: capitalize «{{word}}» → «{{expected}}» — {{why}} (chicago-headline)",
|
|
172
|
+
lowercase:
|
|
173
|
+
"{{where}}: lowercase «{{word}}» → «{{expected}}» — {{why}} (chicago-headline)",
|
|
174
|
+
sentenceFirst:
|
|
175
|
+
"{{where}}: capitalize the first word, «{{word}}» → «{{expected}}» (sentence; not fixed: a product name written in lowercase looks the same)",
|
|
176
|
+
noOptions: `tex/heading-case is on with no options, so it judged nothing: give it the styles the venue asks, e.g. ["error", {"title": "chicago-headline", "headings": "chicago-headline"}] (styles: ${quoted(CASE_STYLES)})`,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/** One finding reported where its word stands, with the fix when the judge is sure of it. */
|
|
180
|
+
function report(context: HeadingCaseContext, f: CaseFinding): void {
|
|
181
|
+
const sc = context.sourceCode;
|
|
182
|
+
const { fix } = f;
|
|
183
|
+
context.report({
|
|
184
|
+
loc: {
|
|
185
|
+
start: sc.getLocFromIndex(f.at.start),
|
|
186
|
+
end: sc.getLocFromIndex(f.at.end),
|
|
187
|
+
},
|
|
188
|
+
messageId: f.messageId,
|
|
189
|
+
data: f.data,
|
|
190
|
+
...(fix === null
|
|
191
|
+
? {}
|
|
192
|
+
: {
|
|
193
|
+
fix: (fixer) =>
|
|
194
|
+
fixer.replaceTextRange([fix.span.start, fix.span.end], fix.text),
|
|
195
|
+
}),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* A style value, as the schema checks it: a string. Which strings are styles is `parseOptions`'s to
|
|
201
|
+
* say, so that the refusal can name them (see the header).
|
|
202
|
+
*/
|
|
203
|
+
const STYLE = { type: "string" } as const;
|
|
204
|
+
|
|
205
|
+
const META: HeadingCaseRule["meta"] = {
|
|
206
|
+
type: "suggestion",
|
|
207
|
+
fixable: "code",
|
|
208
|
+
docs: {
|
|
209
|
+
description:
|
|
210
|
+
"the words of the paper's title and of its headings are in the capitalization the venue asks: chicago-headline or sentence",
|
|
211
|
+
url: DOCS_URL,
|
|
212
|
+
},
|
|
213
|
+
schema: [
|
|
214
|
+
{
|
|
215
|
+
type: "object",
|
|
216
|
+
properties: {
|
|
217
|
+
title: STYLE,
|
|
218
|
+
headings: STYLE,
|
|
219
|
+
levels: {
|
|
220
|
+
type: "object",
|
|
221
|
+
properties: Object.fromEntries(
|
|
222
|
+
HEADING_LEVELS.map((level) => [level, STYLE]),
|
|
223
|
+
),
|
|
224
|
+
additionalProperties: false,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
required: ["title", "headings"],
|
|
228
|
+
additionalProperties: false,
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
messages: MESSAGES,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/** The rule turned on with no options: said once, at the top of the file. */
|
|
235
|
+
function reportNoOptions(context: HeadingCaseContext): void {
|
|
236
|
+
const start = context.sourceCode.getLocFromIndex(0);
|
|
237
|
+
context.report({ loc: { start, end: start }, messageId: "noOptions" });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** One file against the options the rule was given. */
|
|
241
|
+
function judgeFile(
|
|
242
|
+
context: HeadingCaseContext,
|
|
243
|
+
deps: HeadingCaseDeps,
|
|
244
|
+
options: HeadingCaseOptions,
|
|
245
|
+
): void {
|
|
246
|
+
const sc = context.sourceCode;
|
|
247
|
+
const src = sc.raw ?? sc.text;
|
|
248
|
+
judgeHeadingCase(deps.latex.headings(src), options, src).forEach((f) => {
|
|
249
|
+
report(context, f);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The rule: its options are parsed when ESLint loads it, and a refused one stops the run. */
|
|
254
|
+
export const headingCaseRule = (deps: HeadingCaseDeps): HeadingCaseRule => ({
|
|
255
|
+
meta: META,
|
|
256
|
+
create: (context) => {
|
|
257
|
+
const asked = parseOptions(context.options);
|
|
258
|
+
switch (asked.kind) {
|
|
259
|
+
case "refused":
|
|
260
|
+
throw new Error(asked.reason);
|
|
261
|
+
case "none":
|
|
262
|
+
return {
|
|
263
|
+
root: () => {
|
|
264
|
+
reportNoOptions(context);
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
case "options":
|
|
268
|
+
return {
|
|
269
|
+
root: () => {
|
|
270
|
+
judgeFile(context, deps, asked.options);
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
});
|
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"),
|
|
@@ -169,6 +175,7 @@ async function newIn(args: string[], files: Record<string, string> = {}) {
|
|
|
169
175
|
|
|
170
176
|
describe("paperlint new --venue / --kind", () => {
|
|
171
177
|
const PRESET = `{
|
|
178
|
+
"type": "venue", "name": "my-workshop", "url": "https://example.org/cfp",
|
|
172
179
|
"extends": "paperlint:acm-sigconf",
|
|
173
180
|
"format": { "kinds": { "short": { "body_pages_max": 4 } } },
|
|
174
181
|
}
|
|
@@ -199,6 +206,7 @@ describe("paperlint new --venue / --kind", () => {
|
|
|
199
206
|
for (const template of [undefined, "\\documentclass[a]{}"]) {
|
|
200
207
|
const r = await newIn(["--venue", "./venues/bare.jsonc"], {
|
|
201
208
|
"venues/bare.jsonc": JSON.stringify({
|
|
209
|
+
type: "family",
|
|
202
210
|
...(template === undefined ? {} : { template }),
|
|
203
211
|
tex: { packages: { x: ["x.sty"] } },
|
|
204
212
|
}),
|
|
@@ -224,7 +232,9 @@ describe("paperlint new --venue / --kind", () => {
|
|
|
224
232
|
expect(r.out).not.toMatch(/pdf\/profile/);
|
|
225
233
|
expect(r.code).toBe(0);
|
|
226
234
|
});
|
|
235
|
+
});
|
|
227
236
|
|
|
237
|
+
describe("paperlint new --venue — a venue it cannot use", () => {
|
|
228
238
|
it("an unknown venue exits 2, lists the shipped presets, and creates nothing", async () => {
|
|
229
239
|
const r = await newIn(["--venue", "icse"]);
|
|
230
240
|
expect(r.code).toBe(2);
|
|
@@ -240,7 +250,7 @@ describe("paperlint new --venue / --kind — refusals and defaults", () => {
|
|
|
240
250
|
[
|
|
241
251
|
"a kind the preset lacks",
|
|
242
252
|
["--venue", "agenticdev", "--kind", "long"],
|
|
243
|
-
/`
|
|
253
|
+
/`AgenticDev` has no kind `long`; its kinds: short, full, demo/,
|
|
244
254
|
],
|
|
245
255
|
["a kind without a venue", ["--kind", "short"], /--kind needs --venue/],
|
|
246
256
|
[
|
|
@@ -264,7 +274,7 @@ describe("paperlint new --venue / --kind — refusals and defaults", () => {
|
|
|
264
274
|
const r = await newIn(["--venue", "agenticdev"]);
|
|
265
275
|
expect(r.settings()).toEqual({ extends: "paperlint:agenticdev" });
|
|
266
276
|
expect(r.out).toMatch(
|
|
267
|
-
/kind: not set — `
|
|
277
|
+
/kind: not set — `AgenticDev` sets a page limit per kind/,
|
|
268
278
|
);
|
|
269
279
|
expect(r.out).toMatch(/pdf\/profile/);
|
|
270
280
|
});
|
|
@@ -286,6 +296,117 @@ describe("paperlint new --venue / --kind — refusals and defaults", () => {
|
|
|
286
296
|
});
|
|
287
297
|
});
|
|
288
298
|
|
|
299
|
+
describe("paperlint new — a name that names a venue is refused", () => {
|
|
300
|
+
it.each<[string, string, string[], string]>([
|
|
301
|
+
[
|
|
302
|
+
"a venue's label, with the work's name left to suggest",
|
|
303
|
+
"aisec-agent-drift",
|
|
304
|
+
[],
|
|
305
|
+
'`aisec-agent-drift` — the paper folder names a venue ("AISec"); venues change on resubmission, so name it after the work (e.g. "agent-drift")',
|
|
306
|
+
],
|
|
307
|
+
[
|
|
308
|
+
"nothing but a year left: no example",
|
|
309
|
+
"aisec-2026",
|
|
310
|
+
[],
|
|
311
|
+
'`aisec-2026` — the paper folder names a venue ("AISec"); venues change on resubmission, so name it after the work',
|
|
312
|
+
],
|
|
313
|
+
[
|
|
314
|
+
"its own venue too: right today, stale on the next resubmission",
|
|
315
|
+
"aidc-2026",
|
|
316
|
+
["--venue", "aidc"],
|
|
317
|
+
'`aidc-2026` — the paper folder names a venue ("AIDC"); venues change on resubmission, so name it after the work',
|
|
318
|
+
],
|
|
319
|
+
[
|
|
320
|
+
"by an alias, glued to a year",
|
|
321
|
+
"acsac2026-rules",
|
|
322
|
+
[],
|
|
323
|
+
'`acsac2026-rules` — the paper folder names a venue ("ACSAC"); venues change on resubmission, so name it after the work',
|
|
324
|
+
],
|
|
325
|
+
[
|
|
326
|
+
"two venues",
|
|
327
|
+
"realm-aisec-rules",
|
|
328
|
+
[],
|
|
329
|
+
'`realm-aisec-rules` — the paper folder names a venue ("AISec", "REALM"); venues change on resubmission, so name it after the work (e.g. "rules")',
|
|
330
|
+
],
|
|
331
|
+
])("%s: exit 2, nothing written", async (_, name, args, reason) => {
|
|
332
|
+
const r = await newIn(args, {}, name);
|
|
333
|
+
expect(r.code).toBe(2);
|
|
334
|
+
expect(r.out).toBe(
|
|
335
|
+
` ✗ ${reason}\n if the venue really belongs in the name, rerun \`paperlint new\` with --allow-venue-name`,
|
|
336
|
+
);
|
|
337
|
+
expect(existsSync(r.dir)).toBe(false);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("an existing folder that names a venue gets no missing files either", async () => {
|
|
341
|
+
const r = await newIn(
|
|
342
|
+
[],
|
|
343
|
+
{ "papers/aisec-2026/paper.tex": "x" },
|
|
344
|
+
"aisec-2026",
|
|
345
|
+
);
|
|
346
|
+
expect(r.code).toBe(2);
|
|
347
|
+
expect(readdirSync(r.dir)).toEqual(["paper.tex"]);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it.each(["agent-rule-drift", "overrealm-realms"])(
|
|
351
|
+
"%s names no venue: created, as before",
|
|
352
|
+
async (name) => {
|
|
353
|
+
const r = await newIn([], {}, name);
|
|
354
|
+
expect(r.out).not.toMatch(/names a venue/);
|
|
355
|
+
expect(existsSync(join(r.dir, "PIPELINE-STATUS.md"))).toBe(true);
|
|
356
|
+
expect(r.code).toBe(0);
|
|
357
|
+
},
|
|
358
|
+
);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe("paperlint new --allow-venue-name", () => {
|
|
362
|
+
it("the folder is created as any other, and nothing is said about the venue", async () => {
|
|
363
|
+
const r = await newIn(["--allow-venue-name"], {}, "realm-of-agents");
|
|
364
|
+
expect(r.code).toBe(0);
|
|
365
|
+
expect(r.out).not.toMatch(/venue really belongs|names a venue/);
|
|
366
|
+
expect(readdirSync(r.dir).sort()).toEqual([
|
|
367
|
+
"PIPELINE-STATUS.md",
|
|
368
|
+
"paper.tex",
|
|
369
|
+
"paperlint.json",
|
|
370
|
+
]);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it.each<{
|
|
374
|
+
readonly what: string;
|
|
375
|
+
readonly name: string;
|
|
376
|
+
readonly args: readonly string[];
|
|
377
|
+
readonly files: Record<string, string>;
|
|
378
|
+
readonly says: RegExp;
|
|
379
|
+
}>([
|
|
380
|
+
{
|
|
381
|
+
what: "an invalid name",
|
|
382
|
+
name: "Realm",
|
|
383
|
+
args: [],
|
|
384
|
+
files: {},
|
|
385
|
+
says: /may hold only a-z/,
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
what: "an unknown preset",
|
|
389
|
+
name: "realm-of-agents",
|
|
390
|
+
args: ["--venue", "nope"],
|
|
391
|
+
files: {},
|
|
392
|
+
says: /no such venue preset/,
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
what: "an existing paperlint.json with --venue",
|
|
396
|
+
name: "realm-of-agents",
|
|
397
|
+
args: ["--venue", "aisec"],
|
|
398
|
+
files: {
|
|
399
|
+
"papers/realm-of-agents/paperlint.json": '{ "extends": null }\n',
|
|
400
|
+
},
|
|
401
|
+
says: /already exists and is never overwritten/,
|
|
402
|
+
},
|
|
403
|
+
])("skips no other refusal: $what", async ({ name, args, files, says }) => {
|
|
404
|
+
const r = await newIn(["--allow-venue-name", ...args], files, name);
|
|
405
|
+
expect(r.code).toBe(2);
|
|
406
|
+
expect(`${r.out}\n${r.err}`).toMatch(says);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
289
410
|
describe("chooseVenue — on a terminal", () => {
|
|
290
411
|
const at = (answers: string[]) => {
|
|
291
412
|
const asked: string[] = [];
|
|
@@ -340,6 +461,7 @@ describe("a project's own template", () => {
|
|
|
340
461
|
const src = join(papers, OVERRIDE_DIR, "paperlint.json");
|
|
341
462
|
writeFileSync(src, '{\n // our house venue\n "extends": null\n}\n');
|
|
342
463
|
const r = newPaper(papers, "p", "tex", {
|
|
464
|
+
venues: [],
|
|
343
465
|
venue: { extends: "paperlint:aisec", kind: null },
|
|
344
466
|
});
|
|
345
467
|
expect(r).toEqual({
|
|
@@ -353,7 +475,10 @@ describe("a project's own template", () => {
|
|
|
353
475
|
const papers = join(tmp(), "papers");
|
|
354
476
|
mkdirSync(join(papers, OVERRIDE_DIR), { recursive: true });
|
|
355
477
|
writeFileSync(join(papers, OVERRIDE_DIR, "paperlint.json"), "{}\n");
|
|
356
|
-
const lines = reportNewPaper(
|
|
478
|
+
const lines = reportNewPaper(
|
|
479
|
+
newPaper(papers, "p", "tex", { venues: [] }),
|
|
480
|
+
(p) => p,
|
|
481
|
+
);
|
|
357
482
|
expect(lines).toContain(
|
|
358
483
|
" + paperlint.json (from the project's .template/ template)",
|
|
359
484
|
);
|