paperlint 2.0.0 → 2.0.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 +4 -4
- package/CLAUDE.md +3 -3
- package/LICENSE +1 -1
- package/README.md +25 -10
- 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.js +1 -1
- package/dist/build.js +1 -1
- package/dist/cli.d.ts +14 -29
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +36 -74
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +0 -2
- 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/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 +2 -10
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +7 -39
- package/dist/init.js.map +1 -1
- package/dist/link-skills.js +2 -2
- package/dist/link-skills.js.map +1 -1
- package/dist/rules-config.d.ts +1 -1
- package/dist/rules-config.js +2 -2
- 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/docs/configuration.md +6 -10
- package/docs/e2e.md +3 -4
- package/docs/install.md +11 -3
- package/docs/optional-rules.md +1 -1
- package/docs/rules.md +1 -1
- 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/pdf-last-page-balance.harness.mjs +5 -3
- 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.harness.mjs +3 -3
- package/lib/paper-config.mjs +3 -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/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 +1 -1
- package/skills/submit-paper/references/venues/venue-profile.schema.json +1 -1
- 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 +1 -1
- package/src/build.harness.mjs +6 -6
- package/src/build.mutations.mjs +3 -2
- package/src/build.ts +1 -1
- package/src/cli.harness.mjs +68 -129
- package/src/cli.mutations.mjs +7 -32
- package/src/cli.ts +46 -92
- package/src/doctor.harness.mjs +9 -25
- package/src/doctor.ts +0 -2
- package/src/engine.harness.mjs +4 -2
- package/src/engine.ts +2 -2
- 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 +7 -49
- 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 +1 -1
- package/src/pdf-facts.harness.mjs +1 -1
- package/src/rules-config.ts +2 -2
- package/src/structure.harness.mjs +1 -1
- package/src/tex-requirements.harness.mjs +1 -1
- package/src/toolchain.harness.mjs +34 -25
- package/src/toolchain.ts +14 -8
- package/src/types.ts +4 -4
- package/plugin/.claude-plugin/plugin.json +0 -8
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"issue": "https://github.com/zernie/
|
|
2
|
+
"issue": "https://github.com/zernie/paperlint/issues/77",
|
|
3
3
|
"why": "A *.harness.* file tests the agent surface and imports runHook, runHarnessTest or runEval from vigiles. Plain unit tests are *.test.ts, run by vitest. These are the harness files that existed on main on 2026-09-25 and import none of the three. The list only shrinks: rename a file to *.test.ts (issue #77), or make it a real harness, then delete its entry. Checked by scripts/harness-api.test.ts, part of npm test.",
|
|
4
4
|
"harnesses": [
|
|
5
5
|
"action.harness.mjs",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* The rule is read from the file's IMPORTS, parsed with the TypeScript compiler — not from its
|
|
7
7
|
* folder and not from a text search: a comment or a string that mentions `runHook` is not an import,
|
|
8
|
-
* and `src/cli.harness.mjs` calls a function of
|
|
8
|
+
* and `src/cli.harness.mjs` calls a function of paperlint's own that happens to be called `runHook`.
|
|
9
9
|
*
|
|
10
10
|
* Harnesses that predate the rule and import none of the three are frozen in
|
|
11
11
|
* `harness-api.frozen.json` (issue #77). That list only shrinks: an entry that now imports the API,
|
|
@@ -156,7 +156,7 @@ test("an import is what counts — not a mention, not a same-named local functio
|
|
|
156
156
|
const mention =
|
|
157
157
|
'// runHook\nconst s = "runHarnessTest";\nimport { x } from "vigiles";\n';
|
|
158
158
|
assert.equal(importsAgentApi("a.harness.mjs", mention), false);
|
|
159
|
-
// Guards:
|
|
159
|
+
// Guards: paperlint's own `runHook` (src/cli.ts) is not vigiles'.
|
|
160
160
|
assert.equal(
|
|
161
161
|
importsAgentApi(
|
|
162
162
|
"b.harness.mjs",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"issue": "https://github.com/zernie/
|
|
2
|
+
"issue": "https://github.com/zernie/paperlint/issues/76",
|
|
3
3
|
"why": "Legacy src/*.ts files that still do I/O or import an adapter or a tool package directly. May only shrink: fix a site, delete its directive, lower the count here (delete the file entry at zero). A new file may not appear. Counted from ESLint suppressedMessages by scripts/layer-legacy-frozen.mjs, part of npm run check.",
|
|
4
4
|
"files": {
|
|
5
5
|
"src/build-engine.ts": {
|
|
@@ -60,7 +60,7 @@ const driverBattery = (k) =>
|
|
|
60
60
|
`import { runMutations } from "./lib/mutation-driver.mjs";\n` +
|
|
61
61
|
`process.exit(runMutations({ root: ".", cases: [${Array.from({ length: k }, (_, i) => `{ name: "c${String(i)}" }`).join(", ")}] }));\n`;
|
|
62
62
|
|
|
63
|
-
const TMP = realpathSync(mkdtempSync(join(tmpdir(), "
|
|
63
|
+
const TMP = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-frozen-h-")));
|
|
64
64
|
process.on("exit", () => rmSync(TMP, { recursive: true, force: true }));
|
|
65
65
|
mkdirSync(join(TMP, "scripts"), { recursive: true });
|
|
66
66
|
mkdirSync(join(TMP, "src"), { recursive: true });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"issue": "https://github.com/zernie/
|
|
2
|
+
"issue": "https://github.com/zernie/paperlint/issues/52",
|
|
3
3
|
"why": "Hand-written *.mutations.mjs batteries are being removed (#52). This list may only shrink: no new batteries, no new cases in existing ones. Delete a battery or a case, then delete its entry or lower its count here. Checked by scripts/mutation-batteries-frozen.mjs, part of npm run check.",
|
|
4
4
|
"batteries": {
|
|
5
5
|
"action.mutations.mjs": {
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"cases": 11
|
|
97
97
|
},
|
|
98
98
|
"src/cli.mutations.mjs": {
|
|
99
|
-
"cases":
|
|
99
|
+
"cases": 26
|
|
100
100
|
},
|
|
101
101
|
"src/doctor.mutations.mjs": {
|
|
102
102
|
"cases": 10
|
|
@@ -53,7 +53,7 @@ const fix = {
|
|
|
53
53
|
const context = {
|
|
54
54
|
cwd: process.cwd(),
|
|
55
55
|
options: {
|
|
56
|
-
repositoryUrl: "https://github.com/zernie/
|
|
56
|
+
repositoryUrl: "https://github.com/zernie/paperlint",
|
|
57
57
|
},
|
|
58
58
|
lastRelease: { gitTag: "v1.0.0", version: "1.0.0" },
|
|
59
59
|
nextRelease: { gitTag: "v2.0.0", version: "2.0.0", type: "major" },
|
|
@@ -236,7 +236,7 @@ const CASES = [
|
|
|
236
236
|
|
|
237
237
|
// Preflight: a misspelled skill or a changed namespace makes every `fired` predicate permanently
|
|
238
238
|
// false, and the run then reports a wall of confident 0.00s as though the descriptions were dead.
|
|
239
|
-
// Through `installedSkills`, which follows the links `paperlint init` makes (
|
|
239
|
+
// Through `installedSkills`, which follows the links `paperlint init` makes (paperlint#62).
|
|
240
240
|
const installed = new Set(installedSkills(SKILLS_DIR));
|
|
241
241
|
for (const c of CASES) {
|
|
242
242
|
if (!installed.has(c.skill))
|
|
@@ -433,7 +433,7 @@ const CELL_LABEL = {
|
|
|
433
433
|
// as though the descriptions were dead. That is the failure mode this block exists to prevent.
|
|
434
434
|
|
|
435
435
|
// 1. the skills exist and declare the names we test against
|
|
436
|
-
// Through `installedSkills`, which follows the links `paperlint init` makes (
|
|
436
|
+
// Through `installedSkills`, which follows the links `paperlint init` makes (paperlint#62).
|
|
437
437
|
const installed = installedSkills(SKILLS_DIR);
|
|
438
438
|
const installedSet = new Set(installed);
|
|
439
439
|
for (const c of CASES) {
|
|
@@ -586,7 +586,7 @@ if (ONLY.length && RUN_ARMS.length !== ONLY.length)
|
|
|
586
586
|
// a wall of confident 0.00s that read like a finding.
|
|
587
587
|
|
|
588
588
|
// 1. the roster is what the parent measured
|
|
589
|
-
// Through `installedSkills`, which follows the links `paperlint init` makes (
|
|
589
|
+
// Through `installedSkills`, which follows the links `paperlint init` makes (paperlint#62).
|
|
590
590
|
const installed = installedSkills(SKILLS_DIR);
|
|
591
591
|
const installedSet = new Set(installed);
|
|
592
592
|
if (installedSet.has(ARMS[5].rename))
|
|
@@ -737,7 +737,7 @@ function buildArm(a) {
|
|
|
737
737
|
rmSync(dir, { recursive: true, force: true });
|
|
738
738
|
mkdirSync(dir, { recursive: true });
|
|
739
739
|
// 🔴 ONE SKILL AT A TIME, FROM ITS REALPATH — not `cpSync(SKILLS_DIR, dir, { recursive })`.
|
|
740
|
-
// In a consumer every entry of SKILLS_DIR is a link `paperlint init` made (
|
|
740
|
+
// In a consumer every entry of SKILLS_DIR is a link `paperlint init` made (paperlint#62), and `cpSync` copies
|
|
741
741
|
// a link as a link, rewritten to an ABSOLUTE path into the original; `dereference: true` does not
|
|
742
742
|
// change that for nested entries (measured, Node 22.22). The arm's `writeFileSync` below would
|
|
743
743
|
// then land in the real SKILL.md — "the real one is never written" would be false in exactly the
|
|
@@ -397,7 +397,7 @@ const irrelevantFor = (c) => {
|
|
|
397
397
|
function assertSkillIdsExist() {
|
|
398
398
|
if (!existsSync(SKILLS_DIR))
|
|
399
399
|
throw new Error(`no skills dir at ${SKILLS_DIR}`);
|
|
400
|
-
// Through `installedSkills`, which follows the links `paperlint init` makes (
|
|
400
|
+
// Through `installedSkills`, which follows the links `paperlint init` makes (paperlint#62).
|
|
401
401
|
const installed = new Set(installedSkills(SKILLS_DIR));
|
|
402
402
|
const missing = CASES.map((c) => c.skill).filter((s) => !installed.has(s));
|
|
403
403
|
if (missing.length)
|
|
@@ -449,7 +449,7 @@ const CASES = [
|
|
|
449
449
|
function assertSkillIdsExist() {
|
|
450
450
|
if (!existsSync(SKILLS_DIR))
|
|
451
451
|
throw new Error(`no skills dir at ${SKILLS_DIR}`);
|
|
452
|
-
// Through `installedSkills`, which follows the links `paperlint init` makes (
|
|
452
|
+
// Through `installedSkills`, which follows the links `paperlint init` makes (paperlint#62).
|
|
453
453
|
const installed = new Set(installedSkills(SKILLS_DIR));
|
|
454
454
|
const missing = CASES.map((c) => c.skill).filter((s) => !installed.has(s));
|
|
455
455
|
if (missing.length)
|
|
@@ -30,3 +30,4 @@ export declare function packageVenuesDir(): string;
|
|
|
30
30
|
export declare const SHIPPED_SKILLS_DIR: string;
|
|
31
31
|
export declare const PACKAGE_NAME: "paperlint";
|
|
32
32
|
export declare const LEGACY_PACKAGE_NAME: "research-paper-pipeline";
|
|
33
|
+
export declare const BIN_FILE: "bin/paperlint.mjs";
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
pipelineScripts,
|
|
44
44
|
scriptsRoot,
|
|
45
45
|
PACKAGE_NAME,
|
|
46
|
+
BIN_FILE,
|
|
46
47
|
} from "./consumer.mjs";
|
|
47
48
|
|
|
48
49
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -63,6 +64,11 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
63
64
|
[PACKAGE_NAME],
|
|
64
65
|
"the package exposes exactly one command, named like the package",
|
|
65
66
|
);
|
|
67
|
+
assert.equal(
|
|
68
|
+
manifest.bin?.[PACKAGE_NAME],
|
|
69
|
+
BIN_FILE,
|
|
70
|
+
"package.json `bin` points at BIN_FILE from consumer.mjs — the file every hook command runs",
|
|
71
|
+
);
|
|
66
72
|
const wiring = JSON.parse(
|
|
67
73
|
readFileSync(join(root, "plugin", "hooks", "hooks.json"), "utf8"),
|
|
68
74
|
);
|
|
@@ -73,9 +79,9 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
73
79
|
assert.ok(
|
|
74
80
|
commands.length > 0 &&
|
|
75
81
|
commands.every((c) =>
|
|
76
|
-
c.includes(`/node_modules/${PACKAGE_NAME}
|
|
82
|
+
c.includes(`/node_modules/${PACKAGE_NAME}/${BIN_FILE}`),
|
|
77
83
|
),
|
|
78
|
-
`every command in plugin/hooks/hooks.json runs node_modules/${PACKAGE_NAME}
|
|
84
|
+
`every command in plugin/hooks/hooks.json runs node_modules/${PACKAGE_NAME}/${BIN_FILE}`,
|
|
79
85
|
);
|
|
80
86
|
}
|
|
81
87
|
const TMP = realpathSync(mkdtempSync(join(tmpdir(), "consumer-harness-")));
|
|
@@ -472,7 +478,7 @@ assert.equal(
|
|
|
472
478
|
);
|
|
473
479
|
}
|
|
474
480
|
|
|
475
|
-
// ── XI. WHICH SKILLS ARE INSTALLED — a link counts, a dangling link is REFUSED (
|
|
481
|
+
// ── XI. WHICH SKILLS ARE INSTALLED — a link counts, a dangling link is REFUSED (paperlint#62) ────────
|
|
476
482
|
// `paperlint init` installs every skill as a SYMLINK `.claude/skills/<name> -> …/skills/<name>`. A
|
|
477
483
|
// `Dirent` from `readdirSync(…, { withFileTypes: true })` describes the entry ITSELF and does not
|
|
478
484
|
// follow links, so `e.isDirectory()` is false for every one of them: the eval preflights that
|
|
@@ -506,7 +512,7 @@ assert.equal(
|
|
|
506
512
|
const names = installedSkills(home);
|
|
507
513
|
assert.ok(
|
|
508
514
|
names.includes("linked"),
|
|
509
|
-
"a SYMLINKED skill was not counted as installed. This is
|
|
515
|
+
"a SYMLINKED skill was not counted as installed. This is paperlint#62: `paperlint init` installs every " +
|
|
510
516
|
"skill as a link, so a reader that does not follow links sees none of them in any consumer.",
|
|
511
517
|
);
|
|
512
518
|
assert.deepEqual(
|
|
@@ -151,6 +151,13 @@ export const PACKAGE_NAME = "paperlint";
|
|
|
151
151
|
*/
|
|
152
152
|
export const LEGACY_PACKAGE_NAME = "research-paper-pipeline";
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* The package's one executable, relative to the package root — what `bin` in package.json names
|
|
156
|
+
* and what every hook command runs. Named like the package; `consumer.harness.mjs` checks it
|
|
157
|
+
* against the manifest, so the file and the hook commands cannot drift apart.
|
|
158
|
+
*/
|
|
159
|
+
export const BIN_FILE = `bin/${PACKAGE_NAME}.mjs`;
|
|
160
|
+
|
|
154
161
|
/**
|
|
155
162
|
* Where the package keeps the skills it ships, relative to the package root. One constant, read by
|
|
156
163
|
* the linker (`src/link-skills.ts`) and the install e2e, so the two cannot disagree; `files` in
|
|
@@ -168,7 +175,7 @@ export function consumerSkillsDir(opts) {
|
|
|
168
175
|
* sorted by name. The one answer to "which skills are here" — for the consumer's
|
|
169
176
|
* `.claude/skills/`, for this package's own declared skills directory, and for a copy of either.
|
|
170
177
|
*
|
|
171
|
-
* 🔴 IT FOLLOWS SYMLINKS, AND THAT IS THE WHOLE POINT (
|
|
178
|
+
* 🔴 IT FOLLOWS SYMLINKS, AND THAT IS THE WHOLE POINT (paperlint#62). `paperlint init` (`src/link-skills.ts`)
|
|
172
179
|
* installs every skill as a link `.claude/skills/<name> -> …/skills/<name>`. A `Dirent` from
|
|
173
180
|
* `readdirSync(dir, { withFileTypes: true })` describes the entry itself, so `isDirectory()` is
|
|
174
181
|
* false for every link: five eval preflights asked the question that way, saw zero skills in
|
|
@@ -117,7 +117,7 @@ const MUTATIONS = [
|
|
|
117
117
|
"the boundary between the pipeline's scripts and a directory merely named like them",
|
|
118
118
|
],
|
|
119
119
|
|
|
120
|
-
// ── `installedSkills` (
|
|
120
|
+
// ── `installedSkills` (paperlint#62). Its defects fail toward a WRONG ROSTER, not an error: a reader
|
|
121
121
|
// that does not follow links sees zero skills in every consumer; one that skips a dangling link
|
|
122
122
|
// reports a broken install as "not installed"; one that counts it hands out an unreadable skill.
|
|
123
123
|
[
|
|
@@ -4,7 +4,7 @@ description: Compile a LaTeX paper (ACM/IEEE/arXiv) to PDF and render its pages
|
|
|
4
4
|
allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, SendUserFile]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<!-- vigiles:sha256:
|
|
7
|
+
<!-- vigiles:sha256:0943963cae95f8f1 compiled from skills/render-paper/SKILL.md.spec.ts -->
|
|
8
8
|
|
|
9
9
|
# render-paper — .tex → PDF → readable page PNGs
|
|
10
10
|
|
|
@@ -38,7 +38,7 @@ That list was replaced on 2026-08-03; nothing about the toolchain below changed.
|
|
|
38
38
|
## Toolchain (one command, not a checklist)
|
|
39
39
|
- **Compiler:** TeX Live, installed by paperlint with exactly the packages the venue profiles declare:
|
|
40
40
|
```
|
|
41
|
-
npx paperlint toolchain # into ~/.cache/
|
|
41
|
+
npx paperlint toolchain # into ~/.cache/paperlint/texlive (PAPERLINT_TEXLIVE_DIR overrides); idempotent
|
|
42
42
|
npx paperlint toolchain --check # report what is missing, change nothing
|
|
43
43
|
```
|
|
44
44
|
Measured 2026-09-24: 3 min from an empty directory, 269 MB, TeX Live 2026, 47 packages verified
|
|
@@ -50,7 +50,7 @@ That list was replaced on 2026-08-03; nothing about the toolchain below changed.
|
|
|
50
50
|
package gets it there — never in a script, never here. A TeX Live without `libertine` builds an
|
|
51
51
|
acmart paper GREEN in Computer Modern; that is why the files are checked, not the exit code.
|
|
52
52
|
- **Reading the PDF** needs nothing installed: `paperlint build` and `extract-pdf-facts.mjs` use pdf.js,
|
|
53
|
-
which comes with
|
|
53
|
+
which comes with paperlint. Page size, columns and font sizes come from banal (HotCRP's page-geometry
|
|
54
54
|
script), which `npx paperlint toolchain` installs and which runs on pdf.js output — it needs `perl`,
|
|
55
55
|
and no poppler. Without banal those facts are `null` and the build says so.
|
|
56
56
|
- **Renderer:** `pip install --quiet pymupdf` (system rasterizers and ghostscript are often missing or
|
|
@@ -44,7 +44,7 @@ That list was replaced on 2026-08-03; nothing about the toolchain below changed.
|
|
|
44
44
|
## Toolchain (one command, not a checklist)
|
|
45
45
|
- **Compiler:** TeX Live, installed by paperlint with exactly the packages the venue profiles declare:
|
|
46
46
|
\`\`\`
|
|
47
|
-
npx paperlint toolchain # into ~/.cache/
|
|
47
|
+
npx paperlint toolchain # into ~/.cache/paperlint/texlive (PAPERLINT_TEXLIVE_DIR overrides); idempotent
|
|
48
48
|
npx paperlint toolchain --check # report what is missing, change nothing
|
|
49
49
|
\`\`\`
|
|
50
50
|
Measured 2026-09-24: 3 min from an empty directory, 269 MB, TeX Live 2026, 47 packages verified
|
|
@@ -56,7 +56,7 @@ That list was replaced on 2026-08-03; nothing about the toolchain below changed.
|
|
|
56
56
|
package gets it there — never in a script, never here. A TeX Live without \`libertine\` builds an
|
|
57
57
|
acmart paper GREEN in Computer Modern; that is why the files are checked, not the exit code.
|
|
58
58
|
- **Reading the PDF** needs nothing installed: \`paperlint build\` and \`extract-pdf-facts.mjs\` use pdf.js,
|
|
59
|
-
which comes with
|
|
59
|
+
which comes with paperlint. Page size, columns and font sizes come from banal (HotCRP's page-geometry
|
|
60
60
|
script), which \`npx paperlint toolchain\` installs and which runs on pdf.js output — it needs \`perl\`,
|
|
61
61
|
and no poppler. Without banal those facts are \`null\` and the build says so.
|
|
62
62
|
- **Renderer:** \`pip install --quiet pymupdf\` (system rasterizers and ghostscript are often missing or
|
|
@@ -36,7 +36,7 @@ const check = (label, cond, detail = "") => {
|
|
|
36
36
|
n++;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
const root = realpathSync(mkdtempSync(join(tmpdir(), "
|
|
39
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-extract-")));
|
|
40
40
|
/**
|
|
41
41
|
* Run the shim from `root`, with no banal anywhere unless `env` names one. `HOME` is the temp root:
|
|
42
42
|
* without it Node falls back to the account's home, where `paperlint toolchain` may have installed banal.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "https://github.com/zernie/
|
|
3
|
+
"$id": "https://github.com/zernie/paperlint/venue-profile.schema.json",
|
|
4
4
|
"title": "Venue profile",
|
|
5
5
|
"description": "One venue's format profile (venues/<venue>.jsonc) and the TeX base set (venues/tex-base.jsonc). The format numbers are read by the consumer's pdf/profile rule; `tex` is read by `paperlint toolchain` and `paperlint build`.",
|
|
6
6
|
"type": "object",
|
|
@@ -54,7 +54,7 @@ test("with $BANAL: banal gets the .xml and a quoted $PDFTOHTML, and the geometry
|
|
|
54
54
|
[2, 1],
|
|
55
55
|
);
|
|
56
56
|
const c = run.calls[0];
|
|
57
|
-
// Guards: the poppler-free path — banal is handed
|
|
57
|
+
// Guards: the poppler-free path — banal is handed paperlint's XML, never the PDF.
|
|
58
58
|
assert.match(c?.args.at(-1) ?? "", /\.xml$/);
|
|
59
59
|
assert.match(c?.env["PDFTOHTML"] ?? "", /^'.*pdftohtml'$/);
|
|
60
60
|
});
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
import type { LocatedBanal } from "./locate.ts";
|
|
23
23
|
import { XML_DIALECT } from "./xml.ts";
|
|
24
24
|
|
|
25
|
-
test("the staging holds BOTH files: the XML and the -v stub answering the dialect
|
|
25
|
+
test("the staging holds BOTH files: the XML and the -v stub answering the dialect paperlint writes", () => {
|
|
26
26
|
const [xml, stub] = stageBanalInput([]).files;
|
|
27
27
|
assert.equal(xml.name, "paper.xml");
|
|
28
28
|
assert.match(xml.content, /^<\?xml/);
|
|
@@ -50,14 +50,14 @@ test("shQuote survives a single quote", () => {
|
|
|
50
50
|
assert.equal(shQuote("it's"), `'it'"'"'s'`);
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
const root = realpathSync(mkdtempSync(join(tmpdir(), "
|
|
53
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-stage-test-")));
|
|
54
54
|
after(() => rmSync(root, { recursive: true, force: true }));
|
|
55
55
|
|
|
56
56
|
test("🔴 stage: on a real scratch directory the stub answers `-v` through the shell, from a path with a space and a quote", () => {
|
|
57
57
|
const awkward = join(root, "it's a dir");
|
|
58
58
|
mkdirSync(awkward);
|
|
59
59
|
const { workspace } = nodeAdapters({ tmpDir: awkward });
|
|
60
|
-
workspace.within("
|
|
60
|
+
workspace.within("paperlint-banal-", (s) => {
|
|
61
61
|
const staged = stage(s, stageBanalInput([]));
|
|
62
62
|
assert.ok(existsSync(staged.xml));
|
|
63
63
|
// Guards: banal runs `$PDFTOHTML -v 2>&1 |` through /bin/sh, unquoted.
|
|
@@ -30,13 +30,13 @@ export const shQuote = (s: string): ShQuoted =>
|
|
|
30
30
|
/** The stub standing in for `pdftohtml`: it answers `-v` and refuses to convert anything. */
|
|
31
31
|
export const PDFTOHTML_STUB = [
|
|
32
32
|
"#!/bin/sh",
|
|
33
|
-
"#
|
|
34
|
-
"# asks `pdftohtml -v` which dialect to expect; this answers with the one
|
|
33
|
+
"# paperlint: banal reads the banal input XML it writes, never a PDF. banal still",
|
|
34
|
+
"# asks `pdftohtml -v` which dialect to expect; this answers with the one paperlint writes.",
|
|
35
35
|
'if [ "$1" = "-v" ]; then',
|
|
36
36
|
` echo "pdftohtml version ${XML_DIALECT.version}"`,
|
|
37
37
|
" exit 0",
|
|
38
38
|
"fi",
|
|
39
|
-
'echo "
|
|
39
|
+
'echo "paperlint: this pdftohtml only answers -v; banal was given a PDF instead of the banal input XML" >&2',
|
|
40
40
|
"exit 1",
|
|
41
41
|
"",
|
|
42
42
|
].join("\n");
|
|
@@ -8,7 +8,7 @@ import { parseBanalSettings } from "./settings.ts";
|
|
|
8
8
|
|
|
9
9
|
const dirs = { home: "/h", tmp: "/t", cwd: "/w" };
|
|
10
10
|
const project = "/p" as AbsolutePath;
|
|
11
|
-
const cache = `/h/.cache/
|
|
11
|
+
const cache = `/h/.cache/paperlint/banal/${BANAL_PIN.commit.slice(0, 12)}/banal`;
|
|
12
12
|
const on =
|
|
13
13
|
(...files: string[]) =>
|
|
14
14
|
(p: string) =>
|
|
@@ -22,11 +22,11 @@ test("installedBanal: one directory per HotCRP commit", () => {
|
|
|
22
22
|
assert.equal(installedBanal(parseBanalSettings({}, dirs)), cache);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
test("
|
|
25
|
+
test("paperlint's own copy when nothing else is named", () => {
|
|
26
26
|
assert.equal(kind(pick({}, on(cache))), "cache");
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
test("a project's vendor/banal before
|
|
29
|
+
test("a project's vendor/banal before paperlint's copy", () => {
|
|
30
30
|
// Guards: a project that vendors banal keeps using its own copy.
|
|
31
31
|
assert.equal(kind(pick({}, on(cache, "/p/vendor/banal"))), "vendor");
|
|
32
32
|
});
|
|
@@ -44,7 +44,7 @@ test("🔴 $BANAL naming a missing file finds NOTHING — no fallback to another
|
|
|
44
44
|
);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
test("nothing anywhere: not installed, naming where
|
|
47
|
+
test("nothing anywhere: not installed, naming where paperlint's copy would be", () => {
|
|
48
48
|
assert.deepEqual(
|
|
49
49
|
pick({}, on()),
|
|
50
50
|
err({ kind: "not-installed", installed: cache }),
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* ── THE LICENCE BOUNDARY ────────────────────────────────────────────────────────
|
|
10
10
|
* banal is GPL-2.0-or-later; paperlint is MIT. paperlint therefore never contains banal: `paperlint toolchain`
|
|
11
|
-
* DOWNLOADS it from HotCRP at a pinned commit, checks its sha256, and stores it in
|
|
11
|
+
* DOWNLOADS it from HotCRP at a pinned commit, checks its sha256, and stores it in paperlint's cache, and
|
|
12
12
|
* paperlint EXECUTES it as a separate program (`perl banal …`), reading its JSON output. Nothing of banal
|
|
13
13
|
* is copied, vendored, linked or translated into this package.
|
|
14
14
|
*
|
|
@@ -96,7 +96,7 @@ function runBanal(
|
|
|
96
96
|
banal: LocatedBanal,
|
|
97
97
|
pages: readonly PageLayout[],
|
|
98
98
|
): Result<BanalMeasurement, BanalFailure> {
|
|
99
|
-
return d.workspace.within("
|
|
99
|
+
return d.workspace.within("paperlint-banal-", (scratch) =>
|
|
100
100
|
parseBanalOutput(
|
|
101
101
|
d.run.run(
|
|
102
102
|
banalCommand(
|
|
@@ -4,22 +4,28 @@ import { parseBanalSettings } from "./settings.ts";
|
|
|
4
4
|
|
|
5
5
|
const dirs = { home: "/h", tmp: "/t", cwd: "/w" };
|
|
6
6
|
|
|
7
|
-
test("cacheDir: $
|
|
7
|
+
test("cacheDir: $PAPERLINT_BANAL_DIR, else $XDG_CACHE_HOME/paperlint/banal, else ~/.cache/paperlint/banal", () => {
|
|
8
8
|
assert.equal(
|
|
9
|
-
parseBanalSettings({
|
|
9
|
+
parseBanalSettings({ PAPERLINT_BANAL_DIR: "/x" }, dirs).cacheDir,
|
|
10
10
|
"/x",
|
|
11
11
|
);
|
|
12
12
|
assert.equal(
|
|
13
13
|
parseBanalSettings({ XDG_CACHE_HOME: "/c" }, dirs).cacheDir,
|
|
14
|
-
"/c/
|
|
14
|
+
"/c/paperlint/banal",
|
|
15
|
+
);
|
|
16
|
+
assert.equal(
|
|
17
|
+
parseBanalSettings({}, dirs).cacheDir,
|
|
18
|
+
"/h/.cache/paperlint/banal",
|
|
15
19
|
);
|
|
16
|
-
assert.equal(parseBanalSettings({}, dirs).cacheDir, "/h/.cache/rpp/banal");
|
|
17
20
|
});
|
|
18
21
|
|
|
19
22
|
test("an empty variable is unset, and a relative one is relative to the cwd the root read", () => {
|
|
20
|
-
const s = parseBanalSettings(
|
|
23
|
+
const s = parseBanalSettings(
|
|
24
|
+
{ BANAL: "b/banal", PAPERLINT_BANAL_DIR: "" },
|
|
25
|
+
dirs,
|
|
26
|
+
);
|
|
21
27
|
assert.equal(s.explicit, "/w/b/banal");
|
|
22
|
-
assert.equal(s.cacheDir, "/h/.cache/
|
|
28
|
+
assert.equal(s.cacheDir, "/h/.cache/paperlint/banal");
|
|
23
29
|
assert.equal(parseBanalSettings({}, dirs).explicit, null);
|
|
24
30
|
});
|
|
25
31
|
|
|
@@ -11,15 +11,15 @@ import {
|
|
|
11
11
|
} from "../../domain/host.ts";
|
|
12
12
|
import type { AbsolutePath } from "../../domain/paths.ts";
|
|
13
13
|
|
|
14
|
-
/** An explicit banal to use instead of
|
|
14
|
+
/** An explicit banal to use instead of paperlint's (a path to the script). */
|
|
15
15
|
export const BANAL_ENV = "BANAL";
|
|
16
|
-
/** Where `paperlint toolchain` stores banal. Default: `$XDG_CACHE_HOME/
|
|
17
|
-
export const BANAL_DIR_ENV = "
|
|
16
|
+
/** Where `paperlint toolchain` stores banal. Default: `$XDG_CACHE_HOME/paperlint/banal`, else `~/.cache/paperlint/banal`. */
|
|
17
|
+
export const BANAL_DIR_ENV = "PAPERLINT_BANAL_DIR";
|
|
18
18
|
|
|
19
19
|
export interface BanalSettings {
|
|
20
20
|
/** `$BANAL`: the user's explicit choice; null when unset. */
|
|
21
21
|
readonly explicit: AbsolutePath | null;
|
|
22
|
-
/** `$
|
|
22
|
+
/** `$PAPERLINT_BANAL_DIR` › `$XDG_CACHE_HOME/paperlint/banal` › `<home>/.cache/paperlint/banal`. */
|
|
23
23
|
readonly cacheDir: AbsolutePath;
|
|
24
24
|
/** Where scratch directories go. */
|
|
25
25
|
readonly tmpDir: AbsolutePath;
|
|
@@ -37,7 +37,11 @@ const set = (v: string | undefined): string | null => (v ? v : null);
|
|
|
37
37
|
function cacheDirOf(env: Environment, d: HostDirs): string {
|
|
38
38
|
return (
|
|
39
39
|
set(env[BANAL_DIR_ENV]) ??
|
|
40
|
-
join(
|
|
40
|
+
join(
|
|
41
|
+
set(env["XDG_CACHE_HOME"]) ?? join(d.home, ".cache"),
|
|
42
|
+
"paperlint",
|
|
43
|
+
"banal",
|
|
44
|
+
)
|
|
41
45
|
);
|
|
42
46
|
}
|
|
43
47
|
|
|
@@ -47,7 +47,7 @@ const page = (boxes: TextBox[], widthPt = 612, heightPt = 792): PageLayout => ({
|
|
|
47
47
|
const expected = [
|
|
48
48
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
49
49
|
'<!DOCTYPE pdf2xml SYSTEM "pdf2xml.dtd">',
|
|
50
|
-
'<pdf2xml producer="
|
|
50
|
+
'<pdf2xml producer="paperlint (pdf.js)" version="24.02.0">',
|
|
51
51
|
'<page number="1" position="absolute" top="0" left="0" height="2376" width="1836">',
|
|
52
52
|
'\t<fontspec id="0" size="30" family="LinLibertineT" color="#000000"/>',
|
|
53
53
|
'<text top="30.000000" left="60.000000" width="90.000000" height="36.000000" font="0">Hello</text>',
|
|
@@ -104,7 +104,7 @@ export function pdf2xml(pages: readonly PageLayout[]): string {
|
|
|
104
104
|
const lines = [
|
|
105
105
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
106
106
|
'<!DOCTYPE pdf2xml SYSTEM "pdf2xml.dtd">',
|
|
107
|
-
`<pdf2xml producer="
|
|
107
|
+
`<pdf2xml producer="paperlint (pdf.js)" version="${XML_DIALECT.version}">`,
|
|
108
108
|
...pages.flatMap((p, i) => pageXml(p, i + 1, fonts)),
|
|
109
109
|
"</pdf2xml>",
|
|
110
110
|
];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `Download` through `curl -fsSL`. Not Node's `fetch`: undici's `fetch` ignores `HTTPS_PROXY` unless
|
|
3
|
-
* a proxy agent is installed, and
|
|
3
|
+
* a proxy agent is installed, and paperlint's CI and the containers it runs in reach the network only
|
|
4
4
|
* through a proxy. curl reads the proxy variables itself. (A claim from documentation, not measured
|
|
5
5
|
* here — the port is what makes trying the other adapter cheap.)
|
|
6
6
|
*/
|
|
@@ -41,7 +41,9 @@ export function curlDownload(o: {
|
|
|
41
41
|
}): Download {
|
|
42
42
|
return {
|
|
43
43
|
fetch(url: string, timeoutMs: number) {
|
|
44
|
-
const dir = realpathSync(
|
|
44
|
+
const dir = realpathSync(
|
|
45
|
+
mkdtempSync(join(o.tmpDir, "paperlint-download-")),
|
|
46
|
+
);
|
|
45
47
|
const out = join(dir, "body");
|
|
46
48
|
try {
|
|
47
49
|
const seconds = String(Math.ceil(timeoutMs / 1000));
|
|
@@ -19,7 +19,9 @@ import type { AbsolutePath } from "../../domain/paths.ts";
|
|
|
19
19
|
import { curlDownload } from "./download.io.ts";
|
|
20
20
|
import { spawnProcess } from "../node/index.ts";
|
|
21
21
|
|
|
22
|
-
const root = realpathSync(
|
|
22
|
+
const root = realpathSync(
|
|
23
|
+
mkdtempSync(join(tmpdir(), "paperlint-download-test-")),
|
|
24
|
+
);
|
|
23
25
|
after(() => rmSync(root, { recursive: true, force: true }));
|
|
24
26
|
const at = (...p: string[]) => join(root, ...p) as AbsolutePath;
|
|
25
27
|
|
|
@@ -7,7 +7,7 @@ import { afterAll as after, test } from "vitest";
|
|
|
7
7
|
import type { AbsolutePath } from "../../domain/paths.ts";
|
|
8
8
|
import { nodeFiles } from "./files.io.ts";
|
|
9
9
|
|
|
10
|
-
const root = realpathSync(mkdtempSync(join(tmpdir(), "
|
|
10
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-files-test-")));
|
|
11
11
|
after(() => rmSync(root, { recursive: true, force: true }));
|
|
12
12
|
const at = (...p: string[]) => join(root, ...p) as AbsolutePath;
|
|
13
13
|
|
|
@@ -19,8 +19,8 @@ const sh = (script: string, timeoutMs = 10_000): Command => ({
|
|
|
19
19
|
test("a program that is not there is `not-found`, and names the program", () => {
|
|
20
20
|
// Guards: ENOENT is the executable itself — "perl is missing" rather than "banal failed".
|
|
21
21
|
assert.deepEqual(
|
|
22
|
-
run.run({ ...sh(""), file: "
|
|
23
|
-
{ kind: "not-found", file: "
|
|
22
|
+
run.run({ ...sh(""), file: "paperlint-no-such-program-xyz", args: [] }),
|
|
23
|
+
{ kind: "not-found", file: "paperlint-no-such-program-xyz" },
|
|
24
24
|
);
|
|
25
25
|
});
|
|
26
26
|
|
|
@@ -19,12 +19,12 @@ import { join } from "node:path";
|
|
|
19
19
|
import { afterAll as after, test } from "vitest";
|
|
20
20
|
import { tmpWorkspace } from "./workspace.io.ts";
|
|
21
21
|
|
|
22
|
-
const root = realpathSync(mkdtempSync(join(tmpdir(), "
|
|
22
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-ws-test-")));
|
|
23
23
|
after(() => rmSync(root, { recursive: true, force: true }));
|
|
24
24
|
|
|
25
25
|
test("the directory exists inside the scope and is gone after it", () => {
|
|
26
26
|
const ws = tmpWorkspace(root);
|
|
27
|
-
const dir = ws.within("
|
|
27
|
+
const dir = ws.within("paperlint-banal-", (s) => {
|
|
28
28
|
assert.ok(existsSync(s.dir));
|
|
29
29
|
return s.dir;
|
|
30
30
|
});
|
|
@@ -35,7 +35,7 @@ test("the directory exists inside the scope and is gone after it", () => {
|
|
|
35
35
|
test("the directory is gone after a throw too", () => {
|
|
36
36
|
let seen = "";
|
|
37
37
|
assert.throws(() =>
|
|
38
|
-
tmpWorkspace(root).within("
|
|
38
|
+
tmpWorkspace(root).within("paperlint-banal-", (s) => {
|
|
39
39
|
seen = s.dir;
|
|
40
40
|
throw new Error("boom");
|
|
41
41
|
}),
|
|
@@ -46,7 +46,7 @@ test("the directory is gone after a throw too", () => {
|
|
|
46
46
|
test("🔴 write: the file lands in the scope's directory, and `exec` makes it runnable — from a path with a space and a quote", () => {
|
|
47
47
|
const awkward = join(root, "it's a dir");
|
|
48
48
|
mkdirSync(awkward);
|
|
49
|
-
tmpWorkspace(awkward).within("
|
|
49
|
+
tmpWorkspace(awkward).within("paperlint-ws-", (s) => {
|
|
50
50
|
const plain = s.write("data.txt", "hello", "read");
|
|
51
51
|
assert.equal(readFileSync(plain, "utf8"), "hello");
|
|
52
52
|
const script = s.write("run", "#!/bin/sh\necho ran\n", "exec");
|
|
@@ -89,7 +89,7 @@ for (const [a, yes] of [
|
|
|
89
89
|
check(`isYes(${JSON.stringify(a)}) is ${yes}`, E.isYes(a) === yes);
|
|
90
90
|
|
|
91
91
|
// ── 2. prepareEngine ────────────────────────────────────────────────────────────────────
|
|
92
|
-
const work = realpathSync(mkdtempSync(join(tmpdir(), "
|
|
92
|
+
const work = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-engine-h-")));
|
|
93
93
|
const TEX = {
|
|
94
94
|
packages: { acmart: ["acmart.cls"], libertine: ["libertine.sty"] },
|
|
95
95
|
tools: {},
|
|
@@ -144,7 +144,7 @@ const go = async (o) => {
|
|
|
144
144
|
};
|
|
145
145
|
};
|
|
146
146
|
const envWith = (cacheRoot, path) => ({
|
|
147
|
-
|
|
147
|
+
PAPERLINT_TEXLIVE_DIR: cacheRoot,
|
|
148
148
|
PATH: path,
|
|
149
149
|
});
|
|
150
150
|
|
package/src/build-engine.ts
CHANGED
|
@@ -86,7 +86,7 @@ function withDefaults(o: EngineOptions): Resolved {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
|
-
* The facts `resolveEngine` decides on:
|
|
89
|
+
* The facts `resolveEngine` decides on: paperlint's cache and the PATH's TeX Live, each probed. The cache
|
|
90
90
|
* may hold one tree per TeX Live year; the one reported is the newest COMPLETE one, else the newest
|
|
91
91
|
* (so a new year whose install was interrupted never wins over a complete older year).
|
|
92
92
|
*/
|