paperlint 11.1.3 → 12.0.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.
@@ -12,6 +12,7 @@ import { createChecker } from "../lib/check.ts";
12
12
  import type {
13
13
  BibInput,
14
14
  Hashes,
15
+ LogFailure,
15
16
  Observation,
16
17
  State,
17
18
  Step,
@@ -52,6 +53,7 @@ const latex = (o: Partial<LatexPass> = {}): LatexPass => ({
52
53
  bib: NO_BIB,
53
54
  errorLines: [],
54
55
  fls: null,
56
+ failing: [],
55
57
  ...o,
56
58
  });
57
59
  const bibtex = (o: Partial<BibtexPass> = {}): BibtexPass => ({
@@ -63,6 +65,14 @@ const bibtex = (o: Partial<BibtexPass> = {}): BibtexPass => ({
63
65
  errorLines: [],
64
66
  ...o,
65
67
  });
68
+ /** What latex-log reports for the end-of-run "There were undefined references." */
69
+ const undefRefs: LogFailure = {
70
+ id: "undefined-references",
71
+ reason: "a \\ref names a label no pass defined",
72
+ quotes: [
73
+ { text: "LaTeX Warning: There were undefined references.", times: 1 },
74
+ ],
75
+ };
66
76
  const kind = (s: Step) =>
67
77
  s.kind === "latex" ? `latex${s.final ? "-final" : ""}` : s.kind;
68
78
 
@@ -73,7 +83,6 @@ const S = (o: Partial<State> = {}): State => ({
73
83
  unsettled: [],
74
84
  latexPasses: 1,
75
85
  finalDone: false,
76
- warnings: [],
77
86
  ...o,
78
87
  });
79
88
  const exitFail: NonNullable<State["failed"]> = {
@@ -144,15 +153,6 @@ check(
144
153
  q3cap.cause.kind === "no-convergence" &&
145
154
  (q3cap.lines[0] ?? "").includes("paper.aux, labels-changed"),
146
155
  );
147
- check(
148
- "done carries the state's warnings",
149
- (() => {
150
- const done = nextStep(
151
- S({ finalDone: true, warnings: ["undefined-citations"] }),
152
- );
153
- return done.kind === "done" && done.warnings[0] === "undefined-citations";
154
- })(),
155
- );
156
156
 
157
157
  // ── 2. summarize over histories ─────────────────────────────────────────────────────────
158
158
  const sum = summarize;
@@ -165,7 +165,6 @@ check(
165
165
  unsettled: ["paper.aux"],
166
166
  latexPasses: 0,
167
167
  finalDone: false,
168
- warnings: [],
169
168
  }),
170
169
  );
171
170
  // Guards: failing on a failed pass — -halt-on-error stops pdflatex at the first error, and a loop
@@ -222,7 +221,7 @@ check(
222
221
  // never resolves, so rerunning for it burns the cap and reports no-convergence for a typo.
223
222
  check(
224
223
  "summarize: undefined references ALONE leave nothing unsettled — on stable files they are a real bad key",
225
- sum([latex({ markers: ["undefined-references"] })]).unsettled.length === 0,
224
+ sum([latex({ failing: [undefRefs] })]).unsettled.length === 0,
226
225
  );
227
226
  check(
228
227
  "summarize: bibtex that rewrote the .bbl leaves paper.bbl unsettled",
@@ -241,20 +240,8 @@ check(
241
240
  sum([latex(), bibtex(), latex(), latex({ final: true })]).latexPasses === 2,
242
241
  );
243
242
  check(
244
- "summarize: finalDone and the undefined-reference warnings of the final log, nothing else",
245
- (() => {
246
- const s = sum([
247
- latex(),
248
- latex({
249
- final: true,
250
- markers: ["undefined-citations", "rerun-requested"],
251
- }),
252
- ]);
253
- return (
254
- s.finalDone &&
255
- JSON.stringify(s.warnings) === JSON.stringify(["undefined-citations"])
256
- );
257
- })(),
243
+ "summarize: finalDone after a clean final pass",
244
+ sum([latex(), latex({ final: true })]).finalDone,
258
245
  );
259
246
 
260
247
  // ── 3. composed: nextStep(summarize(history)) ───────────────────────────────────────────
@@ -301,7 +288,7 @@ const TABLE: [string, Observation[], string][] = [
301
288
  ],
302
289
  [
303
290
  "undefined references ALONE do not request a rerun — on stable files they are a real bad key",
304
- [latex({ markers: ["undefined-references"] })],
291
+ [latex({ failing: [undefRefs] })],
305
292
  "latex-final",
306
293
  ],
307
294
  [
@@ -445,16 +432,65 @@ check(
445
432
  kind(step(convergedAtCap)) === "latex-final",
446
433
  );
447
434
 
448
- // ── done carries the undefined-reference summaries ─────────────────────────────────────
449
- const doneWarn = step([
435
+ // ── a warning on the list of failing ones (latex-log's FAILING_WARNINGS) fails the build ──
436
+ const lost: LogFailure = {
437
+ id: "lost-character",
438
+ reason: "pdflatex dropped a character its font has no glyph for",
439
+ quotes: [
440
+ { text: "Missing character: There is no \u207b in font cmr10!", times: 1 },
441
+ { text: "Missing character: There is no \ufffd in font cmtt10!", times: 3 },
442
+ ],
443
+ };
444
+ const lostFinal = step([latex(), latex({ final: true, failing: [lost] })]);
445
+ check(
446
+ "🔴 a failing warning in the FINAL log is a fail: latex, the ids, the reason and every quoted line",
447
+ lostFinal.kind === "fail" &&
448
+ lostFinal.step === "latex" &&
449
+ JSON.stringify(lostFinal.cause) ===
450
+ JSON.stringify({ kind: "log-warnings", ids: ["lost-character"] }) &&
451
+ JSON.stringify(lostFinal.lines) ===
452
+ JSON.stringify([
453
+ "lost-character — pdflatex dropped a character its font has no glyph for:",
454
+ " Missing character: There is no \u207b in font cmr10!",
455
+ " Missing character: There is no \ufffd in font cmtt10! (3 times)",
456
+ ]),
457
+ lostFinal,
458
+ );
459
+ check(
460
+ "a failing warning in a NON-final log decides nothing: the loop goes on to the final pass",
461
+ kind(step([latex({ failing: [lost] })])) === "latex-final",
462
+ );
463
+ const lostAndExit = step([
450
464
  latex(),
451
- latex({ final: true, markers: ["undefined-citations", "rerun-requested"] }),
465
+ latex({
466
+ final: true,
467
+ exitCode: 1,
468
+ errorLines: ["! Emergency stop."],
469
+ failing: [lost],
470
+ }),
452
471
  ]);
453
472
  check(
454
- "done reports undefined citations still in the final log, and nothing else",
455
- doneWarn.kind === "done" &&
456
- doneWarn.warnings.length === 1 &&
457
- doneWarn.warnings[0] === "undefined-citations",
473
+ "a final pass that also exited non-zero fails on its exit code, quoting the error",
474
+ lostAndExit.kind === "fail" &&
475
+ lostAndExit.cause.kind === "exit" &&
476
+ lostAndExit.lines[0] === "! Emergency stop.",
477
+ );
478
+ check(
479
+ "an undefined-references summary in the final log fails too — the same path, its own id",
480
+ (() => {
481
+ const s = step([latex(), latex({ final: true, failing: [undefRefs] })]);
482
+ return (
483
+ s.kind === "fail" &&
484
+ JSON.stringify(s.cause) ===
485
+ JSON.stringify({ kind: "log-warnings", ids: ["undefined-references"] })
486
+ );
487
+ })(),
488
+ );
489
+ check(
490
+ "a final log whose only marker is off the list (a rerun request) is done",
491
+ kind(
492
+ step([latex(), latex({ final: true, markers: ["rerun-requested"] })]),
493
+ ) === "done",
458
494
  );
459
495
 
460
496
  // ── helpers ─────────────────────────────────────────────────────────────────────────────
package/src/latex-loop.ts CHANGED
@@ -6,7 +6,9 @@
6
6
  * Both halves are table tests (`latex-loop.harness.ts`), not scenarios that need TeX.
7
7
  *
8
8
  * ── THE LOOP: `summarize` reduces the history to a `State`, and `nextStep` asks four questions ──
9
- * 1. did the last program fail? → fail, with its error lines
9
+ * 1. did the last program fail — a non-zero exit, or a
10
+ * final log carrying a warning on latex-log's list of
11
+ * failing ones (`FAILING_WARNINGS`)? → fail, with its lines
10
12
  * 2. did the citations, a .bib or the style change since
11
13
  * bibtex last ran? → bibtex
12
14
  * 3. did the last pass change a tracked file, or did the
@@ -43,12 +45,17 @@ export const TRACKED: readonly Tracked[] = ["aux", "toc", "out", "bbl"];
43
45
  export type Hashes = { readonly [K in Tracked]: string | null };
44
46
 
45
47
  /** Log markers, as `latex-log.ts` reads them. Re-declared as a type so this module has no imports. */
46
- export type LogMarker =
47
- | "rerun-requested"
48
- | "labels-changed"
49
- | "rerunfilecheck"
50
- | "undefined-references"
51
- | "undefined-citations";
48
+ export type LogMarker = "rerun-requested" | "labels-changed" | "rerunfilecheck";
49
+
50
+ /**
51
+ * A warning on `latex-log.ts`'s list of those that fail the build, as the log of one pass carries it:
52
+ * its id, why it fails, and each distinct line quoted once with how many times it appeared.
53
+ */
54
+ export interface LogFailure {
55
+ readonly id: string;
56
+ readonly reason: string;
57
+ readonly quotes: readonly { readonly text: string; readonly times: number }[];
58
+ }
52
59
 
53
60
  const RERUN: readonly LogMarker[] = [
54
61
  "rerun-requested",
@@ -88,6 +95,8 @@ export type Observation =
88
95
  readonly errorLines: readonly string[];
89
96
  /** What `-recorder` wrote after this pass (`paper.fls`), or null when pdflatex wrote none. */
90
97
  readonly fls: string | null;
98
+ /** The failing warnings this pass's log carries. Only the final pass's are judged. */
99
+ readonly failing: readonly LogFailure[];
91
100
  }
92
101
  | {
93
102
  readonly step: "bibtex";
@@ -101,16 +110,13 @@ export type Observation =
101
110
 
102
111
  export type FailCause =
103
112
  | { readonly kind: "exit"; readonly code: number }
104
- | { readonly kind: "no-convergence"; readonly unsettled: readonly string[] };
113
+ | { readonly kind: "no-convergence"; readonly unsettled: readonly string[] }
114
+ | { readonly kind: "log-warnings"; readonly ids: readonly string[] };
105
115
 
106
116
  export type Step =
107
117
  | { readonly kind: "latex"; readonly final: boolean }
108
118
  | { readonly kind: "bibtex" }
109
- | {
110
- readonly kind: "done";
111
- /** Undefined-reference summaries still in the final log — a converged doc with a bad key. */
112
- readonly warnings: readonly LogMarker[];
113
- }
119
+ | { readonly kind: "done" }
114
120
  | {
115
121
  readonly kind: "fail";
116
122
  readonly step: "latex" | "bibtex";
@@ -136,13 +142,11 @@ export type State = {
136
142
  /** Non-final pdflatex passes so far. */
137
143
  readonly latexPasses: number;
138
144
  readonly finalDone: boolean;
139
- /** Undefined-reference summaries in the log of the final pass. */
140
- readonly warnings: readonly LogMarker[];
141
145
  };
142
146
 
143
147
  export function nextStep(s: State): Step {
144
148
  if (s.failed) return { kind: "fail", ...s.failed };
145
- if (s.finalDone) return { kind: "done", warnings: s.warnings };
149
+ if (s.finalDone) return { kind: "done" };
146
150
  if (s.bibOutdated) return { kind: "bibtex" };
147
151
  if (s.unsettled.length === 0) return { kind: "latex", final: true };
148
152
  if (s.latexPasses < MAX_PASSES) return { kind: "latex", final: false };
@@ -183,31 +187,48 @@ function unsettledBy(history: readonly Observation[]): string[] {
183
187
  return [];
184
188
  }
185
189
 
190
+ /** A failing warning as the human reads it: the id and reason, then each quoted line. */
191
+ const failureLines = (f: LogFailure): readonly string[] => [
192
+ `${f.id} — ${f.reason}:`,
193
+ ...f.quotes.map(
194
+ (q) => ` ${q.text}${q.times > 1 ? ` (${String(q.times)} times)` : ""}`,
195
+ ),
196
+ ];
197
+
198
+ /**
199
+ * Did the last run fail? A non-zero exit, whichever program; or a FINAL pass whose log carries a
200
+ * failing warning. An earlier pass's log is not judged: before the rerun every reference is
201
+ * undefined by construction.
202
+ */
203
+ function failureOf(last: Observation | undefined): State["failed"] {
204
+ if (last === undefined) return null;
205
+ if (last.exitCode !== 0)
206
+ return {
207
+ step: last.step,
208
+ cause: { kind: "exit", code: last.exitCode },
209
+ lines: last.errorLines,
210
+ };
211
+ if (last.step !== "latex" || !last.final || last.failing.length === 0)
212
+ return null;
213
+ return {
214
+ step: "latex",
215
+ cause: { kind: "log-warnings", ids: last.failing.map((f) => f.id) },
216
+ lines: last.failing.flatMap(failureLines),
217
+ };
218
+ }
219
+
186
220
  export function summarize(history: readonly Observation[]): State {
187
221
  const last = history.at(-1);
188
222
  const latexRuns = history.filter((o) => o.step === "latex");
189
223
  const lastLatex = latexRuns.at(-1);
190
224
  const lastBibtex = history.filter((o) => o.step === "bibtex").at(-1);
191
225
  return {
192
- failed:
193
- last && last.exitCode !== 0
194
- ? {
195
- step: last.step,
196
- cause: { kind: "exit", code: last.exitCode },
197
- lines: last.errorLines,
198
- }
199
- : null,
226
+ failed: failureOf(last),
200
227
  bibOutdated:
201
228
  lastLatex?.bib.kind === "needed" &&
202
229
  !(lastBibtex && sameBib(lastBibtex.bib, lastLatex.bib)),
203
230
  unsettled: unsettledBy(history),
204
231
  latexPasses: latexRuns.filter((o) => !o.final).length,
205
232
  finalDone: last?.step === "latex" && last.final,
206
- warnings:
207
- last?.step === "latex"
208
- ? last.markers.filter(
209
- (m) => m === "undefined-references" || m === "undefined-citations",
210
- )
211
- : [],
212
233
  };
213
234
  }