paperlint 3.0.0 → 3.1.1
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 +28 -41
- package/.github/workflows/dependabot-automerge.yml +1 -1
- package/CLAUDE.md +51 -56
- package/CONTRIBUTING.md +30 -18
- package/README.md +179 -180
- package/bin/paperlint.test.mjs +45 -0
- package/dist/adapters/node/files.io.d.ts +2 -0
- package/dist/adapters/node/files.io.d.ts.map +1 -1
- package/dist/adapters/node/files.io.js +3 -2
- package/dist/adapters/node/files.io.js.map +1 -1
- package/dist/adapters/pdfjs/fill.d.ts +15 -4
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
- package/dist/adapters/pdfjs/fill.js +31 -21
- package/dist/adapters/pdfjs/fill.js.map +1 -1
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +14 -5
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/adapters/references/pool.d.ts +8 -0
- package/dist/adapters/references/pool.d.ts.map +1 -0
- package/dist/adapters/references/pool.js +19 -0
- package/dist/adapters/references/pool.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 +10 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +29 -11
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +93 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +296 -80
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +27 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +4 -3
- package/dist/doctor.js.map +1 -1
- package/dist/domain/result.d.ts +0 -2
- package/dist/domain/result.d.ts.map +1 -1
- package/dist/domain/result.js +0 -4
- package/dist/domain/result.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/hooks-settings.d.ts +8 -5
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +32 -32
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +8 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +4 -4
- package/dist/init.js.map +1 -1
- package/dist/latex-log.d.ts +0 -2
- package/dist/latex-log.d.ts.map +1 -1
- package/dist/latex-log.js +7 -13
- package/dist/latex-log.js.map +1 -1
- package/dist/link-skills.d.ts +15 -2
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +36 -20
- 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 +50 -15
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +30 -8
- package/dist/paper-files.d.ts.map +1 -1
- package/dist/paper-files.js +59 -30
- package/dist/paper-files.js.map +1 -1
- package/dist/pdf-facts.d.ts +11 -0
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +16 -18
- package/dist/pdf-facts.js.map +1 -1
- package/dist/pdf-geometry.js +1 -1
- package/dist/pdf-geometry.js.map +1 -1
- package/dist/presets.d.ts +2 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +3 -1
- package/dist/presets.js.map +1 -1
- package/dist/references.js +2 -2
- package/dist/references.js.map +1 -1
- package/dist/structure.js +1 -1
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.d.ts +8 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +6 -5
- package/dist/tex-requirements.js.map +1 -1
- package/dist/toolchain.d.ts +8 -3
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +26 -10
- package/dist/toolchain.js.map +1 -1
- package/dist/types.d.ts +6 -0
- 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 +48 -4
- package/docs/prior-art/README.md +1 -1
- package/docs/prior-art/test-tooling.md +7 -30
- package/docs/rules.md +29 -25
- package/docs/skills.md +80 -0
- package/eslint-rules/install-path-literals.harness.mjs +17 -1
- package/eslint-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.mjs +12 -9
- package/eslint-rules/latex-language.test.mjs +200 -0
- package/eslint-rules/paper-research-question.harness.mjs +73 -7
- package/eslint-rules/paper-stages.harness.mjs +9 -14
- package/eslint-rules/paper-stages.test.mjs +135 -0
- package/eslint-rules/paper-typography.mjs +56 -9
- package/eslint-rules/paper-typography.test.mjs +186 -1
- package/eslint-rules/pdf-last-page-balance.harness.mjs +63 -7
- package/eslint-rules/review-frontmatter.mjs +2 -1
- package/eslint-rules/review-frontmatter.schema.json +1 -1
- package/eslint-rules/review-frontmatter.test.mjs +37 -1
- package/eslint-rules/temp-root-realpath.harness.mjs +49 -0
- package/eslint-rules/tex-build.mjs +13 -13
- package/eslint.config.mjs +6 -2
- 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 +6 -11
- package/hooks/paper-edit-guard.hook.mjs +2 -2
- package/hooks/paper-edit-guard.test.mjs +61 -0
- package/hooks/paper-skills-nudge.hook.mjs +2 -2
- package/hooks/paper-skills-nudge.test.mjs +38 -0
- package/hooks/paper-status-gates.test.mjs +42 -0
- package/lib/agent-cli-version.harness.mjs +7 -0
- package/lib/check.d.mts +17 -0
- package/lib/check.mjs +50 -0
- package/lib/check.test.ts +67 -0
- package/lib/markdown.mjs +16 -69
- package/lib/markdown.test.mjs +246 -0
- package/lib/paper-config.harness.mjs +28 -9
- package/lib/paper-config.mjs +7 -2
- package/lib/skill-checks.mjs +18 -46
- package/lib/skill-checks.test.mjs +297 -0
- package/lib/skill-corpus.mjs +11 -13
- package/lib/skill-corpus.test.mjs +54 -0
- package/lib/skill-trigger-cases.harness.mjs +57 -4
- package/package.json +6 -5
- package/scripts/check.harness.mjs +4 -10
- package/scripts/check.mjs +45 -42
- package/scripts/check.test.mjs +104 -0
- package/scripts/coverage-config.test.ts +406 -0
- package/scripts/eslint-report-guard.test.mjs +48 -0
- package/scripts/harness-api.frozen.json +0 -2
- package/scripts/layer-legacy-frozen.d.mts +9 -1
- package/scripts/layer-legacy-frozen.mjs +28 -11
- package/scripts/layer-legacy-frozen.test.ts +143 -1
- package/scripts/rules-are-content-only.harness.mjs +3 -11
- package/scripts/rules-are-content-only.mjs +29 -19
- package/scripts/rules-are-content-only.test.mjs +61 -0
- package/scripts/rules-see-files.mjs +11 -9
- package/scripts/rules-see-files.test.mjs +71 -0
- package/scripts/test.mjs +44 -0
- package/scripts/test.test.mjs +52 -0
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.mjs.snap +57 -0
- package/skills/grade-paper-writing/prose-lint.mjs +31 -12
- package/skills/grade-paper-writing/prose-lint.test.mjs +153 -0
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +3 -3
- package/skills/paper-pipeline/scripts/README.md +13 -132
- package/skills/paper-pipeline/scripts/announce.test.mjs +102 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.test.mjs +86 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +8 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +3 -3
- package/skills/paper-pipeline/scripts/consumer.mjs +13 -25
- package/skills/paper-pipeline/scripts/consumer.test.mjs +113 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +9 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +380 -0
- package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +71 -0
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +4 -4
- package/skills/paper-pipeline/scripts/generated-code.mjs +3 -2
- package/skills/paper-pipeline/scripts/generated-code.test.mjs +62 -0
- package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +302 -0
- package/skills/paper-pipeline/scripts/ledger.mjs +1 -1
- package/skills/paper-pipeline/scripts/{ledger.selftest.mjs → ledger.test.mjs} +44 -76
- package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +474 -0
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -63
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +37 -21
- package/skills/paper-pipeline/scripts/pipeline-edges.mjs +2 -2
- package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +73 -0
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +4 -47
- package/skills/paper-pipeline/scripts/population-map.mjs +12 -2
- package/skills/paper-pipeline/scripts/population-map.test.mjs +154 -0
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +26 -0
- package/skills/paper-pipeline/scripts/round-diff.harness.mjs +1 -2
- package/skills/paper-pipeline/scripts/round-diff.mjs +13 -6
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +159 -0
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +30 -6
- package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +468 -0
- package/skills/paper-pipeline/scripts/status.mjs +2 -2
- package/skills/paper-pipeline/scripts/status.test.mjs +232 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +2 -2
- package/skills/render-paper/extract-pdf-facts.harness.mjs +56 -7
- package/skills/tighten-paper/__snapshots__/structure.test.mjs.snap +133 -0
- package/skills/tighten-paper/structure.test.mjs +116 -0
- package/skills/verify-citations/SKILL.md +5 -5
- package/skills/verify-citations/SKILL.md.spec.ts +4 -4
- package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +235 -0
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +274 -0
- package/skills/verify-citations/scripts/bib-authors.cli.test.mjs +178 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +2 -1
- package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
- package/skills/verify-citations/scripts/verify-cites.mjs +96 -96
- package/skills/verify-citations/scripts/verify-cites.net.test.mjs +287 -0
- package/skills/verify-citations/scripts/verify-cites.test.mjs +149 -5
- package/src/adapters/banal/failure.test.ts +70 -1
- package/src/adapters/banal/index.test.ts +16 -0
- package/src/adapters/banal/output.test.ts +5 -0
- package/src/adapters/curl/download.test.ts +43 -1
- package/src/adapters/memory/index.test.ts +25 -0
- package/src/adapters/node/files.io.ts +3 -2
- package/src/adapters/node/files.test.ts +13 -1
- package/src/adapters/node/process.test.ts +38 -0
- package/src/adapters/pdfjs/fill.test.ts +34 -0
- package/src/adapters/pdfjs/fill.ts +44 -25
- package/src/adapters/references/index.test.ts +228 -0
- package/src/adapters/references/index.ts +21 -6
- package/src/adapters/references/pool.test.ts +31 -0
- package/src/adapters/references/pool.ts +24 -0
- package/src/build-engine.harness.mjs +3 -7
- package/src/build-engine.test.ts +137 -0
- package/src/build-engine.ts +6 -1
- package/src/build.harness.mjs +51 -11
- package/src/build.test.ts +362 -0
- package/src/build.ts +40 -14
- package/src/cli.harness.mjs +12 -19
- package/src/cli.test.ts +584 -0
- package/src/cli.ts +409 -94
- package/src/doctor.harness.mjs +3 -8
- package/src/doctor.test.ts +64 -0
- package/src/doctor.ts +12 -4
- package/src/domain/result.test.ts +4 -0
- package/src/domain/result.ts +0 -5
- package/src/engine.harness.mjs +77 -7
- package/src/engine.ts +35 -0
- package/src/facts-file.test.ts +110 -0
- package/src/hooks-settings.harness.mjs +43 -13
- package/src/hooks-settings.ts +41 -35
- package/src/init.test.ts +308 -5
- package/src/init.ts +4 -4
- package/src/latex-log.harness.mjs +32 -7
- package/src/latex-log.ts +6 -13
- package/src/latex-loop.harness.mjs +8 -7
- package/src/link-skills.harness.mjs +3 -8
- package/src/link-skills.test.ts +105 -0
- package/src/link-skills.ts +75 -21
- package/src/linted-set.test.ts +118 -0
- package/src/new-paper.harness.mjs +5 -7
- package/src/new-paper.test.ts +203 -2
- package/src/new-paper.ts +84 -17
- package/src/paper-files.test.ts +122 -5
- package/src/paper-files.ts +99 -39
- package/src/paper-settings-commands.test.ts +41 -5
- package/src/paper-settings.test.ts +20 -0
- package/src/parse-args.test.ts +48 -0
- package/src/pdf-facts.harness.mjs +3 -6
- package/src/pdf-facts.test.ts +144 -0
- package/src/pdf-facts.ts +17 -19
- package/src/pdf-geometry.harness.mjs +3 -7
- package/src/pdf-geometry.ts +1 -1
- package/src/presets.test.ts +2 -0
- package/src/presets.ts +7 -1
- package/src/references.test.ts +97 -0
- package/src/references.ts +2 -2
- package/src/rules-config.harness.mjs +10 -8
- package/src/rules-off.test.ts +111 -0
- package/src/structure.harness.mjs +5 -9
- package/src/structure.ts +1 -1
- package/src/tex-requirements.harness.mjs +34 -7
- package/src/tex-requirements.ts +12 -6
- package/src/toolchain.harness.mjs +44 -13
- package/src/toolchain.test.ts +280 -0
- package/src/toolchain.ts +37 -12
- package/src/types.ts +6 -0
- package/src/venue-rules.test.ts +63 -0
- package/src/venue-rules.ts +14 -12
- package/templates/paper/paperlint.json +1 -1
- package/action.mutations.mjs +0 -162
- package/eslint-rules/install-path-literals.mutations.mjs +0 -62
- package/eslint-rules/latex-language.mutations.mjs +0 -196
- package/eslint-rules/paper-research-question.mutations.mjs +0 -127
- package/eslint-rules/paper-stages.mutations.mjs +0 -81
- package/eslint-rules/papers.mutations.mjs +0 -186
- package/eslint-rules/temp-root-realpath.mutations.mjs +0 -99
- package/eslint-rules/tex-build.mutations.mjs +0 -258
- package/hooks/hooks.mutations.mjs +0 -337
- package/lib/agent-cli-version.mutations.mjs +0 -109
- package/lib/mutation-driver.harness.mjs +0 -227
- package/lib/mutation-driver.mjs +0 -397
- package/lib/mutation-driver.mutations.mjs +0 -68
- package/lib/paper-config.mutations.mjs +0 -143
- package/lib/skill-trigger-cases.mutations.mjs +0 -65
- package/scripts/check.mutations.mjs +0 -110
- package/scripts/exclusive.mjs +0 -138
- package/scripts/mutation-batteries-frozen.harness.mjs +0 -204
- package/scripts/mutation-batteries-frozen.mjs +0 -238
- package/scripts/mutation-batteries.frozen.json +0 -108
- package/scripts/rules-are-content-only.mutations.mjs +0 -81
- package/scripts/rules-see-files.mutations.mjs +0 -131
- package/scripts/run-mutations.mjs +0 -100
- package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +0 -218
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +0 -190
- package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +0 -254
- package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/population-map.mutations.mjs +0 -65
- package/skills/paper-pipeline/scripts/population-map.selftest.mjs +0 -122
- package/skills/paper-pipeline/scripts/provenance.mutations.mjs +0 -59
- package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +0 -276
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +0 -65
- package/skills/skill-contract.mutations.mjs +0 -179
- package/src/build.mutations.mjs +0 -196
- package/src/cli.mutations.mjs +0 -414
- package/src/doctor.mutations.mjs +0 -175
- package/src/hooks-settings.mutations.mjs +0 -99
- package/src/link-skills.mutations.mjs +0 -140
- package/src/new-paper.mutations.mjs +0 -79
- package/src/structure.mutations.mjs +0 -83
package/src/tex-requirements.ts
CHANGED
|
@@ -72,8 +72,14 @@ function validatorFor(dir: string): Validate {
|
|
|
72
72
|
return v;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
/**
|
|
76
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Every violation as `<file>: <path> <message>` — one line each, so all of them are fixed at once.
|
|
77
|
+
* Ajv types `errors` as possibly null and a message as optional; both are tolerated, not assumed.
|
|
78
|
+
*/
|
|
79
|
+
export function violations(
|
|
80
|
+
file: string,
|
|
81
|
+
v: Pick<Validate, "errors">,
|
|
82
|
+
): string[] {
|
|
77
83
|
return (v.errors ?? []).map(
|
|
78
84
|
(e) => `${file}: ${e.dataPath || "(top level)"} ${e.message ?? ""}`,
|
|
79
85
|
);
|
|
@@ -158,7 +164,9 @@ interface PresetJson {
|
|
|
158
164
|
readonly extends?: string;
|
|
159
165
|
readonly name?: string;
|
|
160
166
|
readonly template?: string;
|
|
161
|
-
|
|
167
|
+
/** The schema requires `packages` in a `tex` block; `tools` is optional. */
|
|
168
|
+
readonly tex?: Pick<TexRequirements, "packages"> &
|
|
169
|
+
Partial<Pick<TexRequirements, "tools">>;
|
|
162
170
|
readonly format?: FormatJson;
|
|
163
171
|
readonly rules?: Readonly<Record<string, unknown>>;
|
|
164
172
|
}
|
|
@@ -221,9 +229,7 @@ export function parsePreset(
|
|
|
221
229
|
extends: orNull(j.extends),
|
|
222
230
|
name: orNull(j.name),
|
|
223
231
|
template: orNull(j.template),
|
|
224
|
-
tex: j.tex
|
|
225
|
-
? { packages: j.tex.packages ?? {}, tools: j.tex.tools ?? {} }
|
|
226
|
-
: null,
|
|
232
|
+
tex: j.tex ? { packages: j.tex.packages, tools: j.tex.tools ?? {} } : null,
|
|
227
233
|
format: formatOf(j.format),
|
|
228
234
|
rules: j.rules ?? {},
|
|
229
235
|
};
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
* 4. banal, the command's second half: a stand-in banal served from `file://`, never HotCRP —
|
|
17
17
|
* the installer's own tests are `banal.harness.mjs`; here only the wiring into the command.
|
|
18
18
|
*/
|
|
19
|
-
import assert from "node:assert/strict";
|
|
20
19
|
import { spawnSync } from "node:child_process";
|
|
21
20
|
import { createHash } from "node:crypto";
|
|
22
21
|
import {
|
|
@@ -33,6 +32,7 @@ import {
|
|
|
33
32
|
import { tmpdir } from "node:os";
|
|
34
33
|
import { dirname, join } from "node:path";
|
|
35
34
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
35
|
+
import { createChecker } from "../lib/check.mjs";
|
|
36
36
|
|
|
37
37
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
38
38
|
const FIXTURE = join(HERE, "..", "fixtures", "toolchain-mirror");
|
|
@@ -43,11 +43,7 @@ const B = await import(join(HERE, "adapters", "banal", "index.ts"));
|
|
|
43
43
|
const N = await import(join(HERE, "adapters", "node", "index.ts"));
|
|
44
44
|
const C = await import(join(HERE, "adapters", "curl", "index.ts"));
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
const check = (label, cond, detail = "") => {
|
|
48
|
-
assert.ok(cond, `${label}${detail ? ` — ${detail}` : ""}`);
|
|
49
|
-
n++;
|
|
50
|
-
};
|
|
46
|
+
const check = createChecker();
|
|
51
47
|
|
|
52
48
|
// ── 1. pure pieces ──────────────────────────────────────────────────────────────────────
|
|
53
49
|
check(
|
|
@@ -163,14 +159,19 @@ for (const f of [
|
|
|
163
159
|
"stub-pdflatex",
|
|
164
160
|
"stub-kpsewhich",
|
|
165
161
|
"stub-tlmgr",
|
|
162
|
+
"stub-fmtutil-sys",
|
|
166
163
|
"release-texlive.txt",
|
|
167
164
|
])
|
|
168
165
|
copyFileSync(join(FIXTURE, f), join(staging, f));
|
|
169
166
|
spawnSync("chmod", [
|
|
170
167
|
"+x",
|
|
171
|
-
...[
|
|
172
|
-
|
|
173
|
-
|
|
168
|
+
...[
|
|
169
|
+
"install-tl",
|
|
170
|
+
"stub-pdflatex",
|
|
171
|
+
"stub-kpsewhich",
|
|
172
|
+
"stub-tlmgr",
|
|
173
|
+
"stub-fmtutil-sys",
|
|
174
|
+
].map((f) => join(staging, f)),
|
|
174
175
|
]);
|
|
175
176
|
spawnSync(
|
|
176
177
|
"tar",
|
|
@@ -192,6 +193,10 @@ writeFileSync(
|
|
|
192
193
|
const url = (dir) => pathToFileURL(dir).href;
|
|
193
194
|
const MISSING = url(join(work, "no-such-mirror"));
|
|
194
195
|
|
|
196
|
+
/** A call that downloads or installs. `tlmgr check depends` only reads, so it is not one. */
|
|
197
|
+
const installs = (c) =>
|
|
198
|
+
c.cmd === "curl" || (c.cmd.endsWith("tlmgr") && c.args.includes("install"));
|
|
199
|
+
|
|
195
200
|
/** A `spawnSync` that records every program it was asked to run. */
|
|
196
201
|
const recorder = () => {
|
|
197
202
|
const calls = [];
|
|
@@ -364,8 +369,33 @@ const cmd = ({ banal = BANAL, ...over } = {}) => {
|
|
|
364
369
|
);
|
|
365
370
|
check(
|
|
366
371
|
"second run: downloads nothing and installs nothing",
|
|
367
|
-
!r.calls.some(
|
|
368
|
-
JSON.stringify(r.calls.map((c) => c.cmd)),
|
|
372
|
+
!r.calls.some(installs),
|
|
373
|
+
JSON.stringify(r.calls.map((c) => [c.cmd, ...c.args])),
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
// Guards: a tree with every venue package can still be broken. install-tl that lost a package
|
|
377
|
+
// to a mirror prints "continuing anyway"; seen 2026-09-26 with unicode-data, after which the
|
|
378
|
+
// pdflatex format was never built and every build died before writing a log.
|
|
379
|
+
{
|
|
380
|
+
const tree = join(root, "2026");
|
|
381
|
+
writeFileSync(join(tree, "missing-deps.txt"), "unicode-data\n");
|
|
382
|
+
const check1 = cmd({ check: true });
|
|
383
|
+
check(
|
|
384
|
+
"🔴 a dropped base dependency: --check does not call the tree complete",
|
|
385
|
+
check1.code === 1,
|
|
386
|
+
`${check1.out}\n${check1.err}`,
|
|
387
|
+
);
|
|
388
|
+
const r = cmd();
|
|
389
|
+
check(
|
|
390
|
+
"🔴 a dropped base dependency: the run installs it, then builds the missing formats",
|
|
391
|
+
r.code === 0 &&
|
|
392
|
+
readFileSync(join(tree, "tlmgr.log"), "utf8").includes("unicode-data") &&
|
|
393
|
+
readFileSync(join(tree, "fmtutil.log"), "utf8").trim() === "--missing",
|
|
394
|
+
`${r.out}\n${r.err}`,
|
|
395
|
+
);
|
|
396
|
+
check(
|
|
397
|
+
"a dropped base dependency: the tree is whole afterwards",
|
|
398
|
+
readFileSync(join(tree, "missing-deps.txt"), "utf8").trim() === "",
|
|
369
399
|
);
|
|
370
400
|
}
|
|
371
401
|
{
|
|
@@ -500,6 +530,7 @@ const mirrorFor = (name, year, installerYear = year) => {
|
|
|
500
530
|
"stub-pdflatex",
|
|
501
531
|
"stub-kpsewhich",
|
|
502
532
|
"stub-tlmgr",
|
|
533
|
+
"stub-fmtutil-sys",
|
|
503
534
|
])
|
|
504
535
|
copyFileSync(join(staging, f), join(stage, f));
|
|
505
536
|
writeFileSync(
|
|
@@ -544,7 +575,7 @@ check(
|
|
|
544
575
|
r.out.includes("TeX Live 2026") &&
|
|
545
576
|
r.out.includes("nothing to do") &&
|
|
546
577
|
!existsSync(join(years, "2027")) &&
|
|
547
|
-
!r.calls.some(
|
|
578
|
+
!r.calls.some(installs),
|
|
548
579
|
`${r.out}\n${JSON.stringify(r.calls.map((c) => c.cmd))}`,
|
|
549
580
|
);
|
|
550
581
|
}
|
|
@@ -657,4 +688,4 @@ check(
|
|
|
657
688
|
}
|
|
658
689
|
|
|
659
690
|
rmSync(work, { recursive: true, force: true });
|
|
660
|
-
console.log(`toolchain: ${
|
|
691
|
+
console.log(`toolchain: ${check.count} checks passed`);
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* toolchain.ts edges the harness (a fixture mirror) does not reach, with a scripted runner: curl
|
|
3
|
+
* that fails with and without output, a mirror serving something that is not a tarball, an archive
|
|
4
|
+
* without install-tl or without its release file, install-tl that cannot start, tlmgr with nothing
|
|
5
|
+
* to install; and the pure readers on inputs their happy paths never see.
|
|
6
|
+
*/
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { dirname } from "node:path";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { test } from "vitest";
|
|
12
|
+
import { useTempDir, writeTree } from "../test/support.mjs";
|
|
13
|
+
import {
|
|
14
|
+
cachedTrees,
|
|
15
|
+
CACHE_ENV,
|
|
16
|
+
describeGaps,
|
|
17
|
+
downloadInstaller,
|
|
18
|
+
ensureTexLive,
|
|
19
|
+
installBase,
|
|
20
|
+
installPackages,
|
|
21
|
+
releaseGap,
|
|
22
|
+
runToolchain,
|
|
23
|
+
tlYear,
|
|
24
|
+
UNSUPPORTED,
|
|
25
|
+
type ToolchainIO,
|
|
26
|
+
} from "./toolchain.ts";
|
|
27
|
+
|
|
28
|
+
const root = useTempDir("paperlint-toolchain-test-");
|
|
29
|
+
type Answer = {
|
|
30
|
+
status: number | null;
|
|
31
|
+
stdout?: string;
|
|
32
|
+
stderr?: string;
|
|
33
|
+
error?: Error;
|
|
34
|
+
};
|
|
35
|
+
/** A runner answering by program name (the last path segment); `onRun` may write files first. */
|
|
36
|
+
function io(
|
|
37
|
+
answers: Record<string, (args: string[]) => Answer>,
|
|
38
|
+
): ToolchainIO & { lines: string[] } {
|
|
39
|
+
const lines: string[] = [];
|
|
40
|
+
return {
|
|
41
|
+
lines,
|
|
42
|
+
log: (l) => lines.push(l),
|
|
43
|
+
env: {},
|
|
44
|
+
run: ((cmd: string, args: string[]) => {
|
|
45
|
+
const name = cmd.split("/").pop() ?? cmd;
|
|
46
|
+
const answer = answers[`${name} ${args[0] ?? ""}`] ?? answers[name];
|
|
47
|
+
if (!answer) throw new Error(`unexpected ${name} ${args.join(" ")}`);
|
|
48
|
+
return answer(args);
|
|
49
|
+
}) as never,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const ok = (stdout = ""): Answer => ({ status: 0, stdout, stderr: "" });
|
|
53
|
+
|
|
54
|
+
test("pure readers: a release file with no year, a `local:` line with no year, a gap with no proof file", () => {
|
|
55
|
+
assert.equal(tlYear(""), null);
|
|
56
|
+
assert.equal(releaseGap("local: soon\nrepository: 2026"), null);
|
|
57
|
+
assert.equal(
|
|
58
|
+
describeGaps(
|
|
59
|
+
{ packages: ["acmart"], tools: [], dependencies: [] },
|
|
60
|
+
{ packages: {}, tools: {} },
|
|
61
|
+
),
|
|
62
|
+
"acmart ()",
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("the cache: a year whose bin directory holds no runnable pdflatex is not a tree", () => {
|
|
67
|
+
const cache = join(root, "cache");
|
|
68
|
+
mkdirSync(join(cache, "2025", "bin", "x86_64-linux"), { recursive: true });
|
|
69
|
+
writeFileSync(join(cache, "2025", "bin", "x86_64-linux", "pdflatex"), "");
|
|
70
|
+
mkdirSync(join(cache, "2026", "bin", "x86_64-linux"), { recursive: true });
|
|
71
|
+
const exe = join(cache, "2026", "bin", "x86_64-linux", "pdflatex");
|
|
72
|
+
writeFileSync(exe, "#!/bin/sh\n");
|
|
73
|
+
chmodSync(exe, 0o755);
|
|
74
|
+
assert.deepEqual(
|
|
75
|
+
cachedTrees(cache).map((t) => t.year),
|
|
76
|
+
["2026"],
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("download: curl failing with its own line, with only an error, with only a code; then a non-tarball", () => {
|
|
81
|
+
const t = io({
|
|
82
|
+
curl: (args) => {
|
|
83
|
+
const url = args[args.length - 1] ?? "";
|
|
84
|
+
if (url.startsWith("m1"))
|
|
85
|
+
return { status: 22, stdout: "", stderr: "curl: (22) 404\n" };
|
|
86
|
+
if (url.startsWith("m2"))
|
|
87
|
+
return { status: null, error: new Error("spawn curl ENOENT") };
|
|
88
|
+
if (url.startsWith("m3")) return { status: 7 };
|
|
89
|
+
return ok();
|
|
90
|
+
},
|
|
91
|
+
"tar tzf": () => ({ status: 2 }),
|
|
92
|
+
});
|
|
93
|
+
assert.equal(
|
|
94
|
+
downloadInstaller(t, ["m1", "m2", "m3", "m4"], join(root, "x.tgz")),
|
|
95
|
+
null,
|
|
96
|
+
);
|
|
97
|
+
assert.deepEqual(t.lines, [
|
|
98
|
+
" ⚠ m1/install-tl-unx.tar.gz: curl: (22) 404",
|
|
99
|
+
" ⚠ m2/install-tl-unx.tar.gz: spawn curl ENOENT",
|
|
100
|
+
" ⚠ m3/install-tl-unx.tar.gz: curl exited 7",
|
|
101
|
+
" ⚠ m4/install-tl-unx.tar.gz returned something that is not a .tar.gz — trying the next mirror",
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
/** A runner whose download succeeds and whose `tar xzf` lays out `files` in the work directory. */
|
|
106
|
+
const unpacking = (
|
|
107
|
+
files: Record<string, string> | null,
|
|
108
|
+
more: Record<string, (args: string[]) => Answer> = {},
|
|
109
|
+
) =>
|
|
110
|
+
io({
|
|
111
|
+
curl: () => ok(),
|
|
112
|
+
"tar tzf": () => ok(),
|
|
113
|
+
"tar xzf": (args) => {
|
|
114
|
+
if (files === null) return { status: 2 };
|
|
115
|
+
writeTree(args[args.length - 1] ?? "", files);
|
|
116
|
+
return ok();
|
|
117
|
+
},
|
|
118
|
+
...more,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("install: an archive that does not unpack, holds no install-tl, or no release file", () => {
|
|
122
|
+
const bad = [
|
|
123
|
+
unpacking(null),
|
|
124
|
+
unpacking({ "other/x": "" }),
|
|
125
|
+
unpacking({ "install-tl-20260101/install-tl": "" }),
|
|
126
|
+
].map((t) => installBase(t, join(root, "tl"), ["m"]));
|
|
127
|
+
for (const r of bad)
|
|
128
|
+
assert.deepEqual(r, {
|
|
129
|
+
ok: false,
|
|
130
|
+
lines: [
|
|
131
|
+
"the archive from m does not hold an install-tl with a release-texlive.txt",
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("install: install-tl that cannot start names the error and its last lines", () => {
|
|
137
|
+
const t = unpacking(
|
|
138
|
+
{
|
|
139
|
+
"install-tl-20260101/install-tl": "",
|
|
140
|
+
"install-tl-20260101/release-texlive.txt":
|
|
141
|
+
"TeX Live (https://tug.org/texlive) version 2026\n",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"install-tl": () => ({ status: null, error: new Error("spawn EACCES") }),
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
assert.deepEqual(installBase(t, join(root, "tl2"), ["m"]), {
|
|
148
|
+
ok: false,
|
|
149
|
+
lines: ["install-tl failed (spawn EACCES) — its last lines:"],
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("tlmgr: nothing missing, nothing installed", () => {
|
|
154
|
+
const t = io({ kpsewhich: () => ok(), tlmgr: () => ok() });
|
|
155
|
+
const tree = { dir: "/t/2026", year: "2026", bin: "/t/2026/bin/x" };
|
|
156
|
+
assert.deepEqual(
|
|
157
|
+
installPackages(t, tree, { packages: {}, tools: {} }, ["m"]),
|
|
158
|
+
{
|
|
159
|
+
ok: true,
|
|
160
|
+
value: { gaps: { packages: [], tools: [], dependencies: [] }, tail: [] },
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("tlmgr: a run that prints nothing still leaves the gap to verify", () => {
|
|
166
|
+
let kpse = 0;
|
|
167
|
+
const t = io({
|
|
168
|
+
kpsewhich: () => ok(kpse++ ? "/t/acmart.cls\n" : ""),
|
|
169
|
+
"tlmgr check": () => ok(),
|
|
170
|
+
"tlmgr --repository": () => ({ status: 0 }),
|
|
171
|
+
});
|
|
172
|
+
const tree = { dir: "/t/2026", year: "2026", bin: "/t/2026/bin/x" };
|
|
173
|
+
const r = installPackages(
|
|
174
|
+
t,
|
|
175
|
+
tree,
|
|
176
|
+
{ packages: { acmart: ["acmart.cls"] }, tools: {} },
|
|
177
|
+
["m"],
|
|
178
|
+
);
|
|
179
|
+
assert.deepEqual(r, {
|
|
180
|
+
ok: true,
|
|
181
|
+
value: { gaps: { packages: [], tools: [], dependencies: [] }, tail: [] },
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
/** A cache holding one tree for `year` with a runnable pdflatex; returns the cache root. */
|
|
186
|
+
function cacheWith(name: string, year: string): string {
|
|
187
|
+
const cache = join(root, name);
|
|
188
|
+
const exe = join(cache, year, "bin", "x86_64-linux", "pdflatex");
|
|
189
|
+
mkdirSync(dirname(exe), { recursive: true });
|
|
190
|
+
writeFileSync(exe, "#!/bin/sh\n");
|
|
191
|
+
chmodSync(exe, 0o755);
|
|
192
|
+
return cache;
|
|
193
|
+
}
|
|
194
|
+
const ACMART = { packages: { acmart: ["acmart.cls"] }, tools: {} };
|
|
195
|
+
function ensure(
|
|
196
|
+
cache: string,
|
|
197
|
+
answers: Record<string, (args: string[]) => Answer>,
|
|
198
|
+
) {
|
|
199
|
+
const t = io(answers);
|
|
200
|
+
const errs: string[] = [];
|
|
201
|
+
const r = ensureTexLive(ACMART, {
|
|
202
|
+
run: t.run,
|
|
203
|
+
log: t.log,
|
|
204
|
+
err: (l) => errs.push(l),
|
|
205
|
+
env: { [CACHE_ENV]: cache },
|
|
206
|
+
home: root,
|
|
207
|
+
now: () => 0,
|
|
208
|
+
});
|
|
209
|
+
return { r, errs };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
test("ensure: tlmgr leaves the package missing — the verdict quotes tlmgr's last lines", () => {
|
|
213
|
+
const cache = cacheWith("lacks", "2026");
|
|
214
|
+
const { r, errs } = ensure(cache, {
|
|
215
|
+
kpsewhich: () => ok(),
|
|
216
|
+
"tlmgr check": () => ok(),
|
|
217
|
+
"tlmgr --repository": () =>
|
|
218
|
+
ok("tlmgr: package acmart installed, but nothing changed\n"),
|
|
219
|
+
});
|
|
220
|
+
assert.deepEqual(
|
|
221
|
+
[r, errs.slice(-2)],
|
|
222
|
+
[
|
|
223
|
+
{ ok: false },
|
|
224
|
+
[
|
|
225
|
+
" tlmgr's last lines:",
|
|
226
|
+
" tlmgr: package acmart installed, but nothing changed",
|
|
227
|
+
],
|
|
228
|
+
],
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("ensure: tlmgr refuses across releases, the new tree is installed, and tlmgr still refuses", () => {
|
|
233
|
+
const cache = cacheWith("newer", "2025");
|
|
234
|
+
const refusal = ok(
|
|
235
|
+
"tlmgr: Local TeX Live (2025) is older than remote repository (2026).\n",
|
|
236
|
+
);
|
|
237
|
+
const { r, errs } = ensure(cache, {
|
|
238
|
+
kpsewhich: () => ok(),
|
|
239
|
+
"tlmgr check": () => ok(),
|
|
240
|
+
"tlmgr --repository": () => refusal,
|
|
241
|
+
curl: () => ok(),
|
|
242
|
+
"tar tzf": () => ok(),
|
|
243
|
+
"tar xzf": (args) => {
|
|
244
|
+
writeTree(args[args.length - 1] ?? "", {
|
|
245
|
+
"install-tl-20260101/install-tl": "",
|
|
246
|
+
"install-tl-20260101/release-texlive.txt":
|
|
247
|
+
"TeX Live (https://tug.org/texlive) version 2026\n",
|
|
248
|
+
});
|
|
249
|
+
return ok();
|
|
250
|
+
},
|
|
251
|
+
"install-tl": () => {
|
|
252
|
+
const exe = join(cache, "2026", "bin", "x86_64-linux", "pdflatex");
|
|
253
|
+
mkdirSync(dirname(exe), { recursive: true });
|
|
254
|
+
writeFileSync(exe, "#!/bin/sh\n");
|
|
255
|
+
chmodSync(exe, 0o755);
|
|
256
|
+
return ok();
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
assert.deepEqual(
|
|
260
|
+
[r, errs],
|
|
261
|
+
[
|
|
262
|
+
{ ok: false },
|
|
263
|
+
[
|
|
264
|
+
`✗ paperlint toolchain: tlmgr still refuses: the repository is TeX Live 2026 and the new tree in ${join(cache, "2026")} is 2025`,
|
|
265
|
+
],
|
|
266
|
+
],
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("runToolchain with only its installer: the console is the default output", () => {
|
|
271
|
+
const seen: unknown[] = [];
|
|
272
|
+
const saved = console.error;
|
|
273
|
+
console.error = (...a: unknown[]) => void seen.push(a.join(" "));
|
|
274
|
+
try {
|
|
275
|
+
assert.equal(runToolchain({ platform: "win32", banal: {} as never }), 1);
|
|
276
|
+
} finally {
|
|
277
|
+
console.error = saved;
|
|
278
|
+
}
|
|
279
|
+
assert.deepEqual(seen, [`✗ paperlint toolchain: ${UNSUPPORTED}`]);
|
|
280
|
+
});
|
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,
|
|
@@ -161,8 +162,8 @@ export function mirrorsFrom(env: NodeJS.ProcessEnv): readonly string[] {
|
|
|
161
162
|
|
|
162
163
|
/** `release-texlive.txt`'s first line: "TeX Live (https://tug.org/texlive) version 2026". */
|
|
163
164
|
export function tlYear(release: string): string | null {
|
|
164
|
-
|
|
165
|
-
return
|
|
165
|
+
// `^[^\n]*`: the first line only — a year on a later line is not the release.
|
|
166
|
+
return /^[^\n]*\bversion (\d{4})\b/.exec(release)?.[1] ?? null;
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
/** The install-tl profile: scheme-basic, no docs or sources, every tree variable inside `dir`. */
|
|
@@ -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
|
|
|
@@ -348,7 +359,9 @@ function unpack(io: ToolchainIO, archive: string, work: string): string | null {
|
|
|
348
359
|
return dir ? join(work, dir) : null;
|
|
349
360
|
}
|
|
350
361
|
|
|
351
|
-
|
|
362
|
+
/** A failed step always says why: its first line is the headline, the rest detail. */
|
|
363
|
+
export type Step<T> =
|
|
364
|
+
{ ok: true; value: T } | { ok: false; lines: [string, ...string[]] };
|
|
352
365
|
|
|
353
366
|
/** Download, unpack and run install-tl: a fresh scheme-basic tree in `<root>/<year>`. */
|
|
354
367
|
export function installBase(
|
|
@@ -447,7 +460,7 @@ function runInstallTl(
|
|
|
447
460
|
*/
|
|
448
461
|
export type AddStep =
|
|
449
462
|
| { readonly ok: true; readonly value: { gaps: Gaps; tail: string[] } }
|
|
450
|
-
| { readonly ok: false; readonly lines: string[] }
|
|
463
|
+
| { readonly ok: false; readonly lines: [string, ...string[]] }
|
|
451
464
|
| { readonly ok: false; readonly newer: ReleaseGap };
|
|
452
465
|
|
|
453
466
|
export function installPackages(
|
|
@@ -460,7 +473,8 @@ export function installPackages(
|
|
|
460
473
|
let last: string[] = [];
|
|
461
474
|
for (const mirror of mirrors) {
|
|
462
475
|
if (noGaps(gaps)) break;
|
|
463
|
-
const want = [...gaps.packages, ...gaps.tools];
|
|
476
|
+
const want = [...gaps.packages, ...gaps.tools, ...gaps.dependencies];
|
|
477
|
+
const repairsBase = gaps.dependencies.length > 0;
|
|
464
478
|
io.log(` tlmgr: installing ${want.length} package(s) from ${mirror}…`);
|
|
465
479
|
const r = io.run(
|
|
466
480
|
join(tree.bin, "tlmgr"),
|
|
@@ -481,6 +495,10 @@ export function installPackages(
|
|
|
481
495
|
],
|
|
482
496
|
};
|
|
483
497
|
last = tail(r);
|
|
498
|
+
// A dependency of the base was missing, so the formats it feeds were never built (pdflatex dies
|
|
499
|
+
// before writing a log without them). Build the missing ones now that the package is in.
|
|
500
|
+
if (repairsBase)
|
|
501
|
+
io.run(join(tree.bin, "fmtutil-sys"), ["--missing"], quiet(io, TLMGR_MS));
|
|
484
502
|
gaps = gapsOf(tree, tex, io.run);
|
|
485
503
|
}
|
|
486
504
|
return { ok: true, value: { gaps, tail: last } };
|
|
@@ -638,10 +656,9 @@ export const binLine = (tree: CachedTree): string => ` bin: ${tree.bin}`;
|
|
|
638
656
|
|
|
639
657
|
function fail(
|
|
640
658
|
err: (line: string) => void,
|
|
641
|
-
|
|
659
|
+
[head, ...rest]: readonly [string, ...string[]],
|
|
642
660
|
): InstallResult {
|
|
643
|
-
|
|
644
|
-
err(`✗ paperlint toolchain: ${head ?? "failed"}`);
|
|
661
|
+
err(`✗ paperlint toolchain: ${head}`);
|
|
645
662
|
for (const l of rest) err(` ${l}`);
|
|
646
663
|
return { ok: false };
|
|
647
664
|
}
|
|
@@ -688,9 +705,17 @@ function report(o: Resolved, tree: CachedTree | null): number {
|
|
|
688
705
|
o.log(binLine(tree));
|
|
689
706
|
return 0;
|
|
690
707
|
}
|
|
691
|
-
const
|
|
708
|
+
const declared = gaps.packages.length + gaps.tools.length;
|
|
709
|
+
// A dependency the install left out is not one of the venue's declared packages: counted apart,
|
|
710
|
+
// so the line never reads "lacks 0 of 51" followed by a name.
|
|
711
|
+
const what = [
|
|
712
|
+
...(declared ? [`${declared} of ${n} declared packages`] : []),
|
|
713
|
+
...(gaps.dependencies.length
|
|
714
|
+
? [`${gaps.dependencies.length} TeX Live base package(s)`]
|
|
715
|
+
: []),
|
|
716
|
+
].join(" and ");
|
|
692
717
|
o.log(
|
|
693
|
-
`✗ TeX Live ${tree.year} in ${tree.dir} lacks ${
|
|
718
|
+
`✗ TeX Live ${tree.year} in ${tree.dir} lacks ${what}: ${describeGaps(gaps, o.tex)}`,
|
|
694
719
|
);
|
|
695
720
|
o.log(` run \`npx paperlint toolchain\` to install them`);
|
|
696
721
|
return 1;
|
package/src/types.ts
CHANGED
|
@@ -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. */
|
package/src/venue-rules.test.ts
CHANGED
|
@@ -113,6 +113,7 @@ function lint(
|
|
|
113
113
|
p: Paper,
|
|
114
114
|
filename = `${PAPER}/paper.tex`,
|
|
115
115
|
options: Json = {},
|
|
116
|
+
cwd?: string,
|
|
116
117
|
): Finding[] {
|
|
117
118
|
const files = memoryFiles(paperFiles(p));
|
|
118
119
|
const rules = venueRules({ files, venuesDir: VENUES });
|
|
@@ -123,6 +124,7 @@ function lint(
|
|
|
123
124
|
][]) {
|
|
124
125
|
const visitor = rule.create({
|
|
125
126
|
filename,
|
|
127
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
126
128
|
sourceCode: { text: TEX },
|
|
127
129
|
options:
|
|
128
130
|
name === "geometry" && Object.keys(options).length ? [options] : [],
|
|
@@ -316,6 +318,9 @@ describe("pdf/measured — a paper that names a venue and was not measured says
|
|
|
316
318
|
const fs = lint({ venue: DECL, facts: null, pdf: null });
|
|
317
319
|
expect(ids(fs)).toEqual(["pdf/measured:unbuilt"]);
|
|
318
320
|
expect(fs[0]?.message).toMatch(/paperlint build/);
|
|
321
|
+
// Guards: the message stays one short line — it is the first thing a new user sees.
|
|
322
|
+
expect(fs[0]?.message).toMatch(/^not built yet, so /);
|
|
323
|
+
expect(fs[0]?.message.length).toBeLessThan(110);
|
|
319
324
|
});
|
|
320
325
|
|
|
321
326
|
it("facts without geometry (no banal): pdf/measured names it, the three geometry rules are silent, fonts still judge", () => {
|
|
@@ -552,3 +557,61 @@ describe("paperlint's own config turns the venue rules on for every paper.tex",
|
|
|
552
557
|
expect(OPTIONAL_RULES.has("pdf/last-page-balance")).toBe(true);
|
|
553
558
|
});
|
|
554
559
|
});
|
|
560
|
+
|
|
561
|
+
describe("a project's own preset, and the facts' other spellings", () => {
|
|
562
|
+
// A standalone preset (no `extends`, so it carries a `tex` block) that sets a body size with no
|
|
563
|
+
// tolerance, a reference range, two columns — and no page size and no font families.
|
|
564
|
+
const HOUSE = {
|
|
565
|
+
[`${PAPER}/house.jsonc`]: JSON.stringify({
|
|
566
|
+
tex: { packages: { acmart: ["acmart.cls"] } },
|
|
567
|
+
format: { columns: 2, body_pt: 9, ref_pt_min: 7, ref_pt_max: 8 },
|
|
568
|
+
}),
|
|
569
|
+
};
|
|
570
|
+
const house = (patch: (f: Json) => void = () => {}) =>
|
|
571
|
+
lint({
|
|
572
|
+
venue: { extends: "./house.jsonc" },
|
|
573
|
+
facts: withFacts(patch),
|
|
574
|
+
extra: HOUSE,
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it("no page size in the preset: nothing to compare, whatever the PDF measures", () => {
|
|
578
|
+
expect(ids(house((f) => ((f.page_w_in = 5), (f.page_h_in = 5))))).toEqual(
|
|
579
|
+
[],
|
|
580
|
+
);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it("a body size with no tolerance is not judged; a reference range with none is judged exactly", () => {
|
|
584
|
+
expect(ids(house((f) => (f.body_pt = 12)))).toEqual([]);
|
|
585
|
+
// 6.9 pt passes agenticdev (its body_pt_tol widens the range) and fails here, where nothing does.
|
|
586
|
+
expect(ids(house((f) => (f.ref_pt = 6.9)))).toEqual(["pdf/limits:refPt"]);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it("facts naming the PDF by an absolute path are checked against that file", () => {
|
|
590
|
+
expect(
|
|
591
|
+
lint({
|
|
592
|
+
venue: DECL,
|
|
593
|
+
facts: withFacts((f) => (f.pdf = `${PAPER}/paper.pdf`)),
|
|
594
|
+
}),
|
|
595
|
+
).toEqual([]);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it("a PDF with no fonts at all: each missing family says so", () => {
|
|
599
|
+
const fs = lint({ venue: DECL, facts: withFacts((f) => (f.fonts = [])) });
|
|
600
|
+
expect(ids(fs)).toEqual(["pdf/fonts:noFamily", "pdf/fonts:noFamily"]);
|
|
601
|
+
expect(fs[0]?.message).toMatch(/\(no fonts\)/);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it("file names in messages are relative to ESLint's cwd — and never empty", () => {
|
|
605
|
+
const at = (cwd: string) =>
|
|
606
|
+
lint(
|
|
607
|
+
{ venue: { kind: "short" }, facts: null, pdf: null },
|
|
608
|
+
undefined,
|
|
609
|
+
{},
|
|
610
|
+
cwd,
|
|
611
|
+
)[0]?.message;
|
|
612
|
+
expect(at("/work")).toMatch(/set "extends" in papers\/p\/paperlint\.json/);
|
|
613
|
+
expect(at(`${PAPER}/paperlint.json`)).toMatch(
|
|
614
|
+
/set "extends" in \/work\/papers\/p\/paperlint\.json/,
|
|
615
|
+
);
|
|
616
|
+
});
|
|
617
|
+
});
|