paperlint 2.1.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +6 -1
- package/README.md +243 -235
- package/action.yml +4 -6
- package/dist/adapters/references/index.d.ts +13 -0
- package/dist/adapters/references/index.d.ts.map +1 -0
- package/dist/adapters/references/index.js +55 -0
- package/dist/adapters/references/index.js.map +1 -0
- package/dist/adapters/references/reach.io.d.ts +3 -0
- package/dist/adapters/references/reach.io.d.ts.map +1 -0
- package/dist/adapters/references/reach.io.js +14 -0
- package/dist/adapters/references/reach.io.js.map +1 -0
- package/dist/build-engine.d.ts.map +1 -1
- package/dist/build-engine.js +6 -2
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +15 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +60 -7
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +94 -30
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +500 -288
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +40 -72
- package/dist/doctor.js.map +1 -1
- package/dist/engine.d.ts +9 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +27 -0
- package/dist/engine.js.map +1 -1
- package/dist/facts-file.d.ts +2 -2
- package/dist/facts-file.js +2 -2
- package/dist/hooks-settings.d.ts +0 -21
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +17 -135
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +39 -29
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +144 -173
- package/dist/init.js.map +1 -1
- package/dist/link-skills.d.ts +1 -1
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +11 -29
- package/dist/link-skills.js.map +1 -1
- package/dist/new-paper.d.ts +8 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +49 -25
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +85 -0
- package/dist/paper-files.d.ts.map +1 -0
- package/dist/paper-files.js +96 -0
- package/dist/paper-files.js.map +1 -0
- package/dist/paper-settings.d.ts +13 -41
- package/dist/paper-settings.d.ts.map +1 -1
- package/dist/paper-settings.js +71 -71
- package/dist/paper-settings.js.map +1 -1
- package/dist/ports/check-references.d.ts +31 -0
- package/dist/ports/check-references.d.ts.map +1 -0
- package/dist/ports/check-references.js +7 -0
- package/dist/ports/check-references.js.map +1 -0
- package/dist/presets.d.ts +1 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +4 -6
- package/dist/presets.js.map +1 -1
- package/dist/reference-rules.d.ts +47 -0
- package/dist/reference-rules.d.ts.map +1 -0
- package/dist/reference-rules.js +156 -0
- package/dist/reference-rules.js.map +1 -0
- package/dist/references.d.ts +36 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +91 -0
- package/dist/references.js.map +1 -0
- package/dist/rules-config.d.ts +5 -3
- package/dist/rules-config.d.ts.map +1 -1
- package/dist/rules-config.js +15 -6
- package/dist/rules-config.js.map +1 -1
- package/dist/structure.js +3 -3
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.js +2 -2
- package/dist/tex-requirements.js.map +1 -1
- package/dist/toolchain.d.ts +5 -1
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +22 -5
- package/dist/toolchain.js.map +1 -1
- package/dist/types.d.ts +26 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +12 -10
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +163 -104
- package/docs/e2e.md +1 -1
- package/docs/install.md +25 -40
- package/docs/optional-rules.md +5 -4
- package/docs/rules.md +65 -24
- package/docs/skills.md +80 -0
- package/eslint-rules/bib-reachable-entry.mjs +75 -0
- package/eslint-rules/latex-language.mjs +29 -1
- package/eslint-rules/paper-stages.harness.mjs +1 -117
- package/eslint-rules/paper-stages.mjs +0 -120
- package/eslint-rules/paper-stages.mutations.mjs +0 -76
- package/eslint-rules/paper-typography.mjs +306 -191
- package/eslint-rules/paper-typography.test.mjs +337 -0
- package/eslint-rules/papers.harness.mjs +8 -12
- package/eslint-rules/papers.mjs +17 -31
- package/eslint-rules/pdf-last-page-balance.mjs +4 -7
- package/eslint-rules/review-frontmatter.mjs +118 -0
- package/eslint-rules/review-frontmatter.schema.json +36 -0
- package/eslint-rules/review-frontmatter.test.mjs +118 -0
- package/eslint-rules/sibling-frontmatter.mjs +19 -0
- package/eslint-rules/sibling-frontmatter.schema.json +14 -0
- package/eslint.config.mjs +0 -15
- package/fixtures/real-markdown-paper/baseline.json +6 -7
- package/fixtures/toolchain-mirror/catalog.txt +1 -0
- package/fixtures/toolchain-mirror/install-tl +4 -1
- package/fixtures/toolchain-mirror/stub-fmtutil-sys +4 -0
- package/fixtures/toolchain-mirror/stub-tlmgr +15 -0
- package/hooks/hooks.harness.mjs +39 -98
- package/hooks/hooks.mutations.mjs +6 -6
- package/hooks/paper-edit-guard.hook.d.mts +2 -3
- package/hooks/paper-edit-guard.hook.mjs +31 -42
- package/hooks/paper-skills-nudge.hook.mjs +21 -19
- package/hooks/paper-status-gates.hook.mjs +21 -19
- package/hooks/paper-status-gates.sh +8 -13
- package/lib/paper-config.d.mts +15 -25
- package/lib/paper-config.harness.mjs +18 -35
- package/lib/paper-config.mjs +83 -105
- package/lib/paper-config.mutations.mjs +2 -2
- package/lib/skill-checks.mjs +1 -1
- package/lib/skill-trigger-cases.harness.mjs +9 -8
- package/lib/skill-trigger-cases.mjs +7 -15
- package/package.json +1 -1
- package/scripts/eslint-report-guard.mjs +1 -1
- package/scripts/harness-api.frozen.json +0 -3
- package/scripts/mutation-batteries.frozen.json +5 -14
- package/skills/analyze-sibling-paper/SKILL.md +16 -12
- package/skills/analyze-sibling-paper/SKILL.md.spec.ts +15 -11
- package/skills/cold-read-diff/SKILL.md +20 -1
- package/skills/cold-read-diff/SKILL.md.spec.ts +19 -0
- package/skills/grade-paper-writing/SKILL.md +20 -1
- package/skills/grade-paper-writing/SKILL.md.spec.ts +19 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +0 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +8 -12
- package/skills/paper-pipeline/scripts/consumer.mjs +19 -33
- package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +6 -4
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +2 -11
- package/skills/plan-paper-timeline/SKILL.md +2 -2
- package/skills/plan-paper-timeline/SKILL.md.spec.ts +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +1 -1
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +2 -2
- package/skills/render-paper/check-render.sh +2 -3
- package/skills/render-paper/render-paper.harness.mjs +2 -2
- package/skills/verify-citations/SKILL.md +10 -1
- package/skills/verify-citations/SKILL.md.spec.ts +9 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +48 -26
- package/src/adapters/references/index.ts +102 -0
- package/src/adapters/references/reach.io.ts +12 -0
- package/src/build-engine.ts +6 -1
- package/src/build.harness.mjs +54 -6
- package/src/build.ts +89 -11
- package/src/cli.harness.mjs +149 -294
- package/src/cli.mutations.mjs +35 -44
- package/src/cli.ts +637 -328
- package/src/doctor.harness.mjs +41 -84
- package/src/doctor.mutations.mjs +9 -9
- package/src/doctor.ts +44 -89
- package/src/engine.harness.mjs +50 -0
- package/src/engine.ts +35 -0
- package/src/facts-file.test.ts +0 -7
- package/src/facts-file.ts +2 -2
- package/src/hooks-settings.harness.mjs +7 -92
- package/src/hooks-settings.mutations.mjs +2 -17
- package/src/hooks-settings.ts +18 -162
- package/src/init.test.ts +206 -0
- package/src/init.ts +187 -231
- package/src/link-skills.harness.mjs +1 -38
- package/src/link-skills.mutations.mjs +9 -5
- package/src/link-skills.ts +10 -34
- package/src/lint-fix.test.ts +120 -0
- package/src/linted-set.test.ts +118 -0
- package/src/new-paper.harness.mjs +2 -0
- package/src/new-paper.mutations.mjs +0 -14
- package/src/new-paper.test.ts +173 -14
- package/src/new-paper.ts +82 -30
- package/src/paper-files.test.ts +395 -0
- package/src/paper-files.ts +200 -0
- package/src/paper-settings-commands.test.ts +121 -122
- package/src/paper-settings.test.ts +93 -64
- package/src/paper-settings.ts +99 -97
- package/src/parse-args.test.ts +48 -0
- package/src/ports/check-references.ts +28 -0
- package/src/presets.ts +5 -15
- package/src/reference-rules.ts +229 -0
- package/src/references.test.ts +209 -0
- package/src/references.ts +138 -0
- package/src/rules-config.harness.mjs +24 -8
- package/src/rules-config.ts +19 -7
- package/src/rules-off.test.ts +111 -0
- package/src/structure.harness.mjs +4 -2
- package/src/structure.ts +3 -3
- package/src/tex-requirements.ts +2 -2
- package/src/toolchain.harness.mjs +41 -6
- package/src/toolchain.ts +29 -5
- package/src/types.ts +36 -18
- package/src/venue-rules.test.ts +23 -11
- package/src/venue-rules.ts +12 -14
- package/templates/paper/paperlint.json +1 -1
- package/eslint-rules/doc-fields.harness.mjs +0 -336
- package/eslint-rules/doc-fields.mjs +0 -186
- package/eslint-rules/doc-fields.mutations.mjs +0 -96
- package/eslint-rules/paper-typography.harness.mjs +0 -291
- package/eslint-rules/paper-typography.mutations.mjs +0 -131
- package/eslint-rules/review-findings-cause.harness.mjs +0 -228
- package/eslint-rules/review-findings-cause.mjs +0 -135
- package/eslint-rules/review-findings-cause.mutations.mjs +0 -72
- package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +0 -16
- package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +0 -17
- package/fixtures/review-findings-cause/clean.md +0 -17
- package/fixtures/review-findings-cause/defect.md +0 -14
- package/fixtures/review-findings-cause/old-debt.md +0 -14
- package/fixtures/review-findings-cause/quiet-in-fence.md +0 -16
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The online reference checks, as the `CheckReferences` port: `verify-cites` (the cited work
|
|
3
|
+
* exists, the title matches — Crossref, OpenAlex, Semantic Scholar, arXiv) and `bib-authors` (the
|
|
4
|
+
* authors are the published version's — DBLP). Both ship in this package's verify-citations skill;
|
|
5
|
+
* this adapter only runs them and shapes their answers.
|
|
6
|
+
*
|
|
7
|
+
* "Not checked" is decided up front, by one request: when Crossref cannot be reached at all, the
|
|
8
|
+
* checkers would degrade every entry to `unresolvable` / `unchecked`, which reads like a result.
|
|
9
|
+
* Saying `not-checked` with the reason is the honest record.
|
|
10
|
+
*/
|
|
11
|
+
import type {
|
|
12
|
+
CheckReferences,
|
|
13
|
+
EntryVerdict,
|
|
14
|
+
} from "../../ports/check-references.ts";
|
|
15
|
+
import { unreachable } from "./reach.io.ts";
|
|
16
|
+
// @ts-expect-error — a skill script in .mjs, it has no types
|
|
17
|
+
import * as cites from "../../../skills/verify-citations/scripts/verify-cites.mjs";
|
|
18
|
+
// @ts-expect-error — a skill script in .mjs, it has no types
|
|
19
|
+
import * as authors from "../../../skills/verify-citations/scripts/bib-authors.mjs";
|
|
20
|
+
|
|
21
|
+
interface CiteResult {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
readonly verdict: "true" | "false" | "unresolvable";
|
|
24
|
+
readonly reason?: string;
|
|
25
|
+
}
|
|
26
|
+
interface AuthorFinding {
|
|
27
|
+
readonly key: string;
|
|
28
|
+
readonly venue: string;
|
|
29
|
+
readonly missing: readonly string[];
|
|
30
|
+
readonly extra: readonly string[];
|
|
31
|
+
readonly orderDiffers: boolean;
|
|
32
|
+
}
|
|
33
|
+
interface KeyWhy {
|
|
34
|
+
readonly key: string;
|
|
35
|
+
readonly why: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const describeAuthors = (f: AuthorFinding): string =>
|
|
39
|
+
[
|
|
40
|
+
f.missing.length ? `missing ${f.missing.join(", ")}` : "",
|
|
41
|
+
f.extra.length ? `extra ${f.extra.join(", ")}` : "",
|
|
42
|
+
f.orderDiffers ? "order differs" : "",
|
|
43
|
+
]
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
.join("; ") + ` (DBLP: ${f.venue})`;
|
|
46
|
+
|
|
47
|
+
interface AuthorBuckets {
|
|
48
|
+
readonly findings: readonly AuthorFinding[];
|
|
49
|
+
readonly skipped: readonly KeyWhy[];
|
|
50
|
+
readonly unchecked: readonly KeyWhy[];
|
|
51
|
+
readonly matched: readonly string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const authorsOf = (key: string, a: AuthorBuckets): EntryVerdict["authors"] =>
|
|
55
|
+
a.findings.some((f) => f.key === key)
|
|
56
|
+
? "mismatch"
|
|
57
|
+
: a.unchecked.some((u) => u.key === key)
|
|
58
|
+
? "unchecked"
|
|
59
|
+
: a.matched.includes(key)
|
|
60
|
+
? "match"
|
|
61
|
+
: "skipped";
|
|
62
|
+
|
|
63
|
+
/** One entry's verdict from the two checkers' answers. */
|
|
64
|
+
function entryVerdict(
|
|
65
|
+
key: string,
|
|
66
|
+
found: readonly CiteResult[],
|
|
67
|
+
a: AuthorBuckets,
|
|
68
|
+
): EntryVerdict {
|
|
69
|
+
const c = found.find((x) => x.id === key);
|
|
70
|
+
const mismatch = a.findings.find((f) => f.key === key);
|
|
71
|
+
const why = [
|
|
72
|
+
c && c.verdict !== "true" ? c.reason : undefined,
|
|
73
|
+
mismatch ? describeAuthors(mismatch) : undefined,
|
|
74
|
+
a.unchecked.find((u) => u.key === key)?.why,
|
|
75
|
+
]
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.join("; ");
|
|
78
|
+
return {
|
|
79
|
+
key,
|
|
80
|
+
exists: c?.verdict ?? "unresolvable",
|
|
81
|
+
authors: authorsOf(key, a),
|
|
82
|
+
...(why ? { why } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const onlineReferences: CheckReferences = async (bib) => {
|
|
87
|
+
const why = await unreachable();
|
|
88
|
+
if (why !== null) return { kind: "not-checked", why };
|
|
89
|
+
const cache = {};
|
|
90
|
+
const found: CiteResult[] = [];
|
|
91
|
+
for (const c of cites.parseBib(bib) as { id?: string }[])
|
|
92
|
+
if (c.id) found.push(await cites.verifyCitationLive(c, { cache }));
|
|
93
|
+
const a = (await authors.checkAuthors(
|
|
94
|
+
authors.parseBib(bib),
|
|
95
|
+
)) as AuthorBuckets;
|
|
96
|
+
const keys = new Set([
|
|
97
|
+
...found.map((c) => c.id),
|
|
98
|
+
...a.findings.map((f) => f.key),
|
|
99
|
+
]);
|
|
100
|
+
const entries = [...keys].map((key) => entryVerdict(key, found, a));
|
|
101
|
+
return { kind: "checked", entries };
|
|
102
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Whether the citation services answer at all — the one request that decides "not checked". */
|
|
2
|
+
export async function unreachable(): Promise<string | null> {
|
|
3
|
+
try {
|
|
4
|
+
await fetch("https://api.crossref.org/", {
|
|
5
|
+
method: "HEAD",
|
|
6
|
+
signal: AbortSignal.timeout(10_000),
|
|
7
|
+
});
|
|
8
|
+
return null;
|
|
9
|
+
} catch (e) {
|
|
10
|
+
return `the citation services cannot be reached (${(e as Error).message})`;
|
|
11
|
+
}
|
|
12
|
+
}
|
package/src/build-engine.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { homedir } from "node:os";
|
|
|
13
13
|
import { delimiter, join } from "node:path";
|
|
14
14
|
import {
|
|
15
15
|
resolveEngine,
|
|
16
|
+
missingDependencies,
|
|
16
17
|
probeTree,
|
|
17
18
|
supportedPlatform,
|
|
18
19
|
whichOnPath,
|
|
@@ -103,7 +104,11 @@ export function gatherFacts(o: Resolved): EngineFacts {
|
|
|
103
104
|
(tree): TreeProbe => ({
|
|
104
105
|
label: `TeX Live ${tree.year} — paperlint cache (${tree.dir})`,
|
|
105
106
|
bin: tree.bin,
|
|
106
|
-
|
|
107
|
+
// Only a cache tree is asked for its dependencies: its tlmgr writes where we own.
|
|
108
|
+
missing: [
|
|
109
|
+
...probeTree(tree.bin, o.tex.packages, o.run),
|
|
110
|
+
...missingDependencies(tree.bin, o.run),
|
|
111
|
+
],
|
|
107
112
|
}),
|
|
108
113
|
);
|
|
109
114
|
const cache = usableTree(probes, (p) => p.missing.length === 0);
|
package/src/build.harness.mjs
CHANGED
|
@@ -74,6 +74,10 @@ function fakeTex({
|
|
|
74
74
|
log = "",
|
|
75
75
|
missing = false,
|
|
76
76
|
noPdf = false,
|
|
77
|
+
// `noLog`: pdflatex that dies before it opens paper.log (a format it cannot load); what it
|
|
78
|
+
// printed is `stdout`.
|
|
79
|
+
noLog = false,
|
|
80
|
+
stdout = "",
|
|
77
81
|
} = {}) {
|
|
78
82
|
const calls = [];
|
|
79
83
|
const run = (bin, args, opts) => {
|
|
@@ -85,11 +89,11 @@ function fakeTex({
|
|
|
85
89
|
};
|
|
86
90
|
if (bin === "pdflatex") {
|
|
87
91
|
writeFileSync(join(opts.cwd, "paper.aux"), aux);
|
|
88
|
-
writeFileSync(join(opts.cwd, "paper.log"), log);
|
|
92
|
+
if (!noLog) writeFileSync(join(opts.cwd, "paper.log"), log);
|
|
89
93
|
// `noPdf`: what real pdflatex does on a document with no pages — "No pages of output.", exit 0.
|
|
90
94
|
if (exitCode === 0 && !noPdf)
|
|
91
95
|
writeFileSync(join(opts.cwd, "paper.pdf"), "%PDF-fake");
|
|
92
|
-
return { status: exitCode, stdout
|
|
96
|
+
return { status: exitCode, stdout, stderr: "" };
|
|
93
97
|
}
|
|
94
98
|
if (bin === "bibtex") {
|
|
95
99
|
writeFileSync(join(opts.cwd, "paper.bbl"), "\\begin{thebibliography}{1}");
|
|
@@ -273,10 +277,14 @@ try {
|
|
|
273
277
|
// ── the measure step: facts for the lint rules ────────────────────────────────────────
|
|
274
278
|
const factsFile = join(clean, "_build", "paper.facts.json");
|
|
275
279
|
check(
|
|
276
|
-
"measure: the plan's
|
|
277
|
-
r.plan.
|
|
278
|
-
/pdf\.js → _build\/paper\.facts\.json/.test(
|
|
279
|
-
|
|
280
|
+
"measure: the plan's measure step writes the facts, and says it judges nothing",
|
|
281
|
+
r.plan.find((p) => p.step === "measure") !== undefined &&
|
|
282
|
+
/pdf\.js → _build\/paper\.facts\.json/.test(
|
|
283
|
+
r.plan.find((p) => p.step === "measure")?.why ?? "",
|
|
284
|
+
) &&
|
|
285
|
+
/nothing is judged/.test(
|
|
286
|
+
r.plan.find((p) => p.step === "measure")?.why ?? "",
|
|
287
|
+
),
|
|
280
288
|
);
|
|
281
289
|
check(
|
|
282
290
|
"🔴 measure: a green build wrote _build/paper.facts.json through the readPdf port, on paper.pdf",
|
|
@@ -391,6 +399,46 @@ try {
|
|
|
391
399
|
check("…and says the PDF was removed", /paper\.pdf removed/.test(shown));
|
|
392
400
|
check("a failed pass is not retried", ftex.calls.length === 1);
|
|
393
401
|
|
|
402
|
+
// ── pdflatex dies before writing a log ────────────────────────────────────────────────
|
|
403
|
+
// Seen 2026-09-26: a TeX Live whose pdflatex format was never built. pdflatex exits 1 having
|
|
404
|
+
// printed the reason, and paper.log is either absent or left over from an earlier build.
|
|
405
|
+
const nolog = paper("nolog", {
|
|
406
|
+
"paper.tex": CLEAN_TEX,
|
|
407
|
+
"paper.log": "./paper.tex:9: OLD ERROR FROM YESTERDAY\nl.9 \\old\n",
|
|
408
|
+
});
|
|
409
|
+
const dieTex = fakeTex({
|
|
410
|
+
exitCode: 1,
|
|
411
|
+
noLog: true,
|
|
412
|
+
stdout:
|
|
413
|
+
"This is pdfTeX, Version 3.141592653-2.6-1.40.28 (TeX Live 2026) (INITEX)\n" +
|
|
414
|
+
"I can't find the format file `pdflatex.fmt'!\n",
|
|
415
|
+
});
|
|
416
|
+
const rn = await buildPaper(nolog, {
|
|
417
|
+
cwd: root,
|
|
418
|
+
readPdf: fakeRead,
|
|
419
|
+
run: dieTex.run,
|
|
420
|
+
env: ENV,
|
|
421
|
+
log: quiet,
|
|
422
|
+
});
|
|
423
|
+
const shownN = formatResult(rn);
|
|
424
|
+
// Guards: the reason pdflatex printed reaches the human when there is no log to quote.
|
|
425
|
+
check(
|
|
426
|
+
"🔴 no log: the failure shows what pdflatex printed",
|
|
427
|
+
rn.status === "failed" && shownN.includes("I can't find the format file"),
|
|
428
|
+
shownN,
|
|
429
|
+
);
|
|
430
|
+
// Guards: a log from an earlier build is never quoted as this build's error.
|
|
431
|
+
check(
|
|
432
|
+
"🔴 no log: a stale paper.log from an earlier build is not quoted",
|
|
433
|
+
!shownN.includes("OLD ERROR FROM YESTERDAY"),
|
|
434
|
+
shownN,
|
|
435
|
+
);
|
|
436
|
+
check(
|
|
437
|
+
"no log: it does not point at a log file that this run did not write",
|
|
438
|
+
!shownN.includes("full log:") && shownN.includes("wrote no paper.log"),
|
|
439
|
+
shownN,
|
|
440
|
+
);
|
|
441
|
+
|
|
394
442
|
// ── pdflatex not installed ────────────────────────────────────────────────────────────
|
|
395
443
|
const rm = await buildPaper(clean, {
|
|
396
444
|
cwd: root,
|
package/src/build.ts
CHANGED
|
@@ -45,7 +45,7 @@ import { delimiter, join, relative } from "node:path";
|
|
|
45
45
|
// eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
|
|
46
46
|
import { getParser } from "@unified-latex/unified-latex-util-parse";
|
|
47
47
|
import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
|
|
48
|
-
import {
|
|
48
|
+
import { CONFIG_FILE } from "../lib/paper-config.mjs";
|
|
49
49
|
import {
|
|
50
50
|
declaredVenue,
|
|
51
51
|
factsPath,
|
|
@@ -64,6 +64,8 @@ import { whyNoGeometry } from "./domain/geometry.ts";
|
|
|
64
64
|
import type { AbsolutePath } from "./domain/paths.ts";
|
|
65
65
|
import type { Files } from "./ports/files.ts";
|
|
66
66
|
import type { MeasureGeometry } from "./ports/measure-geometry.ts";
|
|
67
|
+
import type { CheckReferences } from "./ports/check-references.ts";
|
|
68
|
+
import { recordReferences, REFERENCES_FILE } from "./references.ts";
|
|
67
69
|
import {
|
|
68
70
|
auxBib,
|
|
69
71
|
bibtexExcerpt,
|
|
@@ -87,7 +89,7 @@ export const PAPER_MARKERS = [
|
|
|
87
89
|
"PIPELINE-STATUS.md",
|
|
88
90
|
"paper.tex",
|
|
89
91
|
"paper.md",
|
|
90
|
-
|
|
92
|
+
CONFIG_FILE,
|
|
91
93
|
];
|
|
92
94
|
|
|
93
95
|
/** The source paperlint compiles, and the job name every output file carries. */
|
|
@@ -135,6 +137,8 @@ export interface BuildContext {
|
|
|
135
137
|
readonly measure: MeasureGeometry;
|
|
136
138
|
/** Where the facts file is written. */
|
|
137
139
|
readonly files: Files;
|
|
140
|
+
/** The online reference checks; the CLI wires the real ones, a test passes a function. */
|
|
141
|
+
readonly checkReferences: CheckReferences;
|
|
138
142
|
}
|
|
139
143
|
|
|
140
144
|
export type StepOutcome =
|
|
@@ -350,9 +354,8 @@ function latexPass(
|
|
|
350
354
|
const r = ctx.run("pdflatex", pdflatexArgs(final), opts);
|
|
351
355
|
if (r.error) return null;
|
|
352
356
|
const exitCode = r.status ?? 1;
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
);
|
|
357
|
+
const written = readOr(join(ctx.paperDir, `${JOB}.log`), "latin1");
|
|
358
|
+
const log = unwrapLog(written ?? "");
|
|
356
359
|
return {
|
|
357
360
|
step: "latex",
|
|
358
361
|
final,
|
|
@@ -361,10 +364,30 @@ function latexPass(
|
|
|
361
364
|
after: hashes(ctx.paperDir),
|
|
362
365
|
markers: logMarkers(log),
|
|
363
366
|
bib: bibInput(ctx.paperDir),
|
|
364
|
-
errorLines:
|
|
367
|
+
errorLines:
|
|
368
|
+
exitCode === 0
|
|
369
|
+
? []
|
|
370
|
+
: written === null
|
|
371
|
+
? printed(r)
|
|
372
|
+
: errorExcerpt(log).map(fromLatin1),
|
|
365
373
|
};
|
|
366
374
|
}
|
|
367
375
|
|
|
376
|
+
/**
|
|
377
|
+
* The last lines pdflatex printed, for the run that wrote no log — it died before opening one (a
|
|
378
|
+
* format it cannot load, a missing TeX Live file). Marked by NO_LOG, which the report keys on.
|
|
379
|
+
*/
|
|
380
|
+
function printed(r: { stdout?: unknown; stderr?: unknown }): string[] {
|
|
381
|
+
const lines = fromLatin1(
|
|
382
|
+
`${String(r.stdout ?? "")}\n${String(r.stderr ?? "")}`,
|
|
383
|
+
)
|
|
384
|
+
.split("\n")
|
|
385
|
+
.filter((l) => l.trim());
|
|
386
|
+
return [NO_LOG, ...lines.slice(-8).map((l) => ` ${l}`)];
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const NO_LOG = `pdflatex wrote no ${JOB}.log — the last lines it printed:`;
|
|
390
|
+
|
|
368
391
|
/** One bibtex run, on the input the `.aux` names BEFORE it runs, or null when it could not start. */
|
|
369
392
|
function bibtexPass(ctx: BuildContext, opts: SpawnOptions): Observation | null {
|
|
370
393
|
const before = hashes(ctx.paperDir);
|
|
@@ -391,6 +414,9 @@ export function compile(ctx: BuildContext): {
|
|
|
391
414
|
} {
|
|
392
415
|
const history: Observation[] = [];
|
|
393
416
|
const opts = spawnOptions(ctx);
|
|
417
|
+
// Every log this loop reads is one its own pdflatex wrote. A pdflatex that dies before opening
|
|
418
|
+
// paper.log would otherwise have an earlier build's error quoted as this one's.
|
|
419
|
+
rmSync(join(ctx.paperDir, `${JOB}.log`), { force: true });
|
|
394
420
|
const runs = { latex: 0, bibtex: 0 };
|
|
395
421
|
for (;;) {
|
|
396
422
|
const step = nextStep(summarize(history));
|
|
@@ -446,7 +472,7 @@ export const compileStep: BuildStep = {
|
|
|
446
472
|
...(end.lines.length
|
|
447
473
|
? end.lines
|
|
448
474
|
: ["(no error line found in the log)"]),
|
|
449
|
-
`full log: ${log}
|
|
475
|
+
...(end.lines[0] === NO_LOG ? [] : [`full log: ${log}`]),
|
|
450
476
|
],
|
|
451
477
|
};
|
|
452
478
|
}
|
|
@@ -521,11 +547,36 @@ export const measureStep: BuildStep = {
|
|
|
521
547
|
},
|
|
522
548
|
};
|
|
523
549
|
|
|
550
|
+
// ── step: references ────────────────────────────────────────────────────────────────────
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Check the bibliography online — the cited work exists and its title matches (verify-cites), its
|
|
554
|
+
* authors are the published version's (bib-authors) — and record the verdicts in
|
|
555
|
+
* `_build/references.json` for the offline lint rules (`reference-rules.ts`). OPTIONAL and NEVER
|
|
556
|
+
* FAILING: without network the PDF is still built, and the record says `not-checked`.
|
|
557
|
+
*/
|
|
558
|
+
export const referencesStep: BuildStep = {
|
|
559
|
+
name: "references",
|
|
560
|
+
required: false,
|
|
561
|
+
applies: (facts) =>
|
|
562
|
+
facts.main
|
|
563
|
+
? {
|
|
564
|
+
yes: true,
|
|
565
|
+
why: `online: citations exist, titles and authors match → ${FACTS_DIR}/${REFERENCES_FILE} (never fails the build)`,
|
|
566
|
+
}
|
|
567
|
+
: { yes: false, why: "nothing is compiled" },
|
|
568
|
+
run: async (ctx) => ({
|
|
569
|
+
ok: true,
|
|
570
|
+
note: await recordReferences(ctx.files, ctx.paperDir, ctx.checkReferences),
|
|
571
|
+
}),
|
|
572
|
+
};
|
|
573
|
+
|
|
524
574
|
/** The build, in order. A new step is one entry here. */
|
|
525
575
|
export const STEPS: readonly BuildStep[] = [
|
|
526
576
|
inputsStep,
|
|
527
577
|
compileStep,
|
|
528
578
|
measureStep,
|
|
579
|
+
referencesStep,
|
|
529
580
|
];
|
|
530
581
|
|
|
531
582
|
// ── the command ─────────────────────────────────────────────────────────────────────────
|
|
@@ -574,6 +625,11 @@ export interface BuildOptions {
|
|
|
574
625
|
measure?: MeasureGeometry;
|
|
575
626
|
/** Where the facts file is written. Default: the disk. */
|
|
576
627
|
files?: Files;
|
|
628
|
+
/**
|
|
629
|
+
* The online reference checks. Default: none — the record then says `not-checked`, and lint
|
|
630
|
+
* warns. The CLI passes the real ones (`adapters/references`).
|
|
631
|
+
*/
|
|
632
|
+
checkReferences?: CheckReferences;
|
|
577
633
|
}
|
|
578
634
|
|
|
579
635
|
/**
|
|
@@ -594,13 +650,21 @@ function baseDefaults({
|
|
|
594
650
|
dryRun = false,
|
|
595
651
|
readPdf = pdfjsReader,
|
|
596
652
|
projectRoot = env["CLAUDE_PROJECT_DIR"] || cwd,
|
|
597
|
-
}: BuildOptions): Required<
|
|
653
|
+
}: BuildOptions): Required<
|
|
654
|
+
Omit<BuildOptions, "measure" | "files" | "checkReferences">
|
|
655
|
+
> {
|
|
598
656
|
return { run, cwd, env, steps, log, dryRun, readPdf, projectRoot };
|
|
599
657
|
}
|
|
600
658
|
|
|
659
|
+
/** Without a checker the record says so — never a pass. The CLI wires the real one. */
|
|
660
|
+
const notWired: CheckReferences = async () => ({
|
|
661
|
+
kind: "not-checked",
|
|
662
|
+
why: "no reference checker was wired into this build",
|
|
663
|
+
});
|
|
664
|
+
|
|
601
665
|
/** banal as the measurer, wired from the build's environment: the one piece of root work left here (#76). */
|
|
602
666
|
function defaultMeasurer(
|
|
603
|
-
b: Required<Omit<BuildOptions, "measure" | "files">>,
|
|
667
|
+
b: Required<Omit<BuildOptions, "measure" | "files" | "checkReferences">>,
|
|
604
668
|
): MeasureGeometry {
|
|
605
669
|
const dirs = hostDirs({ cwd: b.cwd });
|
|
606
670
|
return banalMeasurer(
|
|
@@ -614,7 +678,12 @@ function defaultMeasurer(
|
|
|
614
678
|
function withDefaults(o: BuildOptions): Required<BuildOptions> {
|
|
615
679
|
const base = baseDefaults(o);
|
|
616
680
|
const measure = o.measure ?? defaultMeasurer(base);
|
|
617
|
-
return {
|
|
681
|
+
return {
|
|
682
|
+
...base,
|
|
683
|
+
measure,
|
|
684
|
+
files: o.files ?? nodeFiles,
|
|
685
|
+
checkReferences: o.checkReferences ?? notWired,
|
|
686
|
+
};
|
|
618
687
|
}
|
|
619
688
|
|
|
620
689
|
/** Run the applicable steps in order, each on the environment the steps before it left. */
|
|
@@ -622,7 +691,15 @@ async function runSteps(
|
|
|
622
691
|
paperDir: string,
|
|
623
692
|
dir: string,
|
|
624
693
|
plan: PlanLine[],
|
|
625
|
-
{
|
|
694
|
+
{
|
|
695
|
+
run,
|
|
696
|
+
env,
|
|
697
|
+
steps,
|
|
698
|
+
readPdf,
|
|
699
|
+
measure,
|
|
700
|
+
files,
|
|
701
|
+
checkReferences,
|
|
702
|
+
}: Required<BuildOptions>,
|
|
626
703
|
): Promise<BuildResult> {
|
|
627
704
|
let stepEnv = env;
|
|
628
705
|
const notes: string[] = [];
|
|
@@ -635,6 +712,7 @@ async function runSteps(
|
|
|
635
712
|
readPdf,
|
|
636
713
|
measure,
|
|
637
714
|
files,
|
|
715
|
+
checkReferences,
|
|
638
716
|
});
|
|
639
717
|
if (!out.ok) {
|
|
640
718
|
// The PDF THIS run wrote and the step then rejected (a partial pass).
|