paperlint 3.0.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.
Files changed (73) hide show
  1. package/README.md +179 -180
  2. package/dist/build-engine.d.ts.map +1 -1
  3. package/dist/build-engine.js +6 -2
  4. package/dist/build-engine.js.map +1 -1
  5. package/dist/build.d.ts.map +1 -1
  6. package/dist/build.js +22 -3
  7. package/dist/build.js.map +1 -1
  8. package/dist/cli.d.ts +66 -5
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +230 -30
  11. package/dist/cli.js.map +1 -1
  12. package/dist/engine.d.ts +9 -0
  13. package/dist/engine.d.ts.map +1 -1
  14. package/dist/engine.js +27 -0
  15. package/dist/engine.js.map +1 -1
  16. package/dist/init.js +1 -1
  17. package/dist/init.js.map +1 -1
  18. package/dist/new-paper.d.ts +8 -1
  19. package/dist/new-paper.d.ts.map +1 -1
  20. package/dist/new-paper.js +50 -15
  21. package/dist/new-paper.js.map +1 -1
  22. package/dist/paper-files.d.ts +30 -8
  23. package/dist/paper-files.d.ts.map +1 -1
  24. package/dist/paper-files.js +56 -29
  25. package/dist/paper-files.js.map +1 -1
  26. package/dist/structure.js +1 -1
  27. package/dist/structure.js.map +1 -1
  28. package/dist/toolchain.d.ts +5 -1
  29. package/dist/toolchain.d.ts.map +1 -1
  30. package/dist/toolchain.js +22 -5
  31. package/dist/toolchain.js.map +1 -1
  32. package/dist/types.d.ts +6 -0
  33. package/dist/types.d.ts.map +1 -1
  34. package/dist/venue-rules.js +1 -1
  35. package/dist/venue-rules.js.map +1 -1
  36. package/docs/configuration.md +48 -4
  37. package/docs/rules.md +29 -25
  38. package/docs/skills.md +80 -0
  39. package/eslint-rules/paper-typography.mjs +47 -7
  40. package/eslint-rules/paper-typography.test.mjs +40 -0
  41. package/eslint-rules/review-frontmatter.schema.json +1 -1
  42. package/fixtures/toolchain-mirror/catalog.txt +1 -0
  43. package/fixtures/toolchain-mirror/install-tl +4 -1
  44. package/fixtures/toolchain-mirror/stub-fmtutil-sys +4 -0
  45. package/fixtures/toolchain-mirror/stub-tlmgr +15 -0
  46. package/package.json +1 -1
  47. package/scripts/mutation-batteries.frozen.json +1 -1
  48. package/src/build-engine.ts +6 -1
  49. package/src/build.harness.mjs +46 -2
  50. package/src/build.ts +27 -5
  51. package/src/cli.harness.mjs +2 -2
  52. package/src/cli.ts +301 -32
  53. package/src/engine.harness.mjs +50 -0
  54. package/src/engine.ts +35 -0
  55. package/src/init.ts +1 -1
  56. package/src/linted-set.test.ts +118 -0
  57. package/src/new-paper.harness.mjs +2 -0
  58. package/src/new-paper.mutations.mjs +0 -14
  59. package/src/new-paper.test.ts +172 -1
  60. package/src/new-paper.ts +84 -17
  61. package/src/paper-files.test.ts +110 -5
  62. package/src/paper-files.ts +93 -37
  63. package/src/paper-settings-commands.test.ts +41 -5
  64. package/src/parse-args.test.ts +48 -0
  65. package/src/rules-off.test.ts +111 -0
  66. package/src/structure.harness.mjs +2 -2
  67. package/src/structure.ts +1 -1
  68. package/src/toolchain.harness.mjs +41 -6
  69. package/src/toolchain.ts +29 -5
  70. package/src/types.ts +6 -0
  71. package/src/venue-rules.test.ts +3 -0
  72. package/src/venue-rules.ts +1 -1
  73. package/templates/paper/paperlint.json +1 -1
@@ -50,6 +50,14 @@
50
50
  * inserts the zero there. The arXiv lookbehind stays: in `2310.05736` the dot follows a digit.
