paperlint 3.1.0 → 3.1.2
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 +21 -105
- package/CONTRIBUTING.md +23 -25
- package/README.md +11 -7
- 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/clock.io.d.ts +5 -0
- package/dist/adapters/references/clock.io.d.ts.map +1 -0
- package/dist/adapters/references/clock.io.js +7 -0
- package/dist/adapters/references/clock.io.js.map +1 -0
- package/dist/adapters/references/index.d.ts +24 -2
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +131 -12
- 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 +14 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +17 -11
- 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/lookup-cache.d.ts +75 -0
- package/dist/domain/lookup-cache.d.ts.map +1 -0
- package/dist/domain/lookup-cache.js +137 -0
- package/dist/domain/lookup-cache.js.map +1 -0
- 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/ports/check-references.d.ts +12 -2
- package/dist/ports/check-references.d.ts.map +1 -1
- package/dist/ports/check-references.js +0 -5
- package/dist/ports/check-references.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.d.ts +11 -3
- package/dist/references.d.ts.map +1 -1
- package/dist/references.js +63 -15
- 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 +11 -10
- package/docs/prior-art/citation-checkers.md +174 -0
- package/docs/prior-art/test-tooling.md +7 -30
- package/docs/references.md +126 -0
- package/docs/testing.md +118 -0
- package/docs/toolchain.md +6 -4
- 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/references/artifact-checklist.md +7 -7
- 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/checkers-requirements.txt +30 -19
- package/skills/render-paper/ensure-checkers.sh +20 -4
- 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 +30 -14
- package/skills/verify-citations/SKILL.md.spec.ts +29 -13
- package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +253 -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 +6 -6
- package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
- package/skills/verify-citations/scripts/verify-cites.mjs +360 -215
- package/skills/verify-citations/scripts/verify-cites.net.test.mjs +588 -0
- package/skills/verify-citations/scripts/verify-cites.test.mjs +151 -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/clock.io.ts +8 -0
- package/src/adapters/references/index.test.ts +560 -0
- package/src/adapters/references/index.ts +203 -19
- 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 +23 -12
- 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/lookup-cache.test.ts +178 -0
- package/src/domain/lookup-cache.ts +202 -0
- 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/ports/check-references.ts +16 -2
- package/src/presets.test.ts +2 -0
- package/src/presets.ts +7 -1
- package/src/references.test.ts +257 -10
- package/src/references.ts +93 -17
- 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,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"] {
|
|
@@ -554,6 +562,10 @@ export const measureStep: BuildStep = {
|
|
|
554
562
|
* authors are the published version's (bib-authors) — and record the verdicts in
|
|
555
563
|
* `_build/references.json` for the offline lint rules (`reference-rules.ts`). OPTIONAL and NEVER
|
|
556
564
|
* FAILING: without network the PDF is still built, and the record says `not-checked`.
|
|
565
|
+
*
|
|
566
|
+
* What the services answered is read from and added to `<paper>/repro/references-cache.json`
|
|
567
|
+
* (`references.ts`, `domain/lookup-cache.ts`), committed with the paper, so only a new or edited
|
|
568
|
+
* entry is asked, and an unchanged bibliography is checked with no network at all (#107).
|
|
557
569
|
*/
|
|
558
570
|
export const referencesStep: BuildStep = {
|
|
559
571
|
name: "references",
|
|
@@ -657,9 +669,12 @@ function baseDefaults({
|
|
|
657
669
|
}
|
|
658
670
|
|
|
659
671
|
/** Without a checker the record says so — never a pass. The CLI wires the real one. */
|
|
660
|
-
const notWired: CheckReferences = async () => ({
|
|
661
|
-
|
|
662
|
-
|
|
672
|
+
const notWired: CheckReferences = async (_bib, cache) => ({
|
|
673
|
+
check: {
|
|
674
|
+
kind: "not-checked",
|
|
675
|
+
why: "no reference checker was wired into this build",
|
|
676
|
+
},
|
|
677
|
+
cache,
|
|
663
678
|
});
|
|
664
679
|
|
|
665
680
|
/** banal as the measurer, wired from the build's environment: the one piece of root work left here (#76). */
|
|
@@ -850,10 +865,6 @@ function formatRefusal(r: BuildResult): string {
|
|
|
850
865
|
return r.staleRemoved ? `${refused}\n ${PDF_REMOVED}` : refused;
|
|
851
866
|
}
|
|
852
867
|
|
|
853
|
-
export function formatResults(results: readonly BuildResult[]): string {
|
|
854
|
-
return results.map(formatResult).join("\n");
|
|
855
|
-
}
|
|
856
|
-
|
|
857
868
|
/**
|
|
858
869
|
* 🔴 "Nothing to compile" COUNTS AS A REFUSAL on a par with a failed build. Failing to tell these
|
|
859
870
|
* 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
|
);
|