paperlint 2.0.0 → 2.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/.github/workflows/ci.yml +4 -4
- package/CLAUDE.md +3 -3
- package/CONTRIBUTING.md +21 -0
- package/LICENSE +1 -1
- package/README.md +81 -24
- package/action.harness.mjs +10 -10
- package/action.mutations.mjs +3 -3
- package/action.yml +11 -11
- package/bin/{rpp.mjs → paperlint.mjs} +2 -2
- package/dist/adapters/banal/invocation.js +3 -3
- package/dist/adapters/banal/invocation.js.map +1 -1
- package/dist/adapters/banal/run.d.ts +1 -1
- package/dist/adapters/banal/run.js +1 -1
- package/dist/adapters/banal/run.js.map +1 -1
- package/dist/adapters/banal/settings.d.ts +4 -4
- package/dist/adapters/banal/settings.d.ts.map +1 -1
- package/dist/adapters/banal/settings.js +4 -4
- package/dist/adapters/banal/settings.js.map +1 -1
- package/dist/adapters/banal/xml.js +1 -1
- package/dist/adapters/banal/xml.js.map +1 -1
- package/dist/adapters/curl/download.io.d.ts.map +1 -1
- package/dist/adapters/curl/download.io.js +2 -2
- package/dist/adapters/curl/download.io.js.map +1 -1
- package/dist/build-engine.d.ts +1 -1
- package/dist/build-engine.d.ts.map +1 -1
- package/dist/build-engine.js +6 -3
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +5 -4
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +29 -30
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +174 -98
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +21 -6
- package/dist/doctor.js.map +1 -1
- package/dist/engine.d.ts +2 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1 -1
- package/dist/facts-file.d.ts +33 -9
- package/dist/facts-file.d.ts.map +1 -1
- package/dist/facts-file.js +117 -17
- package/dist/facts-file.js.map +1 -1
- package/dist/hooks-settings.d.ts +0 -18
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +70 -42
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +13 -10
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +74 -42
- package/dist/init.js.map +1 -1
- package/dist/link-skills.js +2 -2
- package/dist/link-skills.js.map +1 -1
- package/dist/new-paper.d.ts +2 -2
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +17 -2
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-settings.d.ts +68 -0
- package/dist/paper-settings.d.ts.map +1 -0
- package/dist/paper-settings.js +144 -0
- package/dist/paper-settings.js.map +1 -0
- package/dist/presets.d.ts +84 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +186 -0
- package/dist/presets.js.map +1 -0
- package/dist/rules-config.d.ts +8 -1
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +13 -8
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +2 -1
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.d.ts +63 -7
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +91 -24
- package/dist/tex-requirements.js.map +1 -1
- package/dist/toolchain.d.ts +2 -2
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +8 -8
- package/dist/toolchain.js.map +1 -1
- package/dist/types.d.ts +4 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts +109 -0
- package/dist/venue-rules.d.ts.map +1 -0
- package/dist/venue-rules.js +387 -0
- package/dist/venue-rules.js.map +1 -0
- package/docs/configuration.md +73 -17
- package/docs/e2e.md +3 -4
- package/docs/install.md +11 -3
- package/docs/optional-rules.md +26 -25
- package/docs/rules.md +132 -3
- package/docs/toolchain.md +5 -5
- package/eslint-rules/paper-typography.mjs +1 -1
- package/eslint-rules/paper-typography.mutations.mjs +1 -1
- package/eslint-rules/papers.mjs +6 -2
- package/eslint-rules/pdf-last-page-balance.harness.mjs +5 -3
- package/eslint-rules/pdf-last-page-balance.mjs +23 -2
- package/fixtures/build-e2e/acmart/paperlint.json +1 -0
- package/fixtures/build-e2e/guards/paper.tex +1 -1
- package/fixtures/pdf-facts/README.md +1 -1
- package/fixtures/real-markdown-paper/baseline.json +1 -1
- package/fixtures/real-markdown-paper/baseline.mjs +2 -2
- package/fixtures/toolchain-mirror/install-tl +2 -2
- package/fixtures/toolchain-mirror/stub-pdflatex +1 -1
- package/fixtures/toolchain-mirror/stub-tlmgr +1 -1
- package/hooks/hooks.harness.mjs +1 -1
- package/lib/paper-config.d.mts +7 -0
- package/lib/paper-config.harness.mjs +3 -3
- package/lib/paper-config.mjs +34 -3
- package/lib/skill-trigger-cases.harness.mjs +1 -1
- package/package.json +4 -4
- package/plugin/hooks/hooks.json +3 -3
- package/scripts/check.mjs +1 -1
- package/scripts/harness-api.frozen.json +1 -1
- package/scripts/harness-api.test.ts +2 -2
- package/scripts/layer-legacy.frozen.json +1 -1
- package/scripts/mutation-batteries-frozen.harness.mjs +1 -1
- package/scripts/mutation-batteries.frozen.json +2 -2
- package/scripts/release-config.test.ts +1 -1
- package/skills/find-venue/SKILL.md +5 -1
- package/skills/find-venue/SKILL.md.spec.ts +4 -0
- package/skills/paper-pipeline/PIPELINE-MAP.md +14 -4
- package/skills/paper-pipeline/SKILL.md +4 -3
- package/skills/paper-pipeline/SKILL.md.spec.ts +3 -2
- package/skills/paper-pipeline/description-language.eval.mjs +1 -1
- package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +1 -1
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +2 -2
- package/skills/paper-pipeline/pipeline-firing.eval.mjs +1 -1
- package/skills/paper-pipeline/pipeline-language.eval.mjs +1 -1
- package/skills/paper-pipeline/scripts/consumer.d.mts +1 -0
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +10 -4
- package/skills/paper-pipeline/scripts/consumer.mjs +8 -1
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +1 -1
- package/skills/render-paper/SKILL.md +3 -3
- package/skills/render-paper/SKILL.md.spec.ts +2 -2
- package/skills/render-paper/extract-pdf-facts.harness.mjs +6 -6
- package/skills/render-paper/extract-pdf-facts.mjs +4 -4
- package/skills/study-accepted-papers/SKILL.md +2 -1
- package/skills/study-accepted-papers/SKILL.md.spec.ts +1 -0
- package/skills/submit-paper/SKILL.md +9 -1
- package/skills/submit-paper/SKILL.md.spec.ts +8 -0
- package/skills/submit-paper/references/venues/acm-sigconf.jsonc +86 -0
- package/skills/submit-paper/references/venues/agenticdev.jsonc +34 -102
- package/skills/submit-paper/references/venues/aisec.jsonc +21 -82
- package/skills/submit-paper/references/venues/realm.jsonc +45 -43
- package/skills/submit-paper/references/venues/tex-base.jsonc +1 -1
- package/skills/submit-paper/references/venues/venue-profile.schema.json +120 -30
- package/src/adapters/banal/index.test.ts +1 -1
- package/src/adapters/banal/invocation.test.ts +3 -3
- package/src/adapters/banal/invocation.ts +3 -3
- package/src/adapters/banal/locate.test.ts +4 -4
- package/src/adapters/banal/run.ts +2 -2
- package/src/adapters/banal/settings.test.ts +12 -6
- package/src/adapters/banal/settings.ts +9 -5
- package/src/adapters/banal/xml.test.ts +1 -1
- package/src/adapters/banal/xml.ts +1 -1
- package/src/adapters/curl/download.io.ts +4 -2
- package/src/adapters/curl/download.test.ts +3 -1
- package/src/adapters/node/files.test.ts +1 -1
- package/src/adapters/node/process.test.ts +2 -2
- package/src/adapters/node/workspace.test.ts +4 -4
- package/src/build-engine.harness.mjs +2 -2
- package/src/build-engine.ts +11 -3
- package/src/build.harness.mjs +27 -22
- package/src/build.mutations.mjs +3 -2
- package/src/build.ts +6 -5
- package/src/cli.harness.mjs +72 -131
- package/src/cli.mutations.mjs +7 -32
- package/src/cli.ts +204 -121
- package/src/doctor.harness.mjs +9 -25
- package/src/doctor.ts +28 -5
- package/src/engine.harness.mjs +4 -2
- package/src/engine.ts +2 -2
- package/src/facts-file.test.ts +54 -7
- package/src/facts-file.ts +145 -24
- package/src/hooks-settings.harness.mjs +24 -15
- package/src/hooks-settings.mutations.mjs +2 -4
- package/src/hooks-settings.ts +92 -49
- package/src/init.ts +88 -52
- package/src/latex-log.harness.mjs +1 -1
- package/src/link-skills.harness.mjs +3 -1
- package/src/link-skills.mutations.mjs +1 -1
- package/src/link-skills.ts +2 -2
- package/src/new-paper.harness.mjs +10 -7
- package/src/new-paper.test.ts +145 -0
- package/src/new-paper.ts +22 -2
- package/src/paper-settings-commands.test.ts +311 -0
- package/src/paper-settings.test.ts +220 -0
- package/src/paper-settings.ts +206 -0
- package/src/pdf-facts.harness.mjs +1 -1
- package/src/presets.test.ts +236 -0
- package/src/presets.ts +307 -0
- package/src/rules-config.ts +14 -9
- package/src/structure.harness.mjs +4 -4
- package/src/structure.ts +2 -1
- package/src/tex-requirements.harness.mjs +12 -18
- package/src/tex-requirements.ts +179 -29
- package/src/toolchain.harness.mjs +34 -25
- package/src/toolchain.ts +14 -8
- package/src/types.ts +4 -4
- package/src/venue-rules.test.ts +545 -0
- package/src/venue-rules.ts +573 -0
- package/templates/paper/paperlint.json +4 -0
- package/fixtures/build-e2e/acmart/venue.json +0 -1
- package/plugin/.claude-plugin/plugin.json +0 -8
package/src/tex-requirements.ts
CHANGED
|
@@ -20,12 +20,13 @@
|
|
|
20
20
|
* allowed to know where this package is installed (rule 10).
|
|
21
21
|
*/
|
|
22
22
|
// eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
|
|
23
|
-
import {
|
|
23
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
24
24
|
import { createRequire } from "node:module";
|
|
25
25
|
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 { PAPER_SETTINGS_FILE } from "../lib/paper-config.mjs";
|
|
29
30
|
|
|
30
31
|
/** CTAN package name → the names that prove it is installed. */
|
|
31
32
|
export type PackageProofs = Readonly<Record<string, readonly string[]>>;
|
|
@@ -78,18 +79,133 @@ function violations(file: string, v: Validate): string[] {
|
|
|
78
79
|
);
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
/** One kind of paper a venue takes (`short`, `research`, …) and its page limits; null = not limited. */
|
|
83
|
+
export interface KindLimits {
|
|
84
|
+
readonly bodyPagesMax: number | null;
|
|
85
|
+
readonly refPagesMax: number | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The format a venue's call for papers sets — what the `pdf/*` venue rules judge a built PDF
|
|
90
|
+
* against. Every field the profile leaves out is null, and a rule that finds null checks nothing:
|
|
91
|
+
* a profile that does not name a number never makes one up.
|
|
92
|
+
*/
|
|
93
|
+
export interface VenueFormat {
|
|
94
|
+
readonly pageWidthIn: number | null;
|
|
95
|
+
readonly pageHeightIn: number | null;
|
|
96
|
+
readonly columns: number | null;
|
|
97
|
+
readonly bodyPt: number | null;
|
|
98
|
+
readonly bodyPtTol: number | null;
|
|
99
|
+
readonly refPtMin: number | null;
|
|
100
|
+
readonly refPtMax: number | null;
|
|
101
|
+
/** The prefix a font name of the body text starts with (`LinLibertine`). */
|
|
102
|
+
readonly fontsText: string | null;
|
|
103
|
+
readonly fontsTitle: string | null;
|
|
104
|
+
readonly kinds: ReadonlyMap<string, KindLimits>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Every format field a preset left out is null; a merge fills it from the parent. */
|
|
108
|
+
export const NO_FORMAT: VenueFormat = {
|
|
109
|
+
pageWidthIn: null,
|
|
110
|
+
pageHeightIn: null,
|
|
111
|
+
columns: null,
|
|
112
|
+
bodyPt: null,
|
|
113
|
+
bodyPtTol: null,
|
|
114
|
+
refPtMin: null,
|
|
115
|
+
refPtMax: null,
|
|
116
|
+
fontsText: null,
|
|
117
|
+
fontsTitle: null,
|
|
118
|
+
kinds: new Map(),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* One preset FILE, parsed — before its `extends` chain is resolved (`src/presets.ts` does that).
|
|
123
|
+
* A preset is a venue or a template family: `{ extends?, name?, template?, format?, tex?, rules? }`.
|
|
124
|
+
*/
|
|
125
|
+
export interface PresetFile {
|
|
126
|
+
/** The preset this one builds on: `paperlint:<name>` or a relative path. */
|
|
127
|
+
readonly extends: string | null;
|
|
128
|
+
/** A display name for messages; the file name otherwise. */
|
|
129
|
+
readonly name: string | null;
|
|
130
|
+
readonly template: string | null;
|
|
131
|
+
/** Null when the file declares no `tex` block (allowed only with `extends`). */
|
|
132
|
+
readonly tex: TexRequirements | null;
|
|
133
|
+
readonly format: VenueFormat;
|
|
134
|
+
/** rule id → ESLint entry, checked against the shipped rules where a config is built. */
|
|
135
|
+
readonly rules: Readonly<Record<string, unknown>>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
type KindsJson = Readonly<
|
|
139
|
+
Record<string, { body_pages_max?: number; ref_pages_max?: number }>
|
|
140
|
+
>;
|
|
141
|
+
|
|
142
|
+
/** A preset's `format` block after the schema accepted it. */
|
|
143
|
+
interface FormatJson {
|
|
144
|
+
readonly page_w_in?: number;
|
|
145
|
+
readonly page_h_in?: number;
|
|
146
|
+
readonly columns?: number;
|
|
147
|
+
readonly body_pt?: number;
|
|
148
|
+
readonly body_pt_tol?: number;
|
|
149
|
+
readonly ref_pt_min?: number;
|
|
150
|
+
readonly ref_pt_max?: number;
|
|
151
|
+
readonly fonts_text?: string;
|
|
152
|
+
readonly fonts_title?: string;
|
|
153
|
+
readonly kinds?: KindsJson;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The preset's JSON after the schema accepted it — the shape `venue-profile.schema.json` allows. */
|
|
157
|
+
interface PresetJson {
|
|
158
|
+
readonly extends?: string;
|
|
159
|
+
readonly name?: string;
|
|
160
|
+
readonly template?: string;
|
|
161
|
+
readonly tex?: Partial<TexRequirements>;
|
|
162
|
+
readonly format?: FormatJson;
|
|
163
|
+
readonly rules?: Readonly<Record<string, unknown>>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** An optional field as the typed preset holds it: absent is null. */
|
|
167
|
+
const orNull = <T>(v: T | undefined): T | null => (v === undefined ? null : v);
|
|
168
|
+
|
|
169
|
+
function kindsOf(k: KindsJson | undefined): ReadonlyMap<string, KindLimits> {
|
|
170
|
+
return new Map(
|
|
171
|
+
Object.entries(k ?? {}).map(([name, v]) => [
|
|
172
|
+
name,
|
|
173
|
+
{
|
|
174
|
+
bodyPagesMax: orNull(v.body_pages_max),
|
|
175
|
+
refPagesMax: orNull(v.ref_pages_max),
|
|
176
|
+
},
|
|
177
|
+
]),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function formatOf(j: FormatJson = {}): VenueFormat {
|
|
182
|
+
return {
|
|
183
|
+
pageWidthIn: orNull(j.page_w_in),
|
|
184
|
+
pageHeightIn: orNull(j.page_h_in),
|
|
185
|
+
columns: orNull(j.columns),
|
|
186
|
+
bodyPt: orNull(j.body_pt),
|
|
187
|
+
bodyPtTol: orNull(j.body_pt_tol),
|
|
188
|
+
refPtMin: orNull(j.ref_pt_min),
|
|
189
|
+
refPtMax: orNull(j.ref_pt_max),
|
|
190
|
+
fontsText: orNull(j.fonts_text),
|
|
191
|
+
fontsTitle: orNull(j.fonts_title),
|
|
192
|
+
kinds: kindsOf(j.kinds),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
81
196
|
/**
|
|
82
|
-
* The text of one
|
|
197
|
+
* The text of one preset file → the typed file, or an Error naming every problem. The ONE parser
|
|
198
|
+
* of a preset: the toolchain reads its `tex`, the venue rules its `format`, the config its `rules`.
|
|
83
199
|
*
|
|
84
200
|
* @param text the file's contents
|
|
85
201
|
* @param file the name to put in messages
|
|
86
202
|
* @param dir the directory holding the schema (the shipped venues directory by default)
|
|
87
203
|
*/
|
|
88
|
-
export function
|
|
204
|
+
export function parsePreset(
|
|
89
205
|
text: string,
|
|
90
206
|
file: string,
|
|
91
207
|
dir: string = packageVenuesDir(),
|
|
92
|
-
):
|
|
208
|
+
): PresetFile {
|
|
93
209
|
const { config, error } = typescript().parseConfigFileTextToJson(file, text);
|
|
94
210
|
if (error)
|
|
95
211
|
throw new Error(
|
|
@@ -100,8 +216,38 @@ export function parseProfile(
|
|
|
100
216
|
throw new Error(
|
|
101
217
|
`${file} does not match ${SCHEMA_FILE}:\n ${violations(file, validate).join("\n ")}`,
|
|
102
218
|
);
|
|
103
|
-
const
|
|
104
|
-
return {
|
|
219
|
+
const j = config as PresetJson;
|
|
220
|
+
return {
|
|
221
|
+
extends: orNull(j.extends),
|
|
222
|
+
name: orNull(j.name),
|
|
223
|
+
template: orNull(j.template),
|
|
224
|
+
tex: j.tex
|
|
225
|
+
? { packages: j.tex.packages ?? {}, tools: j.tex.tools ?? {} }
|
|
226
|
+
: null,
|
|
227
|
+
format: formatOf(j.format),
|
|
228
|
+
rules: j.rules ?? {},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The text of one preset → the TeX requirements it declares itself (none without a `tex` block). */
|
|
233
|
+
export function parseProfile(
|
|
234
|
+
text: string,
|
|
235
|
+
file: string,
|
|
236
|
+
dir: string = packageVenuesDir(),
|
|
237
|
+
): TexRequirements {
|
|
238
|
+
return parsePreset(text, file, dir).tex ?? NO_REQUIREMENTS;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The file a venue's profile lives in, or null when the name cannot be a venue (the base set is a
|
|
243
|
+
* profile file but not a venue). Whether the file exists is the caller's question.
|
|
244
|
+
*/
|
|
245
|
+
export function profileFileOf(venue: string): string | null {
|
|
246
|
+
return venue === BASE_PROFILE.slice(0, -PROFILE_EXT.length) ||
|
|
247
|
+
venue.includes("/") ||
|
|
248
|
+
venue.includes("\\")
|
|
249
|
+
? null
|
|
250
|
+
: `${venue}${PROFILE_EXT}`;
|
|
105
251
|
}
|
|
106
252
|
|
|
107
253
|
function readProfile(dir: string, file: string): TexRequirements {
|
|
@@ -141,41 +287,45 @@ export function packageNames(tex: TexRequirements): string[] {
|
|
|
141
287
|
}
|
|
142
288
|
|
|
143
289
|
/**
|
|
144
|
-
* What ONE paper needs: the base set plus its venue's
|
|
145
|
-
*
|
|
146
|
-
* without
|
|
290
|
+
* What ONE paper needs: the base set plus its venue preset's `tex` (the union over the preset's
|
|
291
|
+
* `extends` chain, resolved by `src/presets.ts`). A paper with no preset gets the base set, and the
|
|
292
|
+
* source says so, so an ACM paper built without a preset is visibly running on the base set.
|
|
147
293
|
*/
|
|
148
294
|
export function requirementsFor(
|
|
149
|
-
|
|
295
|
+
preset: { readonly label: string; readonly tex: TexRequirements } | null,
|
|
150
296
|
dir: string = packageVenuesDir(),
|
|
151
297
|
): PaperRequirements {
|
|
152
298
|
const base = readProfile(dir, BASE_PROFILE);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
tex: base,
|
|
163
|
-
};
|
|
164
|
-
return {
|
|
165
|
-
source: `venue ${venue}`,
|
|
166
|
-
tex: mergeRequirements(base, readProfile(dir, file)),
|
|
167
|
-
};
|
|
299
|
+
return preset === null
|
|
300
|
+
? {
|
|
301
|
+
source: `the base set (no venue preset in ${PAPER_SETTINGS_FILE})`,
|
|
302
|
+
tex: base,
|
|
303
|
+
}
|
|
304
|
+
: {
|
|
305
|
+
source: `venue ${preset.label}`,
|
|
306
|
+
tex: mergeRequirements(base, preset.tex),
|
|
307
|
+
};
|
|
168
308
|
}
|
|
169
309
|
|
|
170
|
-
/**
|
|
171
|
-
|
|
310
|
+
/**
|
|
311
|
+
* Everything any shipped preset declares, plus `extra` — the resolved presets of the project's own
|
|
312
|
+
* papers, which may live outside the package. The set `paperlint toolchain` installs, so one tree
|
|
313
|
+
* builds any paper.
|
|
314
|
+
*/
|
|
315
|
+
export function declaredUnion(
|
|
316
|
+
dir: string = packageVenuesDir(),
|
|
317
|
+
extra: readonly TexRequirements[] = [],
|
|
318
|
+
): {
|
|
172
319
|
readonly tex: TexRequirements;
|
|
173
320
|
readonly profiles: number;
|
|
174
321
|
} {
|
|
175
322
|
const venues = venueNames(dir);
|
|
176
|
-
const
|
|
323
|
+
const shipped = venues.reduce(
|
|
177
324
|
(acc, v) => mergeRequirements(acc, readProfile(dir, `${v}${PROFILE_EXT}`)),
|
|
178
325
|
readProfile(dir, BASE_PROFILE),
|
|
179
326
|
);
|
|
180
|
-
return {
|
|
327
|
+
return {
|
|
328
|
+
tex: extra.reduce(mergeRequirements, shipped),
|
|
329
|
+
profiles: venues.length + 1,
|
|
330
|
+
};
|
|
181
331
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 🔴 NOT ONE BYTE OF TeX LIVE IS DOWNLOADED HERE. The mirror is a directory served by `file://`
|
|
5
5
|
* URLs: the real `curl` fetches from it, the real `tar` lists and unpacks it, and the installer
|
|
6
6
|
* inside is `fixtures/toolchain-mirror/install-tl`, which lays out a tree whose `kpsewhich` and
|
|
7
|
-
* `tlmgr` answer from two text files. So what runs is
|
|
7
|
+
* `tlmgr` answer from two text files. So what runs is paperlint's own download, fallback, unpack,
|
|
8
8
|
* install and verify logic — the part that decides success — and what is faked is only TeX.
|
|
9
9
|
* The real TeX Live half is `test/e2e/toolchain.mjs`.
|
|
10
10
|
*
|
|
@@ -78,22 +78,25 @@ check(
|
|
|
78
78
|
) === '["urw-base35"]',
|
|
79
79
|
);
|
|
80
80
|
check(
|
|
81
|
-
"cacheRoot:
|
|
82
|
-
T.cacheRoot(
|
|
83
|
-
"/ci/tl",
|
|
81
|
+
"cacheRoot: PAPERLINT_TEXLIVE_DIR wins",
|
|
82
|
+
T.cacheRoot(
|
|
83
|
+
{ PAPERLINT_TEXLIVE_DIR: "/ci/tl", XDG_CACHE_HOME: "/x" },
|
|
84
|
+
"/h",
|
|
85
|
+
) === "/ci/tl",
|
|
84
86
|
);
|
|
85
87
|
check(
|
|
86
88
|
"cacheRoot: then XDG_CACHE_HOME",
|
|
87
|
-
T.cacheRoot({ XDG_CACHE_HOME: "/x" }, "/h") === "/x/
|
|
89
|
+
T.cacheRoot({ XDG_CACHE_HOME: "/x" }, "/h") === "/x/paperlint/texlive",
|
|
88
90
|
);
|
|
89
91
|
check(
|
|
90
92
|
"cacheRoot: then ~/.cache",
|
|
91
|
-
T.cacheRoot({}, "/h") === "/h/.cache/
|
|
93
|
+
T.cacheRoot({}, "/h") === "/h/.cache/paperlint/texlive",
|
|
92
94
|
);
|
|
93
95
|
check(
|
|
94
|
-
"mirrorsFrom:
|
|
95
|
-
JSON.stringify(
|
|
96
|
-
|
|
96
|
+
"mirrorsFrom: PAPERLINT_CTAN_MIRROR is the ONLY mirror, trailing slash dropped",
|
|
97
|
+
JSON.stringify(
|
|
98
|
+
T.mirrorsFrom({ PAPERLINT_CTAN_MIRROR: "https://m/tlnet/" }),
|
|
99
|
+
) === '["https://m/tlnet"]',
|
|
97
100
|
);
|
|
98
101
|
check(
|
|
99
102
|
"mirrorsFrom: several defaults, every one https",
|
|
@@ -146,7 +149,9 @@ check(
|
|
|
146
149
|
check("usableTree: an empty cache", T.usableTree([], () => true) === null);
|
|
147
150
|
|
|
148
151
|
// ── the fake mirror ────────────────────────────────────────────────────────────────────
|
|
149
|
-
const work = realpathSync(
|
|
152
|
+
const work = realpathSync(
|
|
153
|
+
mkdtempSync(join(tmpdir(), "paperlint-toolchain-h-")),
|
|
154
|
+
);
|
|
150
155
|
const good = join(work, "good");
|
|
151
156
|
const garbage = join(work, "garbage");
|
|
152
157
|
const staging = join(work, "staging", "install-tl-20260924");
|
|
@@ -259,14 +264,14 @@ const TEX = {
|
|
|
259
264
|
tools: { texcount: ["texcount"] },
|
|
260
265
|
};
|
|
261
266
|
const root = join(work, "cache");
|
|
262
|
-
// 🔴 banal goes into the work directory, never the developer's cache: without
|
|
263
|
-
// command installs into ~/.cache/
|
|
267
|
+
// 🔴 banal goes into the work directory, never the developer's cache: without PAPERLINT_BANAL_DIR the
|
|
268
|
+
// command installs into ~/.cache/paperlint/banal, and a harness must not leave files in a home.
|
|
264
269
|
const banalDir = join(work, "banal");
|
|
265
270
|
const env = {
|
|
266
271
|
...process.env,
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
272
|
+
PAPERLINT_TEXLIVE_DIR: root,
|
|
273
|
+
PAPERLINT_CTAN_MIRROR: url(good),
|
|
274
|
+
PAPERLINT_BANAL_DIR: banalDir,
|
|
270
275
|
};
|
|
271
276
|
// A stand-in banal: a Perl script that prints what banal prints for a one-page, 10 pt probe.
|
|
272
277
|
const fakeBanal = join(work, "fake-banal");
|
|
@@ -446,7 +451,7 @@ const cmd = ({ banal = BANAL, ...over } = {}) => {
|
|
|
446
451
|
const r = cmd({
|
|
447
452
|
env: {
|
|
448
453
|
...env,
|
|
449
|
-
|
|
454
|
+
PAPERLINT_TEXLIVE_DIR: fresh,
|
|
450
455
|
FAKE_INSTALL_TL_FAIL: "1",
|
|
451
456
|
},
|
|
452
457
|
});
|
|
@@ -462,15 +467,15 @@ const cmd = ({ banal = BANAL, ...over } = {}) => {
|
|
|
462
467
|
const r = cmd({
|
|
463
468
|
env: {
|
|
464
469
|
...env,
|
|
465
|
-
|
|
466
|
-
|
|
470
|
+
PAPERLINT_TEXLIVE_DIR: join(work, "cache-nomirror"),
|
|
471
|
+
PAPERLINT_CTAN_MIRROR: MISSING,
|
|
467
472
|
},
|
|
468
473
|
});
|
|
469
474
|
check(
|
|
470
475
|
"no mirror answers: exit 1, names the mirrors and the override",
|
|
471
476
|
r.code === 1 &&
|
|
472
477
|
r.err.includes("no CTAN mirror returned install-tl") &&
|
|
473
|
-
r.err.includes("
|
|
478
|
+
r.err.includes("PAPERLINT_CTAN_MIRROR"),
|
|
474
479
|
r.err,
|
|
475
480
|
);
|
|
476
481
|
}
|
|
@@ -520,7 +525,11 @@ const withLibertine = {
|
|
|
520
525
|
};
|
|
521
526
|
const inYears = (mirror, over = {}) =>
|
|
522
527
|
cmd({
|
|
523
|
-
env: {
|
|
528
|
+
env: {
|
|
529
|
+
...env,
|
|
530
|
+
PAPERLINT_TEXLIVE_DIR: years,
|
|
531
|
+
PAPERLINT_CTAN_MIRROR: mirror,
|
|
532
|
+
},
|
|
524
533
|
...over,
|
|
525
534
|
});
|
|
526
535
|
check(
|
|
@@ -583,12 +592,12 @@ check(
|
|
|
583
592
|
{
|
|
584
593
|
// tlmgr says 2027, but the installer the mirrors hand out is still 2026.
|
|
585
594
|
const split = join(work, "cache-split");
|
|
586
|
-
cmd({ env: { ...env,
|
|
595
|
+
cmd({ env: { ...env, PAPERLINT_TEXLIVE_DIR: split } });
|
|
587
596
|
const r = cmd({
|
|
588
597
|
env: {
|
|
589
598
|
...env,
|
|
590
|
-
|
|
591
|
-
|
|
599
|
+
PAPERLINT_TEXLIVE_DIR: split,
|
|
600
|
+
PAPERLINT_CTAN_MIRROR: mirrorFor("split2027", "2027", "2026"),
|
|
592
601
|
},
|
|
593
602
|
tex: withLibertine,
|
|
594
603
|
});
|
|
@@ -606,7 +615,7 @@ check(
|
|
|
606
615
|
// ── 4. banal, the second half of the command ───────────────────────────────────────────
|
|
607
616
|
{
|
|
608
617
|
const dir = join(work, "banal-4");
|
|
609
|
-
const e = { ...env,
|
|
618
|
+
const e = { ...env, PAPERLINT_BANAL_DIR: dir };
|
|
610
619
|
const before = cmd({ env: e, check: true });
|
|
611
620
|
// Guards: --check covers banal — a complete TeX Live alone must not read as a ready toolchain.
|
|
612
621
|
check(
|
|
@@ -633,7 +642,7 @@ check(
|
|
|
633
642
|
}
|
|
634
643
|
{
|
|
635
644
|
const r = cmd({
|
|
636
|
-
env: { ...env,
|
|
645
|
+
env: { ...env, PAPERLINT_BANAL_DIR: join(work, "banal-bad") },
|
|
637
646
|
banal: { ...BANAL, sha256: "0".repeat(64) },
|
|
638
647
|
});
|
|
639
648
|
// Guards: the two halves are independent — a banal failure fails the command, and TeX Live still
|
package/src/toolchain.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `paperlint toolchain` — install upstream TeX Live, with exactly the packages the venue profiles
|
|
3
|
-
* declare, into
|
|
3
|
+
* declare, into paperlint's own cache, and banal (the page-geometry script HotCRP runs) at its pinned
|
|
4
4
|
* commit (`adapters/banal/`). `paperlint build` offers the TeX Live install on a terminal.
|
|
5
5
|
*
|
|
6
6
|
* 🔴 WHY paperlint INSTALLS TeX AT ALL (rule 11). "Install TeX Live yourself" was a manual step with a
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
* once reported success having installed nothing: installers' exit codes are not evidence.)
|
|
20
20
|
* A second run with everything present does nothing and says so; `--check` only reports.
|
|
21
21
|
*
|
|
22
|
-
* Where: `$
|
|
23
|
-
* `$XDG_CACHE_HOME/
|
|
22
|
+
* Where: `$PAPERLINT_TEXLIVE_DIR` when set (CI points its cache there), else
|
|
23
|
+
* `$XDG_CACHE_HOME/paperlint/texlive`, else `~/.cache/paperlint/texlive` — one tree per TeX Live year inside.
|
|
24
24
|
* Processes run through the injected `run` (the port `build.ts` uses), so the harness drives the
|
|
25
25
|
* real download/unpack/verify logic against a fake mirror on disk, never the network.
|
|
26
26
|
*/
|
|
@@ -56,8 +56,8 @@ import {
|
|
|
56
56
|
type TexRequirements,
|
|
57
57
|
} from "./tex-requirements.ts";
|
|
58
58
|
|
|
59
|
-
export const CACHE_ENV = "
|
|
60
|
-
export const MIRROR_ENV = "
|
|
59
|
+
export const CACHE_ENV = "PAPERLINT_TEXLIVE_DIR";
|
|
60
|
+
export const MIRROR_ENV = "PAPERLINT_CTAN_MIRROR";
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* The CTAN mirrors, redirector first. 🔴 SEVERAL, AND THAT IS A MEASUREMENT: run 33650803245 died
|
|
@@ -99,7 +99,11 @@ export function cacheRoot(
|
|
|
99
99
|
home: string = homedir(),
|
|
100
100
|
): string {
|
|
101
101
|
if (env[CACHE_ENV]) return env[CACHE_ENV];
|
|
102
|
-
return join(
|
|
102
|
+
return join(
|
|
103
|
+
env["XDG_CACHE_HOME"] || join(home, ".cache"),
|
|
104
|
+
"paperlint",
|
|
105
|
+
"texlive",
|
|
106
|
+
);
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
/**
|
|
@@ -353,7 +357,9 @@ export function installBase(
|
|
|
353
357
|
mirrors: readonly string[],
|
|
354
358
|
newerThan: string | null = null,
|
|
355
359
|
): Step<{ tree: CachedTree; mirror: string }> {
|
|
356
|
-
const work = realpathSync(
|
|
360
|
+
const work = realpathSync(
|
|
361
|
+
mkdtempSync(join(tmpdir(), "paperlint-install-tl-")),
|
|
362
|
+
);
|
|
357
363
|
try {
|
|
358
364
|
io.log(` downloading install-tl…`);
|
|
359
365
|
const mirror = downloadInstaller(
|
|
@@ -409,7 +415,7 @@ function runInstallTl(
|
|
|
409
415
|
): Step<{ tree: CachedTree; mirror: string }> {
|
|
410
416
|
const dir = join(a.root, a.year);
|
|
411
417
|
mkdirSync(dir, { recursive: true });
|
|
412
|
-
const profile = join(a.work, "
|
|
418
|
+
const profile = join(a.work, "paperlint.profile");
|
|
413
419
|
writeFileSync(profile, tlProfile(dir));
|
|
414
420
|
io.log(
|
|
415
421
|
` install-tl: TeX Live ${a.year} scheme-basic from ${a.mirror} (~150 MB, about a minute)…`,
|
package/src/types.ts
CHANGED
|
@@ -5,8 +5,8 @@ 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
|
-
/** The
|
|
9
|
-
export interface
|
|
8
|
+
/** The consumer's settings — the `paperlint` key of its `package.json`. */
|
|
9
|
+
export interface PaperlintConfig {
|
|
10
10
|
/** 🔴 REQUIRED. The paper director(ies), relative to the config file ITSELF. The field name is
|
|
11
11
|
* `PAPERS_DIR_FIELD` in lib/paper-config.mjs; code reads it through `papersDirOf()` in cli.ts. */
|
|
12
12
|
papersDir?: string | string[];
|
|
@@ -26,7 +26,7 @@ export interface RppConfig {
|
|
|
26
26
|
structure?: StructureConfig | false;
|
|
27
27
|
/** REMOVED — `paperlint build` compiles the paper itself. Still typed so a leftover key can be named. */
|
|
28
28
|
buildScripts?: unknown;
|
|
29
|
-
/** ESLint config blocks appended after
|
|
29
|
+
/** ESLint config blocks appended after paperlint's own — PARSED by `parseSettings` in cli.ts. */
|
|
30
30
|
rules?: readonly RuleBlock[];
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -102,5 +102,5 @@ export interface BuildResult {
|
|
|
102
102
|
|
|
103
103
|
/** The result of reading the config: either data, or the exit code the caller exits with. */
|
|
104
104
|
export type ConfigRead =
|
|
105
|
-
| { opts:
|
|
105
|
+
| { opts: PaperlintConfig; configPath: string | null; code?: undefined }
|
|
106
106
|
| { code: number; opts?: undefined; configPath?: undefined };
|