paperlint 2.1.0 → 3.1.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/CLAUDE.md +6 -1
- package/README.md +243 -235
- package/action.yml +4 -6
- package/dist/adapters/references/index.d.ts +13 -0
- package/dist/adapters/references/index.d.ts.map +1 -0
- package/dist/adapters/references/index.js +55 -0
- package/dist/adapters/references/index.js.map +1 -0
- package/dist/adapters/references/reach.io.d.ts +3 -0
- package/dist/adapters/references/reach.io.d.ts.map +1 -0
- package/dist/adapters/references/reach.io.js +14 -0
- package/dist/adapters/references/reach.io.js.map +1 -0
- package/dist/build-engine.d.ts.map +1 -1
- package/dist/build-engine.js +6 -2
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +15 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +60 -7
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +94 -30
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +500 -288
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +40 -72
- package/dist/doctor.js.map +1 -1
- package/dist/engine.d.ts +9 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +27 -0
- package/dist/engine.js.map +1 -1
- package/dist/facts-file.d.ts +2 -2
- package/dist/facts-file.js +2 -2
- package/dist/hooks-settings.d.ts +0 -21
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +17 -135
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +39 -29
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +144 -173
- package/dist/init.js.map +1 -1
- package/dist/link-skills.d.ts +1 -1
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +11 -29
- package/dist/link-skills.js.map +1 -1
- package/dist/new-paper.d.ts +8 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +49 -25
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +85 -0
- package/dist/paper-files.d.ts.map +1 -0
- package/dist/paper-files.js +96 -0
- package/dist/paper-files.js.map +1 -0
- package/dist/paper-settings.d.ts +13 -41
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +71 -71
- package/dist/paper-settings.js.map +1 -1
- package/dist/ports/check-references.d.ts +31 -0
- package/dist/ports/check-references.d.ts.map +1 -0
- package/dist/ports/check-references.js +7 -0
- package/dist/ports/check-references.js.map +1 -0
- package/dist/presets.d.ts +1 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +4 -6
- package/dist/presets.js.map +1 -1
- package/dist/reference-rules.d.ts +47 -0
- package/dist/reference-rules.d.ts.map +1 -0
- package/dist/reference-rules.js +156 -0
- package/dist/reference-rules.js.map +1 -0
- package/dist/references.d.ts +36 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +91 -0
- package/dist/references.js.map +1 -0
- package/dist/rules-config.d.ts +5 -3
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +15 -6
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.js +3 -3
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.js +2 -2
- package/dist/tex-requirements.js.map +1 -1
- package/dist/toolchain.d.ts +5 -1
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +22 -5
- package/dist/toolchain.js.map +1 -1
- package/dist/types.d.ts +26 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +12 -10
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +163 -104
- package/docs/e2e.md +1 -1
- package/docs/install.md +25 -40
- package/docs/optional-rules.md +5 -4
- package/docs/rules.md +65 -24
- package/docs/skills.md +80 -0
- package/eslint-rules/bib-reachable-entry.mjs +75 -0
- package/eslint-rules/latex-language.mjs +29 -1
- package/eslint-rules/paper-stages.harness.mjs +1 -117
- package/eslint-rules/paper-stages.mjs +0 -120
- package/eslint-rules/paper-stages.mutations.mjs +0 -76
- package/eslint-rules/paper-typography.mjs +306 -191
- package/eslint-rules/paper-typography.test.mjs +337 -0
- package/eslint-rules/papers.harness.mjs +8 -12
- package/eslint-rules/papers.mjs +17 -31
- package/eslint-rules/pdf-last-page-balance.mjs +4 -7
- package/eslint-rules/review-frontmatter.mjs +118 -0
- package/eslint-rules/review-frontmatter.schema.json +36 -0
- package/eslint-rules/review-frontmatter.test.mjs +118 -0
- package/eslint-rules/sibling-frontmatter.mjs +19 -0
- package/eslint-rules/sibling-frontmatter.schema.json +14 -0
- package/eslint.config.mjs +0 -15
- package/fixtures/real-markdown-paper/baseline.json +6 -7
- package/fixtures/toolchain-mirror/catalog.txt +1 -0
- package/fixtures/toolchain-mirror/install-tl +4 -1
- package/fixtures/toolchain-mirror/stub-fmtutil-sys +4 -0
- package/fixtures/toolchain-mirror/stub-tlmgr +15 -0
- package/hooks/hooks.harness.mjs +39 -98
- package/hooks/hooks.mutations.mjs +6 -6
- package/hooks/paper-edit-guard.hook.d.mts +2 -3
- package/hooks/paper-edit-guard.hook.mjs +31 -42
- package/hooks/paper-skills-nudge.hook.mjs +21 -19
- package/hooks/paper-status-gates.hook.mjs +21 -19
- package/hooks/paper-status-gates.sh +8 -13
- package/lib/paper-config.d.mts +15 -25
- package/lib/paper-config.harness.mjs +18 -35
- package/lib/paper-config.mjs +83 -105
- package/lib/paper-config.mutations.mjs +2 -2
- package/lib/skill-checks.mjs +1 -1
- package/lib/skill-trigger-cases.harness.mjs +9 -8
- package/lib/skill-trigger-cases.mjs +7 -15
- package/package.json +1 -1
- package/scripts/eslint-report-guard.mjs +1 -1
- package/scripts/harness-api.frozen.json +0 -3
- package/scripts/mutation-batteries.frozen.json +5 -14
- package/skills/analyze-sibling-paper/SKILL.md +16 -12
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +15 -11
- package/skills/cold-read-diff/SKILL.md +20 -1
- package/skills/cold-read-diff/SKILL.md.spec.ts +19 -0
- package/skills/grade-paper-writing/SKILL.md +20 -1
- package/skills/grade-paper-writing/SKILL.md.spec.ts +19 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +0 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +8 -12
- package/skills/paper-pipeline/scripts/consumer.mjs +19 -33
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +6 -4
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +2 -11
- package/skills/plan-paper-timeline/SKILL.md +2 -2
- package/skills/plan-paper-timeline/SKILL.md.spec.ts +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +2 -2
- package/skills/render-paper/check-render.sh +2 -3
- package/skills/render-paper/render-paper.harness.mjs +2 -2
- package/skills/verify-citations/SKILL.md +10 -1
- package/skills/verify-citations/SKILL.md.spec.ts +9 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +48 -26
- package/src/adapters/references/index.ts +102 -0
- package/src/adapters/references/reach.io.ts +12 -0
- package/src/build-engine.ts +6 -1
- package/src/build.harness.mjs +54 -6
- package/src/build.ts +89 -11
- package/src/cli.harness.mjs +149 -294
- package/src/cli.mutations.mjs +35 -44
- package/src/cli.ts +637 -328
- package/src/doctor.harness.mjs +41 -84
- package/src/doctor.mutations.mjs +9 -9
- package/src/doctor.ts +44 -89
- package/src/engine.harness.mjs +50 -0
- package/src/engine.ts +35 -0
- package/src/facts-file.test.ts +0 -7
- package/src/facts-file.ts +2 -2
- package/src/hooks-settings.harness.mjs +7 -92
- package/src/hooks-settings.mutations.mjs +2 -17
- package/src/hooks-settings.ts +18 -162
- package/src/init.test.ts +206 -0
- package/src/init.ts +187 -231
- package/src/link-skills.harness.mjs +1 -38
- package/src/link-skills.mutations.mjs +9 -5
- package/src/link-skills.ts +10 -34
- package/src/lint-fix.test.ts +120 -0
- package/src/linted-set.test.ts +118 -0
- package/src/new-paper.harness.mjs +2 -0
- package/src/new-paper.mutations.mjs +0 -14
- package/src/new-paper.test.ts +173 -14
- package/src/new-paper.ts +82 -30
- package/src/paper-files.test.ts +395 -0
- package/src/paper-files.ts +200 -0
- package/src/paper-settings-commands.test.ts +121 -122
- package/src/paper-settings.test.ts +93 -64
- package/src/paper-settings.ts +99 -97
- package/src/parse-args.test.ts +48 -0
- package/src/ports/check-references.ts +28 -0
- package/src/presets.ts +5 -15
- package/src/reference-rules.ts +229 -0
- package/src/references.test.ts +209 -0
- package/src/references.ts +138 -0
- package/src/rules-config.harness.mjs +24 -8
- package/src/rules-config.ts +19 -7
- package/src/rules-off.test.ts +111 -0
- package/src/structure.harness.mjs +4 -2
- package/src/structure.ts +3 -3
- package/src/tex-requirements.ts +2 -2
- package/src/toolchain.harness.mjs +41 -6
- package/src/toolchain.ts +29 -5
- package/src/types.ts +36 -18
- package/src/venue-rules.test.ts +23 -11
- package/src/venue-rules.ts +12 -14
- package/templates/paper/paperlint.json +1 -1
- package/eslint-rules/doc-fields.harness.mjs +0 -336
- package/eslint-rules/doc-fields.mjs +0 -186
- package/eslint-rules/doc-fields.mutations.mjs +0 -96
- package/eslint-rules/paper-typography.harness.mjs +0 -291
- package/eslint-rules/paper-typography.mutations.mjs +0 -131
- package/eslint-rules/review-findings-cause.harness.mjs +0 -228
- package/eslint-rules/review-findings-cause.mjs +0 -135
- package/eslint-rules/review-findings-cause.mutations.mjs +0 -72
- package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +0 -16
- package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +0 -17
- package/fixtures/review-findings-cause/clean.md +0 -17
- package/fixtures/review-findings-cause/defect.md +0 -14
- package/fixtures/review-findings-cause/old-debt.md +0 -14
- package/fixtures/review-findings-cause/quiet-in-fence.md +0 -16
package/src/rules-config.ts
CHANGED
|
@@ -44,6 +44,7 @@ export type Parsed<T> =
|
|
|
44
44
|
| { readonly ok: false; readonly error: string };
|
|
45
45
|
|
|
46
46
|
const SEVERITIES: readonly unknown[] = ["off", "warn", "error", 0, 1, 2];
|
|
47
|
+
|
|
47
48
|
const BLOCK_KEYS = new Set(["files", "ignores", "rules"]);
|
|
48
49
|
|
|
49
50
|
const bad = <T>(error: string): Parsed<T> => ({ ok: false, error });
|
|
@@ -67,7 +68,7 @@ function parseEntry(v: unknown): RuleEntry | null {
|
|
|
67
68
|
|
|
68
69
|
/**
|
|
69
70
|
* A `rules` object — rule id → severity or `[severity, ...options]` — with only the rules paperlint
|
|
70
|
-
* ships. Read from
|
|
71
|
+
* ships. Read from every `paperlint.json`, the root's and each paper's.
|
|
71
72
|
*
|
|
72
73
|
* @param where the key path to name in messages, e.g. `package.json → "paperlint".rules[0].rules`
|
|
73
74
|
*/
|
|
@@ -108,17 +109,20 @@ const globsOf = (o: Record<string, unknown>) => ({
|
|
|
108
109
|
...(o["ignores"] ? { ignores: o["ignores"] as string[] } : {}),
|
|
109
110
|
});
|
|
110
111
|
|
|
112
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
113
|
+
typeof v === "object" && v !== null && !Array.isArray(v);
|
|
114
|
+
|
|
111
115
|
/** One element of the `rules` list. */
|
|
112
116
|
function parseBlock(
|
|
113
117
|
v: unknown,
|
|
114
118
|
at: string,
|
|
115
119
|
ctx: { shipped: ReadonlySet<string>; baseDir: string },
|
|
116
120
|
): Parsed<RuleBlock> {
|
|
117
|
-
if (
|
|
121
|
+
if (!isRecord(v))
|
|
118
122
|
return bad(
|
|
119
123
|
`${at} must be an object like { "files": [...], "rules": {...} }`,
|
|
120
124
|
);
|
|
121
|
-
const o = v
|
|
125
|
+
const o = v;
|
|
122
126
|
const extra = Object.keys(o).filter((k) => !BLOCK_KEYS.has(k));
|
|
123
127
|
if (extra.length)
|
|
124
128
|
return bad(
|
|
@@ -135,10 +139,12 @@ function parseBlock(
|
|
|
135
139
|
}
|
|
136
140
|
|
|
137
141
|
/**
|
|
138
|
-
* The `rules` key → config blocks, or one error naming the key path.
|
|
142
|
+
* The `rules` key → config blocks, or one error naming the key path. Two shapes, at every level:
|
|
143
|
+
* `{ id: severity }` is one block over every paper file under `baseDir`; a list is ESLint blocks
|
|
144
|
+
* (`files`, `ignores`, `rules`), their globs relative to `baseDir`.
|
|
139
145
|
*
|
|
140
146
|
* @param raw the value under `rules` (undefined when absent)
|
|
141
|
-
* @param where how the settings are named in messages, e.g. `
|
|
147
|
+
* @param where how the settings are named in messages, e.g. `paperlint.json`
|
|
142
148
|
* @param shipped the rule ids paperlint ships (`shippedRuleIds`)
|
|
143
149
|
* @param baseDir the directory of the file holding the settings
|
|
144
150
|
*/
|
|
@@ -149,13 +155,19 @@ export function parseRuleBlocks(
|
|
|
149
155
|
baseDir: string,
|
|
150
156
|
): Parsed<readonly RuleBlock[]> {
|
|
151
157
|
if (raw === undefined) return { ok: true, value: [] };
|
|
158
|
+
if (isRecord(raw)) {
|
|
159
|
+
const rules = parseRuleEntries(raw, `${where} → "rules"`, shipped);
|
|
160
|
+
return rules.ok
|
|
161
|
+
? { ok: true, value: [{ basePath: baseDir, rules: rules.value }] }
|
|
162
|
+
: rules;
|
|
163
|
+
}
|
|
152
164
|
if (!Array.isArray(raw))
|
|
153
165
|
return bad(
|
|
154
|
-
`${where}
|
|
166
|
+
`${where} → "rules" must be { "<rule>": "<severity>" }, or a list of blocks like ESLint's flat config: [{ "files": [...], "rules": {...} }]`,
|
|
155
167
|
);
|
|
156
168
|
const out: RuleBlock[] = [];
|
|
157
169
|
for (const [i, v] of raw.entries()) {
|
|
158
|
-
const b = parseBlock(v, `${where}
|
|
170
|
+
const b = parseBlock(v, `${where} → rules[${i}]`, { shipped, baseDir });
|
|
159
171
|
if (!b.ok) return b;
|
|
160
172
|
out.push(b.value);
|
|
161
173
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A consumer that ALSO lints paper files with its own ESLint config (#104).
|
|
3
|
+
*
|
|
4
|
+
* 3.0.0 made `% eslint-disable-next-line <rule> -- <why>` the way to keep a deliberate exception.
|
|
5
|
+
* A project that runs its own `eslint` over the same `paper.tex` — for its own rules, with
|
|
6
|
+
* paperlint's LaTeX language — did not register paperlint's rule plugins, so that run failed on the
|
|
7
|
+
* directive: "Definition for rule 'paper/leading-zero' was not found". `rulesOff` is the fragment
|
|
8
|
+
* such a config spreads in: every rule paperlint ships, registered and turned off.
|
|
9
|
+
*/
|
|
10
|
+
import { ESLint, type Linter } from "eslint";
|
|
11
|
+
import { describe, expect, it } from "vitest";
|
|
12
|
+
// @ts-expect-error — the module is .mjs and has no types.
|
|
13
|
+
import { texLanguage } from "../eslint-rules/latex-language.mjs";
|
|
14
|
+
import { rulePlugins, rulesOff, SHIPPED_RULES } from "./cli.ts";
|
|
15
|
+
|
|
16
|
+
const PAPER = [
|
|
17
|
+
"\\documentclass{article}",
|
|
18
|
+
"\\begin{document}",
|
|
19
|
+
"% eslint-disable-next-line paper/leading-zero -- quoted from the reviewer",
|
|
20
|
+
"We use a threshold of .05 throughout.",
|
|
21
|
+
"% eslint-disable-next-line bib/reachable-entry -- kept for the artifact",
|
|
22
|
+
"Nothing else.",
|
|
23
|
+
"\\end{document}",
|
|
24
|
+
"",
|
|
25
|
+
].join("\n");
|
|
26
|
+
|
|
27
|
+
/** The consumer's own config: its LaTeX files, with paperlint's language, and its own rules. */
|
|
28
|
+
async function lintAsConsumer(
|
|
29
|
+
fragment: Linter.Config[],
|
|
30
|
+
): Promise<Linter.LintMessage[]> {
|
|
31
|
+
const eslint = new ESLint({
|
|
32
|
+
overrideConfigFile: true,
|
|
33
|
+
overrideConfig: [
|
|
34
|
+
...fragment,
|
|
35
|
+
{
|
|
36
|
+
files: ["**/*.tex"],
|
|
37
|
+
plugins:
|
|
38
|
+
fragment.length > 0
|
|
39
|
+
? {}
|
|
40
|
+
: { tex: { languages: { latex: texLanguage } } },
|
|
41
|
+
language: "tex/latex",
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
const [res] = await eslint.lintText(PAPER, {
|
|
46
|
+
filePath: "papers/a/paper.tex",
|
|
47
|
+
});
|
|
48
|
+
return res!.messages;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("rulesOff — paperlint's rules, registered and off, for a consumer's own ESLint run", () => {
|
|
52
|
+
it("without it, the documented escape hatch breaks the consumer's run (the defect)", async () => {
|
|
53
|
+
const messages = await lintAsConsumer([]);
|
|
54
|
+
expect(messages.map((m) => m.message)).toContain(
|
|
55
|
+
"Definition for rule 'paper/leading-zero' was not found.",
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("🔴 spreading it: no 'Definition … not found', and no paperlint finding runs", async () => {
|
|
60
|
+
const messages = await lintAsConsumer(rulesOff(texLanguage));
|
|
61
|
+
// Guards: the defect — every directive names a rule ESLint now knows.
|
|
62
|
+
expect(messages.map((m) => m.message).join("\n")).not.toMatch(
|
|
63
|
+
/Definition for rule/,
|
|
64
|
+
);
|
|
65
|
+
// Guards: off means off — paperlint's own run judges these files, not the consumer's.
|
|
66
|
+
expect(messages.filter((m) => m.ruleId?.startsWith("paper/"))).toEqual([]);
|
|
67
|
+
// Guards: a directive naming an OFF rule suppresses nothing, and ESLint would call it unused
|
|
68
|
+
// ("Unused eslint-disable directive (no problems were reported from 'paper/leading-zero')").
|
|
69
|
+
expect(messages).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("registers every rule paperlint ships, each off, and carries the LaTeX language", () => {
|
|
73
|
+
const [cfg] = rulesOff(texLanguage);
|
|
74
|
+
expect(new Set(Object.keys(cfg?.rules ?? {}))).toEqual(SHIPPED_RULES);
|
|
75
|
+
expect(Object.values(cfg?.rules ?? {}).every((e) => e === "off")).toBe(
|
|
76
|
+
true,
|
|
77
|
+
);
|
|
78
|
+
const plugins = rulePlugins(texLanguage);
|
|
79
|
+
const defined = new Set<string>();
|
|
80
|
+
for (const [name, p] of Object.entries(plugins))
|
|
81
|
+
for (const r of Object.keys(p.rules ?? {})) defined.add(`${name}/${r}`);
|
|
82
|
+
expect(defined).toEqual(SHIPPED_RULES);
|
|
83
|
+
expect(plugins["tex"]?.languages?.["latex"]).toBe(texLanguage);
|
|
84
|
+
// `@eslint/markdown` is a dependency's plugin: the consumer registers its own.
|
|
85
|
+
expect(plugins["markdown"]).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("the unused-directive report is off on paper files only, not on the consumer's code", async () => {
|
|
89
|
+
const eslint = new ESLint({
|
|
90
|
+
overrideConfigFile: true,
|
|
91
|
+
overrideConfig: [
|
|
92
|
+
...rulesOff(texLanguage),
|
|
93
|
+
{
|
|
94
|
+
files: ["**/*.js"],
|
|
95
|
+
linterOptions: { reportUnusedDisableDirectives: "warn" },
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
const [res] = await eslint.lintText(
|
|
100
|
+
"// eslint-disable-next-line paper/leading-zero -- nothing to silence\nconst x = 1;\n",
|
|
101
|
+
{ filePath: "src/x.js" },
|
|
102
|
+
);
|
|
103
|
+
expect(res!.messages.map((m) => m.message)).toEqual([
|
|
104
|
+
"Unused eslint-disable directive (no problems were reported from 'paper/leading-zero').",
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("without a LaTeX language, the `tex` plugin carries rules only", () => {
|
|
109
|
+
expect(rulePlugins()["tex"]?.languages).toBeUndefined();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -71,8 +71,10 @@ try {
|
|
|
71
71
|
);
|
|
72
72
|
check(
|
|
73
73
|
"and the message names the CONSEQUENCE, not a restatement of the condition",
|
|
74
|
-
/
|
|
75
|
-
/
|
|
74
|
+
/are skipped/.test(at("no-scorecard")[0].message) &&
|
|
75
|
+
/stage, source and research-question checks/.test(
|
|
76
|
+
at("no-scorecard")[0].message,
|
|
77
|
+
),
|
|
76
78
|
);
|
|
77
79
|
check(
|
|
78
80
|
"and the consequence names THIS directory by name",
|
package/src/structure.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import { readdirSync, existsSync } from "node:fs";
|
|
26
26
|
import { join, relative, basename, isAbsolute, sep } from "node:path";
|
|
27
27
|
import type { StructureConfig, StructureFinding } from "./types.ts";
|
|
28
|
-
import {
|
|
28
|
+
import { CONFIG_FILE } from "../lib/paper-config.mjs";
|
|
29
29
|
|
|
30
30
|
/** The requirements after the consumer's config is laid over the defaults. */
|
|
31
31
|
type Rules = Required<StructureConfig>;
|
|
@@ -38,7 +38,7 @@ type Rules = Required<StructureConfig>;
|
|
|
38
38
|
* byte-compared by the `paper/stages` rule anyway.
|
|
39
39
|
*/
|
|
40
40
|
export const STRUCTURE_DEFAULTS = {
|
|
41
|
-
markers: ["PIPELINE-STATUS.md", "paper.tex", "paper.md",
|
|
41
|
+
markers: ["PIPELINE-STATUS.md", "paper.tex", "paper.md", CONFIG_FILE],
|
|
42
42
|
require: ["PIPELINE-STATUS.md"],
|
|
43
43
|
requireOneOf: [["paper.tex", "paper.md"]],
|
|
44
44
|
ignore: [],
|
|
@@ -122,7 +122,7 @@ export function checkStructure(
|
|
|
122
122
|
*/
|
|
123
123
|
function whyMissingMatters(file: string, dirName: string): string {
|
|
124
124
|
if (file === "PIPELINE-STATUS.md")
|
|
125
|
-
return
|
|
125
|
+
return `the stage, source and research-question checks are skipped for \`${dirName}\``;
|
|
126
126
|
return `declared as required by your \`structure\` configuration`;
|
|
127
127
|
}
|
|
128
128
|
|
package/src/tex-requirements.ts
CHANGED
|
@@ -26,7 +26,7 @@ import { join } from "node:path";
|
|
|
26
26
|
// eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
|
|
27
27
|
import Ajv from "ajv";
|
|
28
28
|
import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
29
|
-
import {
|
|
29
|
+
import { CONFIG_FILE } from "../lib/paper-config.mjs";
|
|
30
30
|
|
|
31
31
|
/** CTAN package name → the names that prove it is installed. */
|
|
32
32
|
export type PackageProofs = Readonly<Record<string, readonly string[]>>;
|
|
@@ -298,7 +298,7 @@ export function requirementsFor(
|
|
|
298
298
|
const base = readProfile(dir, BASE_PROFILE);
|
|
299
299
|
return preset === null
|
|
300
300
|
? {
|
|
301
|
-
source: `the base set (no venue preset in ${
|
|
301
|
+
source: `the base set (no venue preset in ${CONFIG_FILE})`,
|
|
302
302
|
tex: base,
|
|
303
303
|
}
|
|
304
304
|
: {
|
|
@@ -163,14 +163,19 @@ for (const f of [
|
|
|
163
163
|
"stub-pdflatex",
|
|
164
164
|
"stub-kpsewhich",
|
|
165
165
|
"stub-tlmgr",
|
|
166
|
+
"stub-fmtutil-sys",
|
|
166
167
|
"release-texlive.txt",
|
|
167
168
|
])
|
|
168
169
|
copyFileSync(join(FIXTURE, f), join(staging, f));
|
|
169
170
|
spawnSync("chmod", [
|
|
170
171
|
"+x",
|
|
171
|
-
...[
|
|
172
|
-
|
|
173
|
-
|
|
172
|
+
...[
|
|
173
|
+
"install-tl",
|
|
174
|
+
"stub-pdflatex",
|
|
175
|
+
"stub-kpsewhich",
|
|
176
|
+
"stub-tlmgr",
|
|
177
|
+
"stub-fmtutil-sys",
|
|
178
|
+
].map((f) => join(staging, f)),
|
|
174
179
|
]);
|
|
175
180
|
spawnSync(
|
|
176
181
|
"tar",
|
|
@@ -192,6 +197,10 @@ writeFileSync(
|
|
|
192
197
|
const url = (dir) => pathToFileURL(dir).href;
|
|
193
198
|
const MISSING = url(join(work, "no-such-mirror"));
|
|
194
199
|
|
|
200
|
+
/** A call that downloads or installs. `tlmgr check depends` only reads, so it is not one. */
|
|
201
|
+
const installs = (c) =>
|
|
202
|
+
c.cmd === "curl" || (c.cmd.endsWith("tlmgr") && c.args.includes("install"));
|
|
203
|
+
|
|
195
204
|
/** A `spawnSync` that records every program it was asked to run. */
|
|
196
205
|
const recorder = () => {
|
|
197
206
|
const calls = [];
|
|
@@ -364,8 +373,33 @@ const cmd = ({ banal = BANAL, ...over } = {}) => {
|
|
|
364
373
|
);
|
|
365
374
|
check(
|
|
366
375
|
"second run: downloads nothing and installs nothing",
|
|
367
|
-
!r.calls.some(
|
|
368
|
-
JSON.stringify(r.calls.map((c) => c.cmd)),
|
|
376
|
+
!r.calls.some(installs),
|
|
377
|
+
JSON.stringify(r.calls.map((c) => [c.cmd, ...c.args])),
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
// Guards: a tree with every venue package can still be broken. install-tl that lost a package
|
|
381
|
+
// to a mirror prints "continuing anyway"; seen 2026-09-26 with unicode-data, after which the
|
|
382
|
+
// pdflatex format was never built and every build died before writing a log.
|
|
383
|
+
{
|
|
384
|
+
const tree = join(root, "2026");
|
|
385
|
+
writeFileSync(join(tree, "missing-deps.txt"), "unicode-data\n");
|
|
386
|
+
const check1 = cmd({ check: true });
|
|
387
|
+
check(
|
|
388
|
+
"🔴 a dropped base dependency: --check does not call the tree complete",
|
|
389
|
+
check1.code === 1,
|
|
390
|
+
`${check1.out}\n${check1.err}`,
|
|
391
|
+
);
|
|
392
|
+
const r = cmd();
|
|
393
|
+
check(
|
|
394
|
+
"🔴 a dropped base dependency: the run installs it, then builds the missing formats",
|
|
395
|
+
r.code === 0 &&
|
|
396
|
+
readFileSync(join(tree, "tlmgr.log"), "utf8").includes("unicode-data") &&
|
|
397
|
+
readFileSync(join(tree, "fmtutil.log"), "utf8").trim() === "--missing",
|
|
398
|
+
`${r.out}\n${r.err}`,
|
|
399
|
+
);
|
|
400
|
+
check(
|
|
401
|
+
"a dropped base dependency: the tree is whole afterwards",
|
|
402
|
+
readFileSync(join(tree, "missing-deps.txt"), "utf8").trim() === "",
|
|
369
403
|
);
|
|
370
404
|
}
|
|
371
405
|
{
|
|
@@ -500,6 +534,7 @@ const mirrorFor = (name, year, installerYear = year) => {
|
|
|
500
534
|
"stub-pdflatex",
|
|
501
535
|
"stub-kpsewhich",
|
|
502
536
|
"stub-tlmgr",
|
|
537
|
+
"stub-fmtutil-sys",
|
|
503
538
|
])
|
|
504
539
|
copyFileSync(join(staging, f), join(stage, f));
|
|
505
540
|
writeFileSync(
|
|
@@ -544,7 +579,7 @@ check(
|
|
|
544
579
|
r.out.includes("TeX Live 2026") &&
|
|
545
580
|
r.out.includes("nothing to do") &&
|
|
546
581
|
!existsSync(join(years, "2027")) &&
|
|
547
|
-
!r.calls.some(
|
|
582
|
+
!r.calls.some(installs),
|
|
548
583
|
`${r.out}\n${JSON.stringify(r.calls.map((c) => c.cmd))}`,
|
|
549
584
|
);
|
|
550
585
|
}
|
package/src/toolchain.ts
CHANGED
|
@@ -45,6 +45,7 @@ import { join } from "node:path";
|
|
|
45
45
|
import {
|
|
46
46
|
isExecutable,
|
|
47
47
|
missingTools,
|
|
48
|
+
missingDependencies,
|
|
48
49
|
probeTree,
|
|
49
50
|
supportedPlatform,
|
|
50
51
|
type Runner,
|
|
@@ -240,14 +241,20 @@ function yearAfter(lines: readonly string[], label: string): string | null {
|
|
|
240
241
|
: null;
|
|
241
242
|
}
|
|
242
243
|
|
|
243
|
-
/**
|
|
244
|
+
/**
|
|
245
|
+
* What a tree lacks: the venue's packages by `kpsewhich`, tools by executable, and packages the
|
|
246
|
+
* tree's own database depends on but never installed (`tlmgr check depends`).
|
|
247
|
+
*/
|
|
244
248
|
export interface Gaps {
|
|
245
249
|
readonly packages: readonly string[];
|
|
246
250
|
readonly tools: readonly string[];
|
|
251
|
+
readonly dependencies: readonly string[];
|
|
247
252
|
}
|
|
248
253
|
|
|
249
254
|
export const noGaps = (g: Gaps): boolean =>
|
|
250
|
-
g.packages.length === 0 &&
|
|
255
|
+
g.packages.length === 0 &&
|
|
256
|
+
g.tools.length === 0 &&
|
|
257
|
+
g.dependencies.length === 0;
|
|
251
258
|
|
|
252
259
|
/** `acmart (acmart.cls), texcount (texcount)` — a gap named with the file that proves it. */
|
|
253
260
|
export function describeGaps(g: Gaps, tex: TexRequirements): string {
|
|
@@ -258,6 +265,9 @@ export function describeGaps(g: Gaps, tex: TexRequirements): string {
|
|
|
258
265
|
return [
|
|
259
266
|
...named(g.packages, tex.packages),
|
|
260
267
|
...named(g.tools, tex.tools),
|
|
268
|
+
...g.dependencies.map(
|
|
269
|
+
(d) => `${d} (a TeX Live dependency the install left out)`,
|
|
270
|
+
),
|
|
261
271
|
].join(", ");
|
|
262
272
|
}
|
|
263
273
|
|
|
@@ -296,6 +306,7 @@ export function gapsOf(
|
|
|
296
306
|
return {
|
|
297
307
|
packages: probeTree(tree.bin, tex.packages, run),
|
|
298
308
|
tools: missingTools(tex.tools, tree.bin),
|
|
309
|
+
dependencies: missingDependencies(tree.bin, run),
|
|
299
310
|
};
|
|
300
311
|
}
|
|
301
312
|
|
|
@@ -460,7 +471,8 @@ export function installPackages(
|
|
|
460
471
|
let last: string[] = [];
|
|
461
472
|
for (const mirror of mirrors) {
|
|
462
473
|
if (noGaps(gaps)) break;
|
|
463
|
-
const want = [...gaps.packages, ...gaps.tools];
|
|
474
|
+
const want = [...gaps.packages, ...gaps.tools, ...gaps.dependencies];
|
|
475
|
+
const repairsBase = gaps.dependencies.length > 0;
|
|
464
476
|
io.log(` tlmgr: installing ${want.length} package(s) from ${mirror}…`);
|
|
465
477
|
const r = io.run(
|
|
466
478
|
join(tree.bin, "tlmgr"),
|
|
@@ -481,6 +493,10 @@ export function installPackages(
|
|
|
481
493
|
],
|
|
482
494
|
};
|
|
483
495
|
last = tail(r);
|
|
496
|
+
// A dependency of the base was missing, so the formats it feeds were never built (pdflatex dies
|
|
497
|
+
// before writing a log without them). Build the missing ones now that the package is in.
|
|
498
|
+
if (repairsBase)
|
|
499
|
+
io.run(join(tree.bin, "fmtutil-sys"), ["--missing"], quiet(io, TLMGR_MS));
|
|
484
500
|
gaps = gapsOf(tree, tex, io.run);
|
|
485
501
|
}
|
|
486
502
|
return { ok: true, value: { gaps, tail: last } };
|
|
@@ -688,9 +704,17 @@ function report(o: Resolved, tree: CachedTree | null): number {
|
|
|
688
704
|
o.log(binLine(tree));
|
|
689
705
|
return 0;
|
|
690
706
|
}
|
|
691
|
-
const
|
|
707
|
+
const declared = gaps.packages.length + gaps.tools.length;
|
|
708
|
+
// A dependency the install left out is not one of the venue's declared packages: counted apart,
|
|
709
|
+
// so the line never reads "lacks 0 of 51" followed by a name.
|
|
710
|
+
const what = [
|
|
711
|
+
...(declared ? [`${declared} of ${n} declared packages`] : []),
|
|
712
|
+
...(gaps.dependencies.length
|
|
713
|
+
? [`${gaps.dependencies.length} TeX Live base package(s)`]
|
|
714
|
+
: []),
|
|
715
|
+
].join(" and ");
|
|
692
716
|
o.log(
|
|
693
|
-
`✗ TeX Live ${tree.year} in ${tree.dir} lacks ${
|
|
717
|
+
`✗ TeX Live ${tree.year} in ${tree.dir} lacks ${what}: ${describeGaps(gaps, o.tex)}`,
|
|
694
718
|
);
|
|
695
719
|
o.log(` run \`npx paperlint toolchain\` to install them`);
|
|
696
720
|
return 1;
|
package/src/types.ts
CHANGED
|
@@ -5,29 +5,27 @@ import type { RuleBlock } from "./rules-config.ts";
|
|
|
5
5
|
* in someone's head: a typo in a config key read as "field not set", not as an error.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* The project's settings — the root `paperlint.json`, optional. Every key is optional; the key list
|
|
10
|
+
* (and which keys a paper's own `paperlint.json` may also hold) is `SETTINGS_KEYS` in
|
|
11
|
+
* lib/paper-config.mjs.
|
|
12
|
+
*/
|
|
9
13
|
export interface PaperlintConfig {
|
|
10
|
-
/**
|
|
14
|
+
/** The paper director(ies), relative to the project root. Default `papers`. The field name is
|
|
11
15
|
* `PAPERS_DIR_FIELD` in lib/paper-config.mjs; code reads it through `papersDirOf()` in cli.ts. */
|
|
12
16
|
papersDir?: string | string[];
|
|
13
|
-
/** The command that prints the author list from .bib — each corpus has its own. */
|
|
14
|
-
authorListCommand?: string;
|
|
15
|
-
/** Per-paper typography debt: how many findings already exist; the number may only go down. */
|
|
16
|
-
typographyDebt?: Record<string, Record<string, number>>;
|
|
17
|
-
/** Frontmatter fields required for review notes, and the permitted values of each. */
|
|
18
|
-
docFields?: Record<string, { values: string[] }>;
|
|
19
|
-
/** Ignore review findings older than this date. */
|
|
20
|
-
reviewSince?: string;
|
|
21
|
-
/** How many findings a cold read must produce to count as a cold read. */
|
|
22
|
-
minFindings?: number;
|
|
23
|
-
/** The word with which review notes introduce the cause. */
|
|
24
|
-
causeMarker?: string;
|
|
25
17
|
/** Which files a paper directory must carry; `false` turns the check off entirely. */
|
|
26
18
|
structure?: StructureConfig | false;
|
|
27
|
-
/** REMOVED — `paperlint build` compiles the paper itself. Still typed so a leftover key can be named. */
|
|
28
|
-
buildScripts?: unknown;
|
|
29
19
|
/** ESLint config blocks appended after paperlint's own — PARSED by `parseSettings` in cli.ts. */
|
|
30
20
|
rules?: readonly RuleBlock[];
|
|
21
|
+
/** The default venue preset of every paper that names none (`src/paper-settings.ts`). */
|
|
22
|
+
extends?: string | null;
|
|
23
|
+
/** The default kind of paper. */
|
|
24
|
+
kind?: string;
|
|
25
|
+
/** The default path of the built PDF, relative to each paper. */
|
|
26
|
+
pdf?: string;
|
|
27
|
+
/** A note for humans; ignored. */
|
|
28
|
+
$comment?: unknown;
|
|
31
29
|
}
|
|
32
30
|
|
|
33
31
|
export interface StructureConfig {
|
|
@@ -47,6 +45,8 @@ export interface Args {
|
|
|
47
45
|
paths: string[];
|
|
48
46
|
config: string | null;
|
|
49
47
|
json: boolean;
|
|
48
|
+
/** `--fix`: `lint` writes every fix a rule offers, then reports what is left. */
|
|
49
|
+
fix: boolean;
|
|
50
50
|
all: boolean;
|
|
51
51
|
dryRun: boolean;
|
|
52
52
|
/** `--check`: `toolchain` reports what is missing and changes nothing. */
|
|
@@ -60,11 +60,17 @@ export interface Args {
|
|
|
60
60
|
paper: string | null;
|
|
61
61
|
/** `--format tex|md` for `new` and `init --paper`. Validated by the command, not here. */
|
|
62
62
|
format: string | null;
|
|
63
|
+
/** `--venue <preset>` for `new`: a shipped preset name, or a path to the project's own. */
|
|
64
|
+
venue: string | null;
|
|
65
|
+
/** `--kind <kind>` for `new`: one of the venue preset's kinds. */
|
|
66
|
+
kind: string | null;
|
|
63
67
|
/** `--hooks=<mode>` — parsed only so it can be REFUSED by name rather than read as a path. */
|
|
64
68
|
hooksMode: string | null;
|
|
65
69
|
help?: boolean;
|
|
66
70
|
/** A flag that turned out to have no value. A non-empty field is a REFUSAL, not a default. */
|
|
67
71
|
missingValue?: string;
|
|
72
|
+
/** The first flag the parser does not know. Refused, never read as a path. */
|
|
73
|
+
unknownFlag?: string;
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
/** A finding about the PRESENCE of a file — what an ESLint rule cannot express. */
|
|
@@ -102,5 +108,17 @@ export interface BuildResult {
|
|
|
102
108
|
|
|
103
109
|
/** The result of reading the config: either data, or the exit code the caller exits with. */
|
|
104
110
|
export type ConfigRead =
|
|
105
|
-
| {
|
|
106
|
-
|
|
111
|
+
| {
|
|
112
|
+
opts: PaperlintConfig;
|
|
113
|
+
/** The root `paperlint.json`, or null when the project has none. */
|
|
114
|
+
configPath: string | null;
|
|
115
|
+
/** The project root: `papersDir` and the `rules` globs are relative to it. */
|
|
116
|
+
root: string;
|
|
117
|
+
code?: undefined;
|
|
118
|
+
}
|
|
119
|
+
| {
|
|
120
|
+
code: number;
|
|
121
|
+
opts?: undefined;
|
|
122
|
+
configPath?: undefined;
|
|
123
|
+
root?: undefined;
|
|
124
|
+
};
|
package/src/venue-rules.test.ts
CHANGED
|
@@ -173,7 +173,7 @@ describe("a paper that meets its venue", () => {
|
|
|
173
173
|
});
|
|
174
174
|
|
|
175
175
|
describe("a paper that names no venue", () => {
|
|
176
|
-
it("no paperlint.json
|
|
176
|
+
it("no paperlint.json: every rule is silent, even with no facts and no PDF", () => {
|
|
177
177
|
expect(lint({ venue: undefined, facts: null, pdf: null })).toEqual([]);
|
|
178
178
|
});
|
|
179
179
|
|
|
@@ -228,16 +228,6 @@ describe("pdf/profile — the declaration must resolve, or nothing is judged", (
|
|
|
228
228
|
expect(fs[0]?.message).toMatch(text);
|
|
229
229
|
});
|
|
230
230
|
|
|
231
|
-
it("🔴 a venue.json left from before 2.1.0 is not read — pdf/profile says to run `paperlint init`", () => {
|
|
232
|
-
const fs = lint({
|
|
233
|
-
facts: null,
|
|
234
|
-
pdf: null,
|
|
235
|
-
extra: { [`${PAPER}/venue.json`]: JSON.stringify(DECL) },
|
|
236
|
-
});
|
|
237
|
-
expect(ids(fs)).toEqual(["pdf/profile:legacySettings"]);
|
|
238
|
-
expect(fs[0]?.message).toMatch(/npx paperlint init/);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
231
|
it("a profile that does not parse is named with the file", () => {
|
|
242
232
|
const fs = lint({
|
|
243
233
|
venue: { extends: "paperlint:mine", kind: "short" },
|
|
@@ -302,11 +292,33 @@ describe("pdf/profile — the kind", () => {
|
|
|
302
292
|
});
|
|
303
293
|
});
|
|
304
294
|
|
|
295
|
+
describe("pdf/profile — a preset with no kinds", () => {
|
|
296
|
+
it("🔴 a preset with NO kinds (acm-sigconf) and no `kind`: nothing to pick, no finding", () => {
|
|
297
|
+
const fs = lint({
|
|
298
|
+
venue: { extends: "paperlint:acm-sigconf" },
|
|
299
|
+
facts: withFacts((f) => (f.body_pages = 99)),
|
|
300
|
+
});
|
|
301
|
+
expect(ids(fs).filter((id) => id.startsWith("pdf/profile"))).toEqual([]);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("…but a `kind` named against a kindless preset is still an error that says it has none", () => {
|
|
305
|
+
const fs = lint({
|
|
306
|
+
venue: { extends: "paperlint:acm-sigconf", kind: "short" },
|
|
307
|
+
});
|
|
308
|
+
const profile = fs.filter((f) => f.rule === "pdf/profile");
|
|
309
|
+
expect(ids(profile)).toEqual(["pdf/profile:kindUnknown"]);
|
|
310
|
+
expect(profile[0]?.message).toMatch(/its kinds: \(none\)/);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
305
314
|
describe("pdf/measured — a paper that names a venue and was not measured says so (warn)", () => {
|
|
306
315
|
it("no facts file: one finding, from pdf/measured only", () => {
|
|
307
316
|
const fs = lint({ venue: DECL, facts: null, pdf: null });
|
|
308
317
|
expect(ids(fs)).toEqual(["pdf/measured:unbuilt"]);
|
|
309
318
|
expect(fs[0]?.message).toMatch(/paperlint build/);
|
|
319
|
+
// Guards: the message stays one short line — it is the first thing a new user sees.
|
|
320
|
+
expect(fs[0]?.message).toMatch(/^not built yet, so /);
|
|
321
|
+
expect(fs[0]?.message.length).toBeLessThan(110);
|
|
310
322
|
});
|
|
311
323
|
|
|
312
324
|
it("facts without geometry (no banal): pdf/measured names it, the three geometry rules are silent, fonts still judge", () => {
|
package/src/venue-rules.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* One rule per reason, and the others are silent — so a paper gets one finding that says what to
|
|
25
25
|
* do, not six that say the same thing:
|
|
26
26
|
*
|
|
27
|
-
* no paperlint.json every rule silent
|
|
27
|
+
* no paperlint.json every rule silent (`paperlint new` writes one)
|
|
28
28
|
* extends null / absent pdf/measured (warn) — no venue chosen yet; it names the file to set
|
|
29
29
|
* preset does not resolve pdf/profile (error) — a typo would otherwise switch every check off
|
|
30
30
|
* not built / no facts pdf/measured (warn) — lint often runs before or without a build (the
|
|
@@ -48,10 +48,7 @@ import type { FlatGeometry } from "./domain/geometry.ts";
|
|
|
48
48
|
import type { AbsolutePath } from "./domain/paths.ts";
|
|
49
49
|
import { sha256Hex } from "./domain/sha256.ts";
|
|
50
50
|
import type { Files } from "./ports/files.ts";
|
|
51
|
-
import {
|
|
52
|
-
LEGACY_PAPER_SETTINGS_MESSAGE,
|
|
53
|
-
PAPER_SETTINGS_FILE,
|
|
54
|
-
} from "../lib/paper-config.mjs";
|
|
51
|
+
import { CONFIG_FILE } from "../lib/paper-config.mjs";
|
|
55
52
|
|
|
56
53
|
// ── the verdict's vocabulary ─────────────────────────────────────────────────────────
|
|
57
54
|
|
|
@@ -109,6 +106,11 @@ function kindOf(
|
|
|
109
106
|
kind: string | null,
|
|
110
107
|
): Pick<Resolved, "kind" | "kindProblem"> {
|
|
111
108
|
const known = [...format.kinds.keys()].join(", ") || "(none)";
|
|
109
|
+
// A preset with no kinds (`acm-sigconf`, a family a paper for an unprofiled venue extends
|
|
110
|
+
// directly) has no page limit to pick, so naming no kind is the only valid declaration. A kind
|
|
111
|
+
// named against it is still `kindUnknown`, whose "its kinds: (none)" says why.
|
|
112
|
+
if (kind === null && format.kinds.size === 0)
|
|
113
|
+
return { kind: null, kindProblem: null };
|
|
112
114
|
if (kind === null)
|
|
113
115
|
return {
|
|
114
116
|
kind: null,
|
|
@@ -150,14 +152,11 @@ export function assessPaper(paperDir: string, deps: VenueRuleDeps): Assessment {
|
|
|
150
152
|
if (p.kind === "none")
|
|
151
153
|
return p.settings === null
|
|
152
154
|
? { kind: "no-venue" }
|
|
153
|
-
: { kind: "no-preset", file: join(paperDir,
|
|
155
|
+
: { kind: "no-preset", file: join(paperDir, CONFIG_FILE) };
|
|
154
156
|
if (p.kind === "settings-problem")
|
|
155
157
|
return {
|
|
156
158
|
kind: "unresolved",
|
|
157
|
-
finding:
|
|
158
|
-
p.problem.kind === "legacy"
|
|
159
|
-
? finding("legacySettings")
|
|
160
|
-
: finding("settingsBroken", { why: p.problem.why }),
|
|
159
|
+
finding: finding("settingsBroken", { why: p.problem.why }),
|
|
161
160
|
};
|
|
162
161
|
if (p.kind === "preset-problem")
|
|
163
162
|
return {
|
|
@@ -415,11 +414,10 @@ const META: Readonly<Record<VenueRuleName, Meta>> = {
|
|
|
415
414
|
"the venue preset a paper's paperlint.json extends resolves, and the kind it names exists",
|
|
416
415
|
},
|
|
417
416
|
messages: {
|
|
418
|
-
settingsBroken: `${
|
|
419
|
-
legacySettings: `this paper's venue checks do not run — ${LEGACY_PAPER_SETTINGS_MESSAGE}`,
|
|
417
|
+
settingsBroken: `${CONFIG_FILE} cannot be read: {{why}}`,
|
|
420
418
|
preset:
|
|
421
419
|
"{{why}} — so this paper's page limit, fonts and format are not checked. Fix `extends` in its paperlint.json, or turn pdf/profile off for this paper",
|
|
422
|
-
kindMissing: `${
|
|
420
|
+
kindMissing: `${CONFIG_FILE} names no \`kind\`, so the page limit of \`{{venue}}\` is not checked; its kinds: {{known}}`,
|
|
423
421
|
kindUnknown:
|
|
424
422
|
"`{{venue}}` has no kind `{{kind}}`, so the page limit is not checked; its kinds: {{known}}",
|
|
425
423
|
},
|
|
@@ -432,7 +430,7 @@ const META: Readonly<Record<VenueRuleName, Meta>> = {
|
|
|
432
430
|
},
|
|
433
431
|
messages: {
|
|
434
432
|
unbuilt:
|
|
435
|
-
"
|
|
433
|
+
"not built yet, so `{{venue}}`'s page limit, fonts and format are not checked — run `paperlint build`",
|
|
436
434
|
noPreset:
|
|
437
435
|
'this paper names no venue preset yet, so its page limit, fonts and format are not checked — set "extends" in {{file}} (e.g. "paperlint:agenticdev"; see docs/rules.md)',
|
|
438
436
|
noGeometry:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$comment": "Name this paper's venue preset in \"extends\", e.g. \"paperlint:agenticdev\" (
|
|
2
|
+
"$comment": "Name this paper's venue preset in \"extends\": \"paperlint:<name>\" for a shipped one, e.g. \"paperlint:agenticdev\" (`npx paperlint --help` lists them), or a path to your own preset relative to THIS file, e.g. \"../../venues/my-venue.jsonc\"; add \"kind\" for its page limit. `npx paperlint new <name> --venue <preset>` writes both for a new paper. Until then the page limit, fonts and format are not checked.",
|
|
3
3
|
"extends": null
|
|
4
4
|
}
|