paperlint 3.1.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/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.d.ts +10 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +7 -8
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +27 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +71 -55
- 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/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 +3 -3
- 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/paper-files.d.ts.map +1 -1
- package/dist/paper-files.js +6 -4
- 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/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 +3 -2
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +4 -5
- package/dist/toolchain.js.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +11 -9
- package/dist/venue-rules.js.map +1 -1
- package/docs/prior-art/README.md +1 -1
- package/docs/prior-art/test-tooling.md +7 -30
- 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 +9 -2
- package/eslint-rules/paper-typography.test.mjs +146 -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.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/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.harness.mjs +5 -9
- package/src/build.test.ts +362 -0
- package/src/build.ts +13 -9
- package/src/cli.harness.mjs +10 -17
- package/src/cli.test.ts +584 -0
- package/src/cli.ts +112 -66
- 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 +27 -7
- 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 +3 -3
- 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/new-paper.harness.mjs +3 -7
- package/src/new-paper.test.ts +31 -1
- package/src/paper-files.test.ts +12 -0
- package/src/paper-files.ts +9 -5
- package/src/paper-settings.test.ts +20 -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/structure.harness.mjs +3 -7
- package/src/tex-requirements.harness.mjs +34 -7
- package/src/tex-requirements.ts +12 -6
- package/src/toolchain.harness.mjs +3 -7
- package/src/toolchain.test.ts +280 -0
- package/src/toolchain.ts +8 -7
- package/src/venue-rules.test.ts +60 -0
- package/src/venue-rules.ts +13 -11
- 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 -65
- package/src/structure.mutations.mjs +0 -83
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* build-engine.ts arms the harness does not reach: a system pdflatex that lacks packages (said
|
|
3
|
+
* before asking), an install that fails, an install that leaves packages missing, a cache used from
|
|
4
|
+
* an environment with no PATH, and the refusal line for a decision that is neither refuse nor ask.
|
|
5
|
+
*/
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { test } from "vitest";
|
|
10
|
+
import { useTempDir } from "../test/support.mjs";
|
|
11
|
+
import { prepareEngine, refusal } from "./build-engine.ts";
|
|
12
|
+
|
|
13
|
+
const work = useTempDir("paperlint-build-engine-test-");
|
|
14
|
+
const TEX = { packages: { acmart: ["acmart.cls"] }, tools: {} };
|
|
15
|
+
/** A runnable pdflatex in `dir`. */
|
|
16
|
+
const texAt = (dir: string): string => {
|
|
17
|
+
mkdirSync(dir, { recursive: true });
|
|
18
|
+
writeFileSync(join(dir, "pdflatex"), "");
|
|
19
|
+
chmodSync(join(dir, "pdflatex"), 0o755);
|
|
20
|
+
return dir;
|
|
21
|
+
};
|
|
22
|
+
/** kpsewhich answers per bin directory. */
|
|
23
|
+
const fakeRun = (found: Record<string, string>) =>
|
|
24
|
+
((cmd: string) => ({
|
|
25
|
+
stdout: found[dirname(cmd)] ?? "",
|
|
26
|
+
status: 0,
|
|
27
|
+
})) as never;
|
|
28
|
+
|
|
29
|
+
async function go(o: Parameters<typeof prepareEngine>[0]) {
|
|
30
|
+
const out: string[] = [];
|
|
31
|
+
const err: string[] = [];
|
|
32
|
+
const r = await prepareEngine({
|
|
33
|
+
log: (l) => out.push(l),
|
|
34
|
+
err: (l) => err.push(l),
|
|
35
|
+
platform: "linux",
|
|
36
|
+
interactive: true,
|
|
37
|
+
ask: async () => "y",
|
|
38
|
+
...o,
|
|
39
|
+
});
|
|
40
|
+
return { r, out, err };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test("a system pdflatex lacking packages is named before asking; a failed install stops the run", async () => {
|
|
44
|
+
const sys = texAt(join(work, "sys", "bin"));
|
|
45
|
+
const x = await go({
|
|
46
|
+
tex: TEX,
|
|
47
|
+
env: { PAPERLINT_TEXLIVE_DIR: join(work, "empty-cache"), PATH: sys },
|
|
48
|
+
run: fakeRun({}),
|
|
49
|
+
install: () => ({ ok: false }),
|
|
50
|
+
});
|
|
51
|
+
assert.deepEqual(
|
|
52
|
+
[x.r, x.out],
|
|
53
|
+
[
|
|
54
|
+
{ ok: false, code: 1 },
|
|
55
|
+
[`${join(sys, "pdflatex")} is not used: it lacks acmart`],
|
|
56
|
+
],
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("an install that verifies but still lacks a package is refused, naming it", async () => {
|
|
61
|
+
const cache = join(work, "cache-lacking");
|
|
62
|
+
const x = await go({
|
|
63
|
+
tex: TEX,
|
|
64
|
+
env: { PAPERLINT_TEXLIVE_DIR: cache, PATH: join(work, "nothing") },
|
|
65
|
+
run: fakeRun({}),
|
|
66
|
+
install: () => {
|
|
67
|
+
texAt(join(cache, "2026", "bin", "x86_64-linux"));
|
|
68
|
+
return { ok: true, tree: { dir: "", year: "2026", bin: "" } };
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
assert.deepEqual(x.r, { ok: false, code: 1 });
|
|
72
|
+
assert.match(x.err.join("\n"), /missing: 1 package\(s\): acmart/);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("an install that leaves no tree at all is refused with nothing named missing", async () => {
|
|
76
|
+
const x = await go({
|
|
77
|
+
tex: TEX,
|
|
78
|
+
env: {
|
|
79
|
+
PAPERLINT_TEXLIVE_DIR: join(work, "cache-none"),
|
|
80
|
+
PATH: join(work, "nothing"),
|
|
81
|
+
},
|
|
82
|
+
run: fakeRun({}),
|
|
83
|
+
install: () => ({ ok: true, tree: { dir: "", year: "2026", bin: "" } }),
|
|
84
|
+
});
|
|
85
|
+
assert.deepEqual(x.r, { ok: false, code: 1 });
|
|
86
|
+
assert.match(x.err.join("\n"), /missing: 0 package\(s\): \)/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a complete cache is used from an environment with no PATH at all", async () => {
|
|
90
|
+
const bin = texAt(join(work, "cache-ok", "2026", "bin", "x86_64-linux"));
|
|
91
|
+
const x = await go({
|
|
92
|
+
tex: TEX,
|
|
93
|
+
env: { PAPERLINT_TEXLIVE_DIR: join(work, "cache-ok") },
|
|
94
|
+
run: fakeRun({ [bin]: "/t/acmart.cls\n" }),
|
|
95
|
+
});
|
|
96
|
+
assert.deepEqual(x.r, {
|
|
97
|
+
ok: true,
|
|
98
|
+
env: { PAPERLINT_TEXLIVE_DIR: join(work, "cache-ok"), PATH: `${bin}:` },
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("the refusal line for a decision to use a tree names nothing missing", () => {
|
|
103
|
+
assert.equal(
|
|
104
|
+
refusal({ kind: "use-cache" } as never, { system: null } as never),
|
|
105
|
+
"✗ paperlint build: no TeX Live with every package these papers need — run `npx paperlint toolchain` (missing: 0 package(s): )",
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("without an ask or an install given: Enter is yes, and the default install runs (here: every mirror fails)", async () => {
|
|
110
|
+
const cache = join(work, "cache-default");
|
|
111
|
+
const calls: string[] = [];
|
|
112
|
+
const out: string[] = [];
|
|
113
|
+
const err: string[] = [];
|
|
114
|
+
const r = await prepareEngine({
|
|
115
|
+
tex: TEX,
|
|
116
|
+
log: (l) => out.push(l),
|
|
117
|
+
err: (l) => err.push(l),
|
|
118
|
+
platform: "linux",
|
|
119
|
+
interactive: true,
|
|
120
|
+
env: {
|
|
121
|
+
PAPERLINT_TEXLIVE_DIR: cache,
|
|
122
|
+
PAPERLINT_CTAN_MIRROR: "https://mirror.invalid/tlnet",
|
|
123
|
+
PATH: join(work, "nothing"),
|
|
124
|
+
},
|
|
125
|
+
run: ((cmd: string) => {
|
|
126
|
+
calls.push(cmd.split("/").pop() ?? cmd);
|
|
127
|
+
return {
|
|
128
|
+
status: 22,
|
|
129
|
+
stdout: "",
|
|
130
|
+
stderr: "curl: (6) could not resolve\n",
|
|
131
|
+
};
|
|
132
|
+
}) as never,
|
|
133
|
+
});
|
|
134
|
+
assert.deepEqual(r, { ok: false, code: 1 });
|
|
135
|
+
assert.ok(calls.includes("curl"), calls.join(","));
|
|
136
|
+
assert.match(err.join("\n"), /no CTAN mirror returned install-tl/);
|
|
137
|
+
});
|
package/src/build.harness.mjs
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
* `build.sh` is never executed (checked by the trace it would leave on disk and by the list of
|
|
11
11
|
* processes started), and a failed build removes the stale `paper.pdf` (checked on disk).
|
|
12
12
|
*/
|
|
13
|
-
import assert from "node:assert/strict";
|
|
14
13
|
import {
|
|
15
14
|
mkdtempSync,
|
|
16
15
|
mkdirSync,
|
|
@@ -23,6 +22,7 @@ import {
|
|
|
23
22
|
import { tmpdir } from "node:os";
|
|
24
23
|
import { join, dirname, delimiter } from "node:path";
|
|
25
24
|
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { createChecker } from "../lib/check.mjs";
|
|
26
26
|
|
|
27
27
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
28
28
|
const {
|
|
@@ -44,11 +44,7 @@ const { packageVenuesDir } = await import(
|
|
|
44
44
|
join(HERE, "..", "skills", "paper-pipeline", "scripts", "consumer.mjs")
|
|
45
45
|
);
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
const check = (label, cond) => {
|
|
49
|
-
assert.ok(cond, label);
|
|
50
|
-
n++;
|
|
51
|
-
};
|
|
47
|
+
const check = createChecker();
|
|
52
48
|
|
|
53
49
|
const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-build-")));
|
|
54
50
|
// 🔴 banal is looked for under the temp root, never in the developer's cache: with the real
|
|
@@ -511,8 +507,8 @@ try {
|
|
|
511
507
|
check("on full success there is no remedy", remedyFor([r]) === "");
|
|
512
508
|
|
|
513
509
|
// ── 🔴 A STALE PDF NEVER SURVIVES A RUN THAT DID NOT REPLACE IT — every path of buildPapers ──
|
|
514
|
-
// The order matters
|
|
515
|
-
// existence check by the empty
|
|
510
|
+
// The order matters here: the up-front removal is exercised by the no-engine case below, the
|
|
511
|
+
// existence check by the empty-document case, and neither depends on the other defence.
|
|
516
512
|
const REMOVED =
|
|
517
513
|
"paper.pdf removed — a stale PDF must not pass for this build";
|
|
518
514
|
|
|
@@ -680,5 +676,5 @@ try {
|
|
|
680
676
|
}
|
|
681
677
|
|
|
682
678
|
console.log(
|
|
683
|
-
`✓ ${String(
|
|
679
|
+
`✓ ${String(check.count)} assertions passed — build: paperlint compiles, build.sh never runs, a red build leaves no PDF`,
|
|
684
680
|
);
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* build.ts edges the harness does not reach: the compile step's wording for every way pdflatex or
|
|
3
|
+
* bibtex stops, bibtex's input when the `.aux` names a database, the measure note for a review
|
|
4
|
+
* build, a stub page and a measured geometry, the loop over non-applicable steps, and the verdict
|
|
5
|
+
* lines. pdflatex and bibtex are a fake runner that writes the files the real ones would.
|
|
6
|
+
*/
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { test } from "vitest";
|
|
11
|
+
import { useTempDir, writeTree } from "../test/support.mjs";
|
|
12
|
+
import {
|
|
13
|
+
bibInput,
|
|
14
|
+
buildPaper,
|
|
15
|
+
buildPapers,
|
|
16
|
+
compileStep,
|
|
17
|
+
formatResult,
|
|
18
|
+
parseDocumentclass,
|
|
19
|
+
readFacts,
|
|
20
|
+
type BuildOptions,
|
|
21
|
+
type PaperFacts,
|
|
22
|
+
} from "./build.ts";
|
|
23
|
+
import type { Geometry } from "./domain/geometry.ts";
|
|
24
|
+
import type { PdfReader } from "./pdf-facts.ts";
|
|
25
|
+
|
|
26
|
+
const root = useTempDir("paperlint-build-test-");
|
|
27
|
+
const TEX = "\\documentclass{article}\\begin{document}x\\end{document}";
|
|
28
|
+
let n = 0;
|
|
29
|
+
const paper = (files: Record<string, string> = { "paper.tex": TEX }) =>
|
|
30
|
+
writeTree(join(root, `p${String(n++)}`), files);
|
|
31
|
+
|
|
32
|
+
interface Fake {
|
|
33
|
+
/** pdflatex's exit status; `null` is a signal. */
|
|
34
|
+
latex?: number | null;
|
|
35
|
+
/** bibtex's exit status, and what it prints. */
|
|
36
|
+
bibtex?: number | null;
|
|
37
|
+
bibtexOut?: string;
|
|
38
|
+
/** Which program cannot be started. */
|
|
39
|
+
missing?: "pdflatex" | "bibtex";
|
|
40
|
+
aux?: string;
|
|
41
|
+
log?: string;
|
|
42
|
+
/** pdflatex dies before opening paper.log, printing nothing. */
|
|
43
|
+
noLog?: boolean;
|
|
44
|
+
/** Each pass writes a different .aux, so the loop never settles. */
|
|
45
|
+
churn?: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** A pdflatex pass: writes the .aux, the log unless it dies first, and a PDF when it exits 0. */
|
|
48
|
+
function latexPass(f: Fake, cwd: string, pass: number) {
|
|
49
|
+
const status = "latex" in f ? f.latex : 0;
|
|
50
|
+
const aux = f.churn ? `\\relax % ${String(pass)}\n` : (f.aux ?? "\\relax\n");
|
|
51
|
+
writeFileSync(join(cwd, "paper.aux"), aux);
|
|
52
|
+
if (!f.noLog) writeFileSync(join(cwd, "paper.log"), f.log ?? "");
|
|
53
|
+
if (status === 0) writeFileSync(join(cwd, "paper.pdf"), "%PDF-fake");
|
|
54
|
+
return f.noLog ? { status } : { status, stdout: "", stderr: "" };
|
|
55
|
+
}
|
|
56
|
+
/** A bibtex run: writes the .bbl; status and output as the case says. */
|
|
57
|
+
function bibtexRun(f: Fake, cwd: string) {
|
|
58
|
+
writeFileSync(join(cwd, "paper.bbl"), "\\begin{thebibliography}{1}");
|
|
59
|
+
if (f.bibtex === undefined)
|
|
60
|
+
return { status: 0, stdout: "This is BibTeX\n", stderr: "" };
|
|
61
|
+
return f.bibtexOut === undefined
|
|
62
|
+
? { status: f.bibtex }
|
|
63
|
+
: { status: f.bibtex, stdout: f.bibtexOut };
|
|
64
|
+
}
|
|
65
|
+
function fakeRun(f: Fake): BuildOptions["run"] {
|
|
66
|
+
let pass = 0;
|
|
67
|
+
return ((bin: string, _args: string[], opts: { cwd: string }) => {
|
|
68
|
+
if (bin === f.missing)
|
|
69
|
+
return {
|
|
70
|
+
error: Object.assign(new Error("spawn ENOENT"), { code: "ENOENT" }),
|
|
71
|
+
status: null,
|
|
72
|
+
};
|
|
73
|
+
return bin === "pdflatex"
|
|
74
|
+
? latexPass(f, opts.cwd, ++pass)
|
|
75
|
+
: bibtexRun(f, opts.cwd);
|
|
76
|
+
}) as never;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const NO_GEOMETRY: Geometry = {
|
|
80
|
+
kind: "unmeasured",
|
|
81
|
+
why: ["no banal"],
|
|
82
|
+
tried: null,
|
|
83
|
+
};
|
|
84
|
+
const page = (words: number, numbered = 0) => ({
|
|
85
|
+
widthPt: 612,
|
|
86
|
+
heightPt: 792,
|
|
87
|
+
words: [
|
|
88
|
+
...Array.from({ length: numbered }, (_, i) => ({
|
|
89
|
+
x0: 20,
|
|
90
|
+
y0: 60 + i * 10,
|
|
91
|
+
x1: 30,
|
|
92
|
+
y1: 70 + i * 10,
|
|
93
|
+
text: String(i + 1),
|
|
94
|
+
})),
|
|
95
|
+
...Array.from({ length: words }, (_, i) => ({
|
|
96
|
+
x0: i % 2 ? 320 : 54,
|
|
97
|
+
y0: 60 + (i >> 1) * 10,
|
|
98
|
+
x1: i % 2 ? 340 : 74,
|
|
99
|
+
y1: 70 + (i >> 1) * 10,
|
|
100
|
+
text: "w",
|
|
101
|
+
})),
|
|
102
|
+
],
|
|
103
|
+
});
|
|
104
|
+
const reader =
|
|
105
|
+
(last = page(80)): PdfReader =>
|
|
106
|
+
async () => ({
|
|
107
|
+
ok: true,
|
|
108
|
+
facts: {
|
|
109
|
+
pages: 1,
|
|
110
|
+
fonts: {
|
|
111
|
+
kind: "drawn",
|
|
112
|
+
list: [{ kind: "embedded", name: "CMR10", program: "Type1" }],
|
|
113
|
+
},
|
|
114
|
+
last,
|
|
115
|
+
layout: [],
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
const quiet = () => {};
|
|
119
|
+
/** buildPaper with fakes; the verdict line is what the command would print for it. */
|
|
120
|
+
async function build(dir: string, f: Fake = {}, over: BuildOptions = {}) {
|
|
121
|
+
const r = await buildPaper(dir, {
|
|
122
|
+
cwd: root,
|
|
123
|
+
log: quiet,
|
|
124
|
+
env: { PAPERLINT_BANAL_DIR: join(root, "no-banal") },
|
|
125
|
+
run: fakeRun(f),
|
|
126
|
+
readPdf: reader(),
|
|
127
|
+
measure: { measure: () => NO_GEOMETRY },
|
|
128
|
+
...over,
|
|
129
|
+
});
|
|
130
|
+
return { r, line: formatResult(r).replaceAll(root, "<root>") };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
test("documentclass: an empty name is no class; a macro inside the name contributes nothing", () => {
|
|
134
|
+
assert.equal(parseDocumentclass("\\documentclass{}"), null);
|
|
135
|
+
// Nested deep enough to exhaust the parser's stack: no class, not a crash.
|
|
136
|
+
assert.equal(
|
|
137
|
+
parseDocumentclass(`\\documentclass{x}${"{".repeat(20_000)}`),
|
|
138
|
+
null,
|
|
139
|
+
);
|
|
140
|
+
assert.deepEqual(parseDocumentclass("\\documentclass{a\\foo{}b}"), {
|
|
141
|
+
name: "ab",
|
|
142
|
+
options: [],
|
|
143
|
+
});
|
|
144
|
+
// unified-latex attaches arguments to the macros it has a signature for; a tree where
|
|
145
|
+
// `\documentclass` came back bare has no name to read, and is no class.
|
|
146
|
+
assert.equal(
|
|
147
|
+
parseDocumentclass("\\documentclass", {
|
|
148
|
+
parse: () =>
|
|
149
|
+
({
|
|
150
|
+
type: "root",
|
|
151
|
+
content: [{ type: "macro", content: "documentclass" }],
|
|
152
|
+
}) as never,
|
|
153
|
+
}),
|
|
154
|
+
null,
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("facts of a paper with no paper.tex: no class", () => {
|
|
159
|
+
assert.deepEqual(readFacts(paper({ "paper.md": "# P\n" })), {
|
|
160
|
+
main: null,
|
|
161
|
+
documentclass: null,
|
|
162
|
+
venue: null,
|
|
163
|
+
ignoredScripts: [],
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("compile's plan line names the options, a missing class and the venue", () => {
|
|
168
|
+
const facts = (over: Partial<PaperFacts>): PaperFacts => ({
|
|
169
|
+
main: "paper.tex",
|
|
170
|
+
documentclass: null,
|
|
171
|
+
venue: null,
|
|
172
|
+
ignoredScripts: [],
|
|
173
|
+
...over,
|
|
174
|
+
});
|
|
175
|
+
assert.deepEqual(
|
|
176
|
+
[
|
|
177
|
+
compileStep.applies(
|
|
178
|
+
facts({
|
|
179
|
+
documentclass: { name: "acmart", options: ["sigconf", "review"] },
|
|
180
|
+
}),
|
|
181
|
+
),
|
|
182
|
+
compileStep.applies(facts({ venue: "aisec" })),
|
|
183
|
+
],
|
|
184
|
+
[
|
|
185
|
+
{ yes: true, why: "paper.tex (\\documentclass[sigconf,review]{acmart})" },
|
|
186
|
+
{ yes: true, why: "paper.tex (no \\documentclass found, venue aisec)" },
|
|
187
|
+
],
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("bibtex's input: no .aux, nothing named, a database named with and without .bib, one that is missing", () => {
|
|
192
|
+
assert.deepEqual(bibInput(paper({})), { kind: "none" });
|
|
193
|
+
const none = paper({ "paper.aux": "\\relax\n" });
|
|
194
|
+
assert.deepEqual(bibInput(none), { kind: "none" });
|
|
195
|
+
const named = paper({
|
|
196
|
+
"paper.aux": "\\citation{k}\n\\bibstyle{plain}\n\\bibdata{a.bib,missing}\n",
|
|
197
|
+
"a.bib": "@misc{k}\n",
|
|
198
|
+
});
|
|
199
|
+
const got = bibInput(named);
|
|
200
|
+
assert.deepEqual(
|
|
201
|
+
{ ...got, bibHash: got.kind === "needed" && typeof got.bibHash },
|
|
202
|
+
{
|
|
203
|
+
kind: "needed",
|
|
204
|
+
citations: ["k"],
|
|
205
|
+
databases: ["a.bib", "missing"],
|
|
206
|
+
style: "plain",
|
|
207
|
+
bibHash: "string",
|
|
208
|
+
},
|
|
209
|
+
);
|
|
210
|
+
const gone = bibInput(paper({ "paper.aux": "\\bibdata{gone}\n" }));
|
|
211
|
+
assert.equal(gone.kind === "needed" && gone.bibHash, null);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("compile: pdflatex killed by a signal, with no log and nothing printed", async () => {
|
|
215
|
+
const { line } = await build(paper(), { latex: null, noLog: true });
|
|
216
|
+
assert.equal(
|
|
217
|
+
line,
|
|
218
|
+
[
|
|
219
|
+
" ✗ compile: pdflatex exited with 1",
|
|
220
|
+
" pdflatex wrote no paper.log — the last lines it printed:",
|
|
221
|
+
" paper.pdf removed — a stale PDF must not pass for this build",
|
|
222
|
+
].join("\n"),
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("compile: bibtex that cannot start, and bibtex that fails, quote the .blg", async () => {
|
|
227
|
+
const aux = "\\citation{k}\n\\bibstyle{plain}\n\\bibdata{refs}\n";
|
|
228
|
+
const files = { "paper.tex": TEX, "refs.bib": "@misc{k}\n" };
|
|
229
|
+
const cannot = await build(paper(files), { aux, missing: "bibtex" });
|
|
230
|
+
assert.match(cannot.line, /^ {2}✗ compile: bibtex exited with 127\n/);
|
|
231
|
+
const dir = paper(files);
|
|
232
|
+
const failed = await build(dir, {
|
|
233
|
+
aux,
|
|
234
|
+
bibtex: 2,
|
|
235
|
+
bibtexOut:
|
|
236
|
+
"I couldn't open database file refs.bib\n---line 3 of file paper.aux\n",
|
|
237
|
+
});
|
|
238
|
+
assert.match(failed.line, /^ {2}✗ compile: bibtex exited with 2\n/);
|
|
239
|
+
assert.match(failed.line, /full log: <root>\/p\d+\/paper\.blg/);
|
|
240
|
+
const signal = await build(paper(files), { aux, bibtex: null });
|
|
241
|
+
assert.match(
|
|
242
|
+
signal.line,
|
|
243
|
+
/bibtex exited with 1\n {6}\(no error line found in the log\)/,
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("compile: a document whose .aux never settles did not converge", async () => {
|
|
248
|
+
const { line } = await build(paper(), { churn: true });
|
|
249
|
+
assert.match(line, /^ {2}✗ compile: pdflatex did not converge\n/);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("compile: warnings the final log still reports are named in the note", async () => {
|
|
253
|
+
const { r } = await build(paper(), {
|
|
254
|
+
log: "LaTeX Warning: There were undefined references.\n",
|
|
255
|
+
});
|
|
256
|
+
assert.equal(r.status, "built");
|
|
257
|
+
assert.match(r.notes?.[0] ?? "", /— ⚠️ the final log still reports /);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("measure: a review build and a stub last page are named; a measured geometry adds nothing", async () => {
|
|
261
|
+
const review = await build(paper(), {}, { readPdf: reader(page(20, 60)) });
|
|
262
|
+
assert.match(
|
|
263
|
+
review.line,
|
|
264
|
+
/last page has numbered lines \(a review build\), not measured/,
|
|
265
|
+
);
|
|
266
|
+
const stub = await build(paper(), {}, { readPdf: reader(page(3)) });
|
|
267
|
+
assert.match(stub.line, /last page has 3 words, too few to measure/);
|
|
268
|
+
const measured = await build(
|
|
269
|
+
paper(),
|
|
270
|
+
{},
|
|
271
|
+
{
|
|
272
|
+
measure: {
|
|
273
|
+
measure: () => ({
|
|
274
|
+
kind: "measured",
|
|
275
|
+
by: { tool: "banal", path: "/b", how: "$BANAL" },
|
|
276
|
+
geometry: {
|
|
277
|
+
pageWidthIn: 8.5,
|
|
278
|
+
pageHeightIn: 11,
|
|
279
|
+
columns: 2,
|
|
280
|
+
bodyPt: 9,
|
|
281
|
+
refPt: 7,
|
|
282
|
+
bodyPages: 1,
|
|
283
|
+
refPages: 0,
|
|
284
|
+
appendixPages: 0,
|
|
285
|
+
pagesByType: { body: 1 },
|
|
286
|
+
},
|
|
287
|
+
}),
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
assert.doesNotMatch(measured.line, /page geometry not measured/);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("a step that does not apply is skipped; a run whose steps leave no note prints the bare PDF", async () => {
|
|
295
|
+
const { line } = await build(
|
|
296
|
+
paper(),
|
|
297
|
+
{},
|
|
298
|
+
{
|
|
299
|
+
steps: [
|
|
300
|
+
{
|
|
301
|
+
name: "never",
|
|
302
|
+
required: false,
|
|
303
|
+
applies: () => ({ yes: false, why: "no" }),
|
|
304
|
+
run: () => {
|
|
305
|
+
throw new Error("ran");
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: "quiet",
|
|
310
|
+
required: true,
|
|
311
|
+
applies: () => ({ yes: true, why: "yes" }),
|
|
312
|
+
run: () => ({ ok: true }),
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
},
|
|
316
|
+
);
|
|
317
|
+
assert.equal(line, " ✓ paper.pdf");
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("paths: a paper that IS the working directory is named by its full path", async () => {
|
|
321
|
+
const dir = paper();
|
|
322
|
+
const logged: string[] = [];
|
|
323
|
+
await buildPaper(dir, {
|
|
324
|
+
cwd: dir,
|
|
325
|
+
log: (l) => logged.push(l),
|
|
326
|
+
dryRun: true,
|
|
327
|
+
measure: { measure: () => NO_GEOMETRY },
|
|
328
|
+
});
|
|
329
|
+
assert.equal(logged[0], dir);
|
|
330
|
+
const lines: string[] = [];
|
|
331
|
+
writeFileSync(join(dir, "paper.pdf"), "%PDF-old");
|
|
332
|
+
const run = await buildPapers([dir], {
|
|
333
|
+
cwd: dir,
|
|
334
|
+
log: (l) => lines.push(l),
|
|
335
|
+
engine: async () => null,
|
|
336
|
+
});
|
|
337
|
+
assert.deepEqual(
|
|
338
|
+
[run, lines],
|
|
339
|
+
[
|
|
340
|
+
{ kind: "no-engine" },
|
|
341
|
+
[`${dir}: paper.pdf removed — a stale PDF must not pass for this build`],
|
|
342
|
+
],
|
|
343
|
+
);
|
|
344
|
+
assert.equal(existsSync(join(dir, "paper.pdf")), false);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test("facts that cannot be read fail the build at `facts`", async () => {
|
|
348
|
+
const { line } = await build(
|
|
349
|
+
paper({ "paper.tex": TEX, "paperlint.json": "{ nope" }),
|
|
350
|
+
);
|
|
351
|
+
assert.match(line, /^ {2}✗ facts: /);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("a failed result without its lines still says what failed", () => {
|
|
355
|
+
assert.equal(
|
|
356
|
+
formatResult({ dir: "d", status: "failed", plan: [] }),
|
|
357
|
+
[
|
|
358
|
+
" ✗ build: failed",
|
|
359
|
+
" paper.pdf removed — a stale PDF must not pass for this build",
|
|
360
|
+
].join("\n"),
|
|
361
|
+
);
|
|
362
|
+
});
|
package/src/build.ts
CHANGED
|
@@ -187,11 +187,16 @@ function argText(arg: LatexArgument | undefined): string {
|
|
|
187
187
|
.join("");
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
/** A unified-latex AST, or null when the text does not parse. */
|
|
191
|
-
function parseTex(
|
|
190
|
+
/** A unified-latex AST, or null when the text does not parse. `parse` is unified-latex's parser. */
|
|
191
|
+
function parseTex(
|
|
192
|
+
tex: string,
|
|
193
|
+
parse: (tex: string) => LatexRoot = (t) => getParser().parse(t),
|
|
194
|
+
): LatexRoot | null {
|
|
192
195
|
try {
|
|
193
|
-
return
|
|
196
|
+
return parse(tex);
|
|
194
197
|
} catch {
|
|
198
|
+
// The parser recurses per group: ~20 000 nested `{` exhaust the stack (RangeError). Such a
|
|
199
|
+
// file has no class to report, and must not fail the build at `facts` for it.
|
|
195
200
|
return null;
|
|
196
201
|
}
|
|
197
202
|
}
|
|
@@ -200,8 +205,11 @@ function parseTex(tex: string): LatexRoot | null {
|
|
|
200
205
|
* `\documentclass[opts]{name}`, read by the unified-latex parser the lint rules already use —
|
|
201
206
|
* so a `\documentclass` inside a comment is a comment, not a class.
|
|
202
207
|
*/
|
|
203
|
-
export function parseDocumentclass(
|
|
204
|
-
|
|
208
|
+
export function parseDocumentclass(
|
|
209
|
+
tex: string,
|
|
210
|
+
{ parse }: { parse?: (tex: string) => LatexRoot } = {},
|
|
211
|
+
): PaperFacts["documentclass"] {
|
|
212
|
+
return documentclassOf(parseTex(tex, parse));
|
|
205
213
|
}
|
|
206
214
|
|
|
207
215
|
function documentclassOf(ast: LatexRoot | null): PaperFacts["documentclass"] {
|
|
@@ -850,10 +858,6 @@ function formatRefusal(r: BuildResult): string {
|
|
|
850
858
|
return r.staleRemoved ? `${refused}\n ${PDF_REMOVED}` : refused;
|
|
851
859
|
}
|
|
852
860
|
|
|
853
|
-
export function formatResults(results: readonly BuildResult[]): string {
|
|
854
|
-
return results.map(formatResult).join("\n");
|
|
855
|
-
}
|
|
856
|
-
|
|
857
861
|
/**
|
|
858
862
|
* 🔴 "Nothing to compile" COUNTS AS A REFUSAL on a par with a failed build. Failing to tell these
|
|
859
863
|
* two apart is what once produced a green run over a paper no job had built.
|
package/src/cli.harness.mjs
CHANGED
|
@@ -11,8 +11,6 @@
|
|
|
11
11
|
* leaked out of the depths of eslint-helpers.
|
|
12
12
|
* Both are pinned down by the assertions below so there is no going back.
|
|
13
13
|
*/
|
|
14
|
-
import assert from "node:assert/strict";
|
|
15
|
-
import { recordCheck } from "vigiles";
|
|
16
14
|
import { load as yamlLoad } from "js-yaml";
|
|
17
15
|
import {
|
|
18
16
|
mkdtempSync,
|
|
@@ -31,6 +29,7 @@ import { tmpdir } from "node:os";
|
|
|
31
29
|
import { join, dirname } from "node:path";
|
|
32
30
|
import { fileURLToPath } from "node:url";
|
|
33
31
|
import { PAPERS_DIR_FIELD, findProjectRoot } from "../lib/paper-config.mjs";
|
|
32
|
+
import { createChecker } from "../lib/check.mjs";
|
|
34
33
|
|
|
35
34
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
36
35
|
const { run, parseArgs, buildConfig, nextSteps, toPaths, runHook } =
|
|
@@ -39,14 +38,9 @@ const { init, choosePapers, offerWorkflow, missingPrograms, WORKFLOW_PATH } =
|
|
|
39
38
|
await import(join(HERE, "init.ts"));
|
|
40
39
|
const { PROGRAMS } = await import(join(HERE, "doctor.ts"));
|
|
41
40
|
|
|
42
|
-
let n = 0;
|
|
43
41
|
// Counted by vigiles too: `init` now loads `vigiles/claude-code` for its merge, and a harness that
|
|
44
42
|
// loads vigiles without recording a check is reported as having verified nothing.
|
|
45
|
-
const check = (
|
|
46
|
-
assert.ok(cond, label);
|
|
47
|
-
recordCheck(label);
|
|
48
|
-
n++;
|
|
49
|
-
};
|
|
43
|
+
const check = createChecker();
|
|
50
44
|
|
|
51
45
|
/** Runs the utility with output captured — quieter and faster than spawning a process. */
|
|
52
46
|
async function cli(args, cwd) {
|
|
@@ -73,10 +67,9 @@ async function cli(args, cwd) {
|
|
|
73
67
|
};
|
|
74
68
|
} catch (e) {
|
|
75
69
|
// 🔴 A LEAKED EXCEPTION IS A PROPERTY THAT MUST BE ASSERTED, NOT CAUGHT BY LETTING THE
|
|
76
|
-
// HARNESS CRASH.
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
// real defect looked like a weak test.
|
|
70
|
+
// HARNESS CRASH. Removing the catch around ESLint brings the whole harness down with a
|
|
71
|
+
// stack trace instead of a clean, named assertion failure — i.e. the real defect would
|
|
72
|
+
// look like a broken test, not a caught one.
|
|
80
73
|
return {
|
|
81
74
|
code: 99,
|
|
82
75
|
out: `THREW: ${e?.message ?? e}`,
|
|
@@ -537,8 +530,8 @@ check(
|
|
|
537
530
|
// declaration had already been written — i.e. the install both succeeded and looked like
|
|
538
531
|
// a crash.
|
|
539
532
|
// 🔴 THE `.catch` HERE IS LOAD-BEARING, NOT CAUTION. A leaked exception is a PROPERTY that
|
|
540
|
-
// gets asserted;
|
|
541
|
-
//
|
|
533
|
+
// gets asserted; without it, the harness crashes instead of failing a named assertion,
|
|
534
|
+
// i.e. the real defect would look like a hole in the test rather than a caught one.
|
|
542
535
|
const aborted = await offerWorkflow(dir, "writing", {
|
|
543
536
|
interactive: true,
|
|
544
537
|
ask: async () => {
|
|
@@ -599,7 +592,7 @@ check(
|
|
|
599
592
|
});
|
|
600
593
|
// 🔴 JUDGE ONLY init's OWN REPORT, BEFORE doctor's banner. The first version of these
|
|
601
594
|
// three assertions looked at the WHOLE output — and doctor also prints `✗ pdflatex` and
|
|
602
|
-
// the same install command.
|
|
595
|
+
// the same install command. A version of init with the remedy stripped OUT still stayed
|
|
603
596
|
// green: the assertion found a line printed by a different command and reported coverage
|
|
604
597
|
// that did not exist.
|
|
605
598
|
const own = out.text().split("── paperlint doctor")[0];
|
|
@@ -1260,7 +1253,7 @@ check(
|
|
|
1260
1253
|
}
|
|
1261
1254
|
|
|
1262
1255
|
console.log(
|
|
1263
|
-
`✓ ${String(
|
|
1256
|
+
`✓ ${String(check.count)} assertions passed — paperlint lint, one command instead of hand-rolled config`,
|
|
1264
1257
|
);
|
|
1265
1258
|
|
|
1266
1259
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -1799,5 +1792,5 @@ console.log(
|
|
|
1799
1792
|
}
|
|
1800
1793
|
|
|
1801
1794
|
console.log(
|
|
1802
|
-
`✓ ${String(
|
|
1795
|
+
`✓ ${String(check.count)} assertions in total, including init's hooks and paperlint new`,
|
|
1803
1796
|
);
|