51
51
  * A text macro outside the known set (`\hl{.05}`) is read as a parameter and NOT reported β€” a
52
52
  * miss costs less than a finding on markup.
53
+ *
54
+ * πŸ”΄ A NUMBER AFTER `ΒΆ` OR `Β§` IS A DESIGNATOR, NOT A DECIMAL (#102). An auditing standard is cited
55
+ * by paragraph as `ΒΆΒΆ.42`, and `--fix` rewrote it to `ΒΆΒΆ0.42` β€” a fix that changed a reference.
56
+ * So a lexeme right after the sign (`ΒΆ`, `Β§`, `\P`, `\S`, spaces or `~` between) is silent. The
57
+ * same sentence then goes on `and .44`: a paragraph number continuing the list, or a decimal β€”
58
+ * the text does not say which. In a paragraph that already cites `ΒΆ.nn` this way, a bare decimal
59
+ * is REPORTED WITHOUT A FIX and the zero is offered as a suggestion: an autofix must never be the
60
+ * one to decide what a number means. A real `.05` in any other paragraph is fixed as before.
53
61
  */
54
62
  import { getParser } from "@unified-latex/unified-latex-util-parse";
55
63
 
@@ -312,16 +320,43 @@ function sectionWord(context) {
312
320
  // (2310.05736) out: there the dot follows a digit.
313
321
  const BARE_DECIMAL = /(?<![\d.\w])\.\d{2,}\b/g;
314
322
 
323
+ // A paragraph or section sign β€” the glyph, or `\P`/`\S` not followed by a letter β€” and what may
324
+ // sit between it and the number. Both read the SOURCE, because in LaTeX `\P\P.42` reaches the
325
+ // visible runs as `.42` alone: the macros end the run.
326
+ const DESIGNATOR_BEFORE = /(?:[ΒΆΒ§]|\\[PS](?![A-Za-z]))[ \t~]*$/;
327
+ const DESIGNATED = /(?:[ΒΆΒ§]|\\[PS](?![A-Za-z]))[ \t~]*\.\d/;
328
+
329
+ /** The source of the paragraph holding `i`, up to `i`: from the last blank line before it. */
330
+ const paragraphBefore = (src, i) => {
331
+ const head = src.slice(0, i);
332
+ const blank = [...head.matchAll(/\n[ \t]*\n/g)].at(-1);
333
+ return head.slice(blank ? blank.index + blank[0].length : 0);
334
+ };
335
+
315
336
  function leadingZero(context) {
316
- for (const r of visibleRuns(context.sourceCode))
337
+ const sc = context.sourceCode;
338
+ const src = isTex(sc) ? sc.raw : sc.text;
339
+ for (const r of visibleRuns(sc))
317
340
  for (const m of r.text.matchAll(BARE_DECIMAL)) {
318
341
  const dot = r.offs[m.index];
319
342
  if (dot === null || dot === undefined) continue;
320
- at(context, dot, dot + 1, {
321
- messageId: "bare",
322
- data: { n: m[0] },
323
- fix: (f) => f.insertTextBeforeRange([dot, dot], "0"),
324
- });
343
+ const before = paragraphBefore(src, dot);
344
+ if (DESIGNATOR_BEFORE.test(before)) continue;
345
+ const zero = (f) => f.insertTextBeforeRange([dot, dot], "0");
346
+ at(
347
+ context,
348
+ dot,
349
+ dot + 1,
350
+ DESIGNATED.test(before)
351
+ ? {
352
+ messageId: "ambiguous",
353
+ data: { n: m[0] },
354
+ suggest: [
355
+ { messageId: "insertZero", data: { n: m[0] }, fix: zero },
356
+ ],
357
+ }
358
+ : { messageId: "bare", data: { n: m[0] }, fix: zero },
359
+ );
325
360
  }
326
361
  }
327
362
 
@@ -349,13 +384,14 @@ function figureRefStyle(context) {
349
384
  }
350
385
  }
351
386
 
352
- const rule = (description, messages, check) => ({
387
+ const rule = (description, messages, check, meta = {}) => ({
353
388
  meta: {
354
389
  type: "suggestion",
355
390
  fixable: "code",
356
391
  docs: { description },
357
392
  schema: [],
358
393
  messages,
394
+ ...meta,
359
395
  },
360
396
  create: (context) => ({ "root:exit": () => check(context) }),
361
397
  });
@@ -373,8 +409,12 @@ export default {
373
409
  "a decimal below 1 written without its leading zero (IEEE / ISO 80000-1); fixable",
374
410
  {
375
411
  bare: "`{{n}}` has no leading zero β€” write `0{{n}}` (IEEE / ISO 80000-1); `paperlint lint --fix` inserts it",
412
+ ambiguous:
413
+ "`{{n}}` in a paragraph that cites `ΒΆ`/`Β§` numbers: a decimal without its leading zero, or another paragraph number? Not fixed automatically β€” write `0{{n}}` for a decimal, or put the sign before a paragraph number",
414
+ insertZero: "It is a decimal: write `0{{n}}`",
376
415
  },
377
416
  leadingZero,
417
+ { hasSuggestions: true },
378
418
  ),
379
419
  "figure-ref-style": rule(
380
420
  "`Fig.~\\ref` and `Figure~\\ref` mixed in one document; fixable to the majority form",
@@ -203,6 +203,46 @@ describe("paper/leading-zero", () => {
203
203
  });
204
204
  });
205
205
 
206
+ describe("paper/leading-zero β€” a number after ΒΆ or Β§ is not a decimal (#102)", () => {
207
+ // From a consumer's bibliography: `--fix` rewrote the paragraph number `ΒΆΒΆ.42` to `ΒΆΒΆ0.42`.
208
+ const CITE =
209
+ "PCAOB. \\emph{Audit of Financial Statements}, ΒΆΒΆ.42 (design\neffectiveness) and .44 (operating effectiveness).";
210
+
211
+ it.each([
212
+ ["ΒΆΒΆ.42", "Standard 5, ΒΆΒΆ.42 applies."],
213
+ ["ΒΆ .42", "Standard 5, ΒΆ .42 applies."],
214
+ ["\\P\\P.42", "Standard 5, \\P\\P.42 applies."],
215
+ ["Β§.12", "Rule 10b-5, Β§.12 applies."],
216
+ ])("SILENT: %s is a designator", async (_, body) => {
217
+ // Guards: the defect β€” the lexeme after a paragraph/section sign was read as a decimal.
218
+ expect(ids(await lint(doc(body)), "paper/leading-zero")).toEqual([]);
219
+ expect(await fixed(doc(body))).toBe(doc(body));
220
+ });
221
+
222
+ it("πŸ”΄ the list continuation `and .44` is reported WITHOUT a fix, with the zero as a suggestion", async () => {
223
+ const res = await lint(doc(CITE));
224
+ const found = ids(res, "paper/leading-zero");
225
+ expect(found.map((m) => [m.line, m.message.slice(0, 5)])).toEqual([
226
+ [4, "`.44`"],
227
+ ]);
228
+ expect(found[0].fix).toBeUndefined();
229
+ expect(found[0].suggestions).toHaveLength(1);
230
+ // Guards: --fix changes nothing in an ambiguous paragraph β€” neither number.
231
+ expect(await fixed(doc(CITE))).toBe(doc(CITE));
232
+ });
233
+
234
+ it("…while a real decimal still fixes, in markdown too, even beside a cited paragraph", async () => {
235
+ const body = `${CITE}\n\nWe use a threshold of .05 throughout.`;
236
+ expect(await fixed(doc(body))).toBe(
237
+ doc(`${CITE}\n\nWe use a threshold of 0.05 throughout.`),
238
+ );
239
+ const md = "See ΒΆΒΆ.42 and .44.\n\nSignificant at p<.05.\n";
240
+ expect(await fixed(md, "paper.md")).toBe(
241
+ "See ΒΆΒΆ.42 and .44.\n\nSignificant at p<0.05.\n",
242
+ );
243
+ });
244
+ });
245
+
206
246
  describe("paper/figure-ref-style", () => {
207
247
  it("the minority form is reported and rewritten to the majority", async () => {
208
248
  const src = doc("Figure~\\ref{a}, Figure~\\ref{b} and Fig.~\\ref{c}.");
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "urn:paperlint:review-frontmatter",
4
4
  "title": "paperlint review frontmatter",
5
- "description": "The YAML frontmatter of a review report under reviews/. A review with no `findings` key is not a findings record and is not checked for findings. A project adds its own required fields with the `reviewSchema` setting; its schema is applied together with this one.",
5
+ "description": "The YAML frontmatter of a review report under reviews/. A review with no `findings` key is not a findings record and is not checked for findings.",
6
6
  "type": "object",
7
7
  "properties": {
8
8
  "findings": {
@@ -3,3 +3,4 @@ acmart acmart.cls
3
3
  libertine libertine.sty
4
4
  texcount bin:texcount
5
5
  liar other.sty
6
+ unicode-data GraphemeBreakProperty.txt
@@ -17,11 +17,14 @@ if [ -n "${FAKE_INSTALL_TL_FAIL:-}" ]; then
17
17
  exit 1
18
18
  fi
19
19
  mkdir -p "$dir/bin/x86_64-linux"
20
- for t in pdflatex kpsewhich tlmgr; do
20
+ for t in pdflatex kpsewhich tlmgr fmtutil-sys; do
21
21
  cp "$here/stub-$t" "$dir/bin/x86_64-linux/$t"
22
22
  chmod +x "$dir/bin/x86_64-linux/$t"
23
23
  done
24
24
  : > "$dir/files.txt"
25
25
  # The real install-tl leaves release-texlive.txt at TEXDIR; stub-tlmgr reads the tree's year there.
26
26
  cp "$here/release-texlive.txt" "$dir/release-texlive.txt"
27
+ # The real install-tl, when a mirror fails to serve a package, prints "continuing anyway" and
28
+ # exits as if nothing happened. FAKE_INSTALL_TL_DROP names the package it lost.
29
+ if [ -n "${FAKE_INSTALL_TL_DROP:-}" ]; then echo "$FAKE_INSTALL_TL_DROP" > "$dir/missing-deps.txt"; fi
27
30
  echo "fake install-tl: scheme-basic into $dir"
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+ # Stands in for fmtutil-sys: records its arguments in fmtutil.log at the tree root.
3
+ bin=$(cd "$(dirname "$0")" && pwd)
4
+ echo "$*" >> "$bin/../../fmtutil.log"
@@ -9,6 +9,15 @@ while [ $# -gt 0 ]; do
9
9
  case "$1" in
10
10
  --repository) repo=$2; shift 2 ;;
11
11
  install) shift; break ;;
12
+ # `check depends` in tlmgr's own layout: every name in missing-deps.txt (what the stub
13
+ # install-tl was told to drop) is a dependency without a package. Not logged: it installs nothing.
14
+ check)
15
+ if [ -s "$tree/missing-deps.txt" ]; then
16
+ echo " DEPENDS WITHOUT PACKAGES:"
17
+ sed 's/$/ in: collection-basic latex-bin/' "$tree/missing-deps.txt"
18
+ exit 2
19
+ fi
20
+ exit 0 ;;
12
21
  *) shift ;;
13
22
  esac
14
23
  done
@@ -34,6 +43,12 @@ for p in "$@"; do
34
43
  status=1
35
44
  continue
36
45
  fi
46
+ # Installing a dropped dependency repairs the tree.
47
+ # Portable on purpose: macOS sed -i takes a backup suffix, GNU sed does not.
48
+ if [ -f "$tree/missing-deps.txt" ]; then
49
+ grep -vx "$p" "$tree/missing-deps.txt" > "$tree/missing-deps.tmp" || true
50
+ mv "$tree/missing-deps.tmp" "$tree/missing-deps.txt"
51
+ fi
37
52
  for item in ${line#* }; do
38
53
  case "$item" in
39
54
  bin:*) : > "$bin/${item#bin:}"; chmod +x "$bin/${item#bin:}" ;;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperlint",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "A linter for scientific papers written in LaTeX: catches mistakes before you submit to a conference or journal.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -99,7 +99,7 @@
99
99
  "cases": 6
100
100
  },
101
101
  "src/new-paper.mutations.mjs": {
102
- "cases": 4
102
+ "cases": 3
103
103
  },
104
104
  "src/structure.mutations.mjs": {
105
105
  "cases": 4
@@ -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
- missing: probeTree(tree.bin, o.tex.packages, o.run),
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);
@@ -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: "", stderr: "" };
96
+ return { status: exitCode, stdout, stderr: "" };
93
97
  }
94
98
  if (bin === "bibtex") {
95
99
  writeFileSync(join(opts.cwd, "paper.bbl"), "\\begin{thebibliography}{1}");
@@ -395,6 +399,46 @@ try {
395
399
  check("…and says the PDF was removed", /paper\.pdf removed/.test(shown));
396
400
  check("a failed pass is not retried", ftex.calls.length === 1);
397
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
+
398
442
  // ── pdflatex not installed ────────────────────────────────────────────────────────────
399
443
  const rm = await buildPaper(clean, {
400
444
  cwd: root,
package/src/build.ts CHANGED
@@ -354,9 +354,8 @@ function latexPass(
354
354
  const r = ctx.run("pdflatex", pdflatexArgs(final), opts);
355
355
  if (r.error) return null;
356
356
  const exitCode = r.status ?? 1;
357
- const log = unwrapLog(
358
- readOr(join(ctx.paperDir, `${JOB}.log`), "latin1") ?? "",
359
- );
357
+ const written = readOr(join(ctx.paperDir, `${JOB}.log`), "latin1");
358
+ const log = unwrapLog(written ?? "");
360
359
  return {
361
360
  step: "latex",
362
361
  final,
@@ -365,10 +364,30 @@ function latexPass(
365
364
  after: hashes(ctx.paperDir),
366
365
  markers: logMarkers(log),
367
366
  bib: bibInput(ctx.paperDir),
368
- errorLines: exitCode === 0 ? [] : errorExcerpt(log).map(fromLatin1),
367
+ errorLines:
368
+ exitCode === 0
369
+ ? []
370
+ : written === null
371
+ ? printed(r)
372
+ : errorExcerpt(log).map(fromLatin1),
369
373
  };
370
374
  }
371
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
+
372
391
  /** One bibtex run, on the input the `.aux` names BEFORE it runs, or null when it could not start. */
373
392
  function bibtexPass(ctx: BuildContext, opts: SpawnOptions): Observation | null {
374
393
  const before = hashes(ctx.paperDir);
@@ -395,6 +414,9 @@ export function compile(ctx: BuildContext): {
395
414
  } {
396
415
  const history: Observation[] = [];
397
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 });
398
420
  const runs = { latex: 0, bibtex: 0 };
399
421
  for (;;) {
400
422
  const step = nextStep(summarize(history));
@@ -450,7 +472,7 @@ export const compileStep: BuildStep = {
450
472
  ...(end.lines.length
451
473
  ? end.lines
452
474
  : ["(no error line found in the log)"]),
453
- `full log: ${log}`,
475
+ ...(end.lines[0] === NO_LOG ? [] : [`full log: ${log}`]),
454
476
  ],
455
477
  };
456
478
  }
@@ -996,7 +996,7 @@ check(
996
996
  );
997
997
  check(
998
998
  "and the finding is printed before the ESLint report, with its consequence",
999
- /missing `PIPELINE-STATUS\.md`/.test(r.out) && /is checked/.test(r.out),
999
+ /missing `PIPELINE-STATUS\.md`/.test(r.out) && /are skipped/.test(r.out),
1000
1000
  );
1001
1001
  check(
1002
1002
  'and it does NOT print "no findings" over something that was found',
@@ -1783,7 +1783,7 @@ console.log(
1783
1783
  "πŸ”΄ an optional rule turned on for a glob that reaches no paper.tex FAILS the run β€” not a green zero",
1784
1784
  silent.code === 1 &&
1785
1785
  silent.out.includes(
1786
- 'pdf/last-page-balance is turned on in "rules", but no linted paper.tex gets it',
1786
+ 'pdf/last-page-balance is turned on in "rules", but no paper.tex of the project gets it',
1787
1787
  ),
1788
1788
  );
1789
1789
  settings({