paperlint 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/.github/workflows/ci.yml +4 -4
  2. package/CLAUDE.md +3 -3
  3. package/LICENSE +1 -1
  4. package/README.md +25 -10
  5. package/action.harness.mjs +10 -10
  6. package/action.mutations.mjs +3 -3
  7. package/action.yml +11 -11
  8. package/bin/{rpp.mjs → paperlint.mjs} +2 -2
  9. package/dist/adapters/banal/invocation.js +3 -3
  10. package/dist/adapters/banal/invocation.js.map +1 -1
  11. package/dist/adapters/banal/run.d.ts +1 -1
  12. package/dist/adapters/banal/run.js +1 -1
  13. package/dist/adapters/banal/run.js.map +1 -1
  14. package/dist/adapters/banal/settings.d.ts +4 -4
  15. package/dist/adapters/banal/settings.d.ts.map +1 -1
  16. package/dist/adapters/banal/settings.js +4 -4
  17. package/dist/adapters/banal/settings.js.map +1 -1
  18. package/dist/adapters/banal/xml.js +1 -1
  19. package/dist/adapters/banal/xml.js.map +1 -1
  20. package/dist/adapters/curl/download.io.d.ts.map +1 -1
  21. package/dist/adapters/curl/download.io.js +2 -2
  22. package/dist/adapters/curl/download.io.js.map +1 -1
  23. package/dist/build-engine.d.ts +1 -1
  24. package/dist/build-engine.js +1 -1
  25. package/dist/build.js +1 -1
  26. package/dist/cli.d.ts +14 -29
  27. package/dist/cli.d.ts.map +1 -1
  28. package/dist/cli.js +36 -74
  29. package/dist/cli.js.map +1 -1
  30. package/dist/doctor.d.ts.map +1 -1
  31. package/dist/doctor.js +0 -2
  32. package/dist/doctor.js.map +1 -1
  33. package/dist/engine.d.ts +2 -2
  34. package/dist/engine.d.ts.map +1 -1
  35. package/dist/engine.js +1 -1
  36. package/dist/hooks-settings.d.ts +0 -18
  37. package/dist/hooks-settings.d.ts.map +1 -1
  38. package/dist/hooks-settings.js +70 -42
  39. package/dist/hooks-settings.js.map +1 -1
  40. package/dist/init.d.ts +2 -10
  41. package/dist/init.d.ts.map +1 -1
  42. package/dist/init.js +7 -39
  43. package/dist/init.js.map +1 -1
  44. package/dist/link-skills.js +2 -2
  45. package/dist/link-skills.js.map +1 -1
  46. package/dist/rules-config.d.ts +1 -1
  47. package/dist/rules-config.js +2 -2
  48. package/dist/toolchain.d.ts +2 -2
  49. package/dist/toolchain.d.ts.map +1 -1
  50. package/dist/toolchain.js +8 -8
  51. package/dist/toolchain.js.map +1 -1
  52. package/dist/types.d.ts +4 -4
  53. package/dist/types.d.ts.map +1 -1
  54. package/docs/configuration.md +6 -10
  55. package/docs/e2e.md +3 -4
  56. package/docs/install.md +11 -3
  57. package/docs/optional-rules.md +1 -1
  58. package/docs/rules.md +1 -1
  59. package/docs/toolchain.md +5 -5
  60. package/eslint-rules/paper-typography.mjs +1 -1
  61. package/eslint-rules/paper-typography.mutations.mjs +1 -1
  62. package/eslint-rules/pdf-last-page-balance.harness.mjs +5 -3
  63. package/fixtures/build-e2e/guards/paper.tex +1 -1
  64. package/fixtures/pdf-facts/README.md +1 -1
  65. package/fixtures/real-markdown-paper/baseline.json +1 -1
  66. package/fixtures/real-markdown-paper/baseline.mjs +2 -2
  67. package/fixtures/toolchain-mirror/install-tl +2 -2
  68. package/fixtures/toolchain-mirror/stub-pdflatex +1 -1
  69. package/fixtures/toolchain-mirror/stub-tlmgr +1 -1
  70. package/hooks/hooks.harness.mjs +1 -1
  71. package/lib/paper-config.harness.mjs +3 -3
  72. package/lib/paper-config.mjs +3 -3
  73. package/lib/skill-trigger-cases.harness.mjs +1 -1
  74. package/package.json +4 -4
  75. package/plugin/hooks/hooks.json +3 -3
  76. package/scripts/check.mjs +1 -1
  77. package/scripts/harness-api.frozen.json +1 -1
  78. package/scripts/harness-api.test.ts +2 -2
  79. package/scripts/layer-legacy.frozen.json +1 -1
  80. package/scripts/mutation-batteries-frozen.harness.mjs +1 -1
  81. package/scripts/mutation-batteries.frozen.json +2 -2
  82. package/scripts/release-config.test.ts +1 -1
  83. package/skills/paper-pipeline/description-language.eval.mjs +1 -1
  84. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +1 -1
  85. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +2 -2
  86. package/skills/paper-pipeline/pipeline-firing.eval.mjs +1 -1
  87. package/skills/paper-pipeline/pipeline-language.eval.mjs +1 -1
  88. package/skills/paper-pipeline/scripts/consumer.d.mts +1 -0
  89. package/skills/paper-pipeline/scripts/consumer.harness.mjs +10 -4
  90. package/skills/paper-pipeline/scripts/consumer.mjs +8 -1
  91. package/skills/paper-pipeline/scripts/consumer.mutations.mjs +1 -1
  92. package/skills/render-paper/SKILL.md +3 -3
  93. package/skills/render-paper/SKILL.md.spec.ts +2 -2
  94. package/skills/render-paper/extract-pdf-facts.harness.mjs +1 -1
  95. package/skills/submit-paper/references/venues/venue-profile.schema.json +1 -1
  96. package/src/adapters/banal/index.test.ts +1 -1
  97. package/src/adapters/banal/invocation.test.ts +3 -3
  98. package/src/adapters/banal/invocation.ts +3 -3
  99. package/src/adapters/banal/locate.test.ts +4 -4
  100. package/src/adapters/banal/run.ts +2 -2
  101. package/src/adapters/banal/settings.test.ts +12 -6
  102. package/src/adapters/banal/settings.ts +9 -5
  103. package/src/adapters/banal/xml.test.ts +1 -1
  104. package/src/adapters/banal/xml.ts +1 -1
  105. package/src/adapters/curl/download.io.ts +4 -2
  106. package/src/adapters/curl/download.test.ts +3 -1
  107. package/src/adapters/node/files.test.ts +1 -1
  108. package/src/adapters/node/process.test.ts +2 -2
  109. package/src/adapters/node/workspace.test.ts +4 -4
  110. package/src/build-engine.harness.mjs +2 -2
  111. package/src/build-engine.ts +1 -1
  112. package/src/build.harness.mjs +6 -6
  113. package/src/build.mutations.mjs +3 -2
  114. package/src/build.ts +1 -1
  115. package/src/cli.harness.mjs +68 -129
  116. package/src/cli.mutations.mjs +7 -32
  117. package/src/cli.ts +46 -92
  118. package/src/doctor.harness.mjs +9 -25
  119. package/src/doctor.ts +0 -2
  120. package/src/engine.harness.mjs +4 -2
  121. package/src/engine.ts +2 -2
  122. package/src/hooks-settings.harness.mjs +24 -15
  123. package/src/hooks-settings.mutations.mjs +2 -4
  124. package/src/hooks-settings.ts +92 -49
  125. package/src/init.ts +7 -49
  126. package/src/latex-log.harness.mjs +1 -1
  127. package/src/link-skills.harness.mjs +3 -1
  128. package/src/link-skills.mutations.mjs +1 -1
  129. package/src/link-skills.ts +2 -2
  130. package/src/new-paper.harness.mjs +1 -1
  131. package/src/pdf-facts.harness.mjs +1 -1
  132. package/src/rules-config.ts +2 -2
  133. package/src/structure.harness.mjs +1 -1
  134. package/src/tex-requirements.harness.mjs +1 -1
  135. package/src/toolchain.harness.mjs +34 -25
  136. package/src/toolchain.ts +14 -8
  137. package/src/types.ts +4 -4
  138. package/plugin/.claude-plugin/plugin.json +0 -8
@@ -1,5 +1,5 @@
1
1
  {
2
- "issue": "https://github.com/zernie/research-paper-pipeline/issues/77",
2
+ "issue": "https://github.com/zernie/paperlint/issues/77",
3
3
  "why": "A *.harness.* file tests the agent surface and imports runHook, runHarnessTest or runEval from vigiles. Plain unit tests are *.test.ts, run by vitest. These are the harness files that existed on main on 2026-09-25 and import none of the three. The list only shrinks: rename a file to *.test.ts (issue #77), or make it a real harness, then delete its entry. Checked by scripts/harness-api.test.ts, part of npm test.",
4
4
  "harnesses": [
5
5
  "action.harness.mjs",
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * The rule is read from the file's IMPORTS, parsed with the TypeScript compiler — not from its
7
7
  * folder and not from a text search: a comment or a string that mentions `runHook` is not an import,
8
- * and `src/cli.harness.mjs` calls a function of rpp's own that happens to be called `runHook`.
8
+ * and `src/cli.harness.mjs` calls a function of paperlint's own that happens to be called `runHook`.
9
9
  *
10
10
  * Harnesses that predate the rule and import none of the three are frozen in
11
11
  * `harness-api.frozen.json` (issue #77). That list only shrinks: an entry that now imports the API,
@@ -156,7 +156,7 @@ test("an import is what counts — not a mention, not a same-named local functio
156
156
  const mention =
157
157
  '// runHook\nconst s = "runHarnessTest";\nimport { x } from "vigiles";\n';
158
158
  assert.equal(importsAgentApi("a.harness.mjs", mention), false);
159
- // Guards: rpp's own `runHook` (src/cli.ts) is not vigiles'.
159
+ // Guards: paperlint's own `runHook` (src/cli.ts) is not vigiles'.
160
160
  assert.equal(
161
161
  importsAgentApi(
162
162
  "b.harness.mjs",
@@ -1,5 +1,5 @@
1
1
  {
2
- "issue": "https://github.com/zernie/research-paper-pipeline/issues/76",
2
+ "issue": "https://github.com/zernie/paperlint/issues/76",
3
3
  "why": "Legacy src/*.ts files that still do I/O or import an adapter or a tool package directly. May only shrink: fix a site, delete its directive, lower the count here (delete the file entry at zero). A new file may not appear. Counted from ESLint suppressedMessages by scripts/layer-legacy-frozen.mjs, part of npm run check.",
4
4
  "files": {
5
5
  "src/build-engine.ts": {
@@ -60,7 +60,7 @@ const driverBattery = (k) =>
60
60
  `import { runMutations } from "./lib/mutation-driver.mjs";\n` +
61
61
  `process.exit(runMutations({ root: ".", cases: [${Array.from({ length: k }, (_, i) => `{ name: "c${String(i)}" }`).join(", ")}] }));\n`;
62
62
 
63
- const TMP = realpathSync(mkdtempSync(join(tmpdir(), "rpp-frozen-h-")));
63
+ const TMP = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-frozen-h-")));
64
64
  process.on("exit", () => rmSync(TMP, { recursive: true, force: true }));
65
65
  mkdirSync(join(TMP, "scripts"), { recursive: true });
66
66
  mkdirSync(join(TMP, "src"), { recursive: true });
@@ -1,5 +1,5 @@
1
1
  {
2
- "issue": "https://github.com/zernie/research-paper-pipeline/issues/52",
2
+ "issue": "https://github.com/zernie/paperlint/issues/52",
3
3
  "why": "Hand-written *.mutations.mjs batteries are being removed (#52). This list may only shrink: no new batteries, no new cases in existing ones. Delete a battery or a case, then delete its entry or lower its count here. Checked by scripts/mutation-batteries-frozen.mjs, part of npm run check.",
4
4
  "batteries": {
5
5
  "action.mutations.mjs": {
@@ -96,7 +96,7 @@
96
96
  "cases": 11
97
97
  },
98
98
  "src/cli.mutations.mjs": {
99
- "cases": 28
99
+ "cases": 26
100
100
  },
101
101
  "src/doctor.mutations.mjs": {
102
102
  "cases": 10
@@ -53,7 +53,7 @@ const fix = {
53
53
  const context = {
54
54
  cwd: process.cwd(),
55
55
  options: {
56
- repositoryUrl: "https://github.com/zernie/research-paper-pipeline",
56
+ repositoryUrl: "https://github.com/zernie/paperlint",
57
57
  },
58
58
  lastRelease: { gitTag: "v1.0.0", version: "1.0.0" },
59
59
  nextRelease: { gitTag: "v2.0.0", version: "2.0.0", type: "major" },
@@ -236,7 +236,7 @@ const CASES = [
236
236
 
237
237
  // Preflight: a misspelled skill or a changed namespace makes every `fired` predicate permanently
238
238
  // false, and the run then reports a wall of confident 0.00s as though the descriptions were dead.
239
- // Through `installedSkills`, which follows the links `paperlint init` makes (rpp#62).
239
+ // Through `installedSkills`, which follows the links `paperlint init` makes (paperlint#62).
240
240
  const installed = new Set(installedSkills(SKILLS_DIR));
241
241
  for (const c of CASES) {
242
242
  if (!installed.has(c.skill))
@@ -433,7 +433,7 @@ const CELL_LABEL = {
433
433
  // as though the descriptions were dead. That is the failure mode this block exists to prevent.
434
434
 
435
435
  // 1. the skills exist and declare the names we test against
436
- // Through `installedSkills`, which follows the links `paperlint init` makes (rpp#62).
436
+ // Through `installedSkills`, which follows the links `paperlint init` makes (paperlint#62).
437
437
  const installed = installedSkills(SKILLS_DIR);
438
438
  const installedSet = new Set(installed);
439
439
  for (const c of CASES) {
@@ -586,7 +586,7 @@ if (ONLY.length && RUN_ARMS.length !== ONLY.length)
586
586
  // a wall of confident 0.00s that read like a finding.
587
587
 
588
588
  // 1. the roster is what the parent measured
589
- // Through `installedSkills`, which follows the links `paperlint init` makes (rpp#62).
589
+ // Through `installedSkills`, which follows the links `paperlint init` makes (paperlint#62).
590
590
  const installed = installedSkills(SKILLS_DIR);
591
591
  const installedSet = new Set(installed);
592
592
  if (installedSet.has(ARMS[5].rename))
@@ -737,7 +737,7 @@ function buildArm(a) {
737
737
  rmSync(dir, { recursive: true, force: true });
738
738
  mkdirSync(dir, { recursive: true });
739
739
  // 🔴 ONE SKILL AT A TIME, FROM ITS REALPATH — not `cpSync(SKILLS_DIR, dir, { recursive })`.
740
- // In a consumer every entry of SKILLS_DIR is a link `paperlint init` made (rpp#62), and `cpSync` copies
740
+ // In a consumer every entry of SKILLS_DIR is a link `paperlint init` made (paperlint#62), and `cpSync` copies
741
741
  // a link as a link, rewritten to an ABSOLUTE path into the original; `dereference: true` does not
742
742
  // change that for nested entries (measured, Node 22.22). The arm's `writeFileSync` below would
743
743
  // then land in the real SKILL.md — "the real one is never written" would be false in exactly the
@@ -397,7 +397,7 @@ const irrelevantFor = (c) => {
397
397
  function assertSkillIdsExist() {
398
398
  if (!existsSync(SKILLS_DIR))
399
399
  throw new Error(`no skills dir at ${SKILLS_DIR}`);
400
- // Through `installedSkills`, which follows the links `paperlint init` makes (rpp#62).
400
+ // Through `installedSkills`, which follows the links `paperlint init` makes (paperlint#62).
401
401
  const installed = new Set(installedSkills(SKILLS_DIR));
402
402
  const missing = CASES.map((c) => c.skill).filter((s) => !installed.has(s));
403
403
  if (missing.length)
@@ -449,7 +449,7 @@ const CASES = [
449
449
  function assertSkillIdsExist() {
450
450
  if (!existsSync(SKILLS_DIR))
451
451
  throw new Error(`no skills dir at ${SKILLS_DIR}`);
452
- // Through `installedSkills`, which follows the links `paperlint init` makes (rpp#62).
452
+ // Through `installedSkills`, which follows the links `paperlint init` makes (paperlint#62).
453
453
  const installed = new Set(installedSkills(SKILLS_DIR));
454
454
  const missing = CASES.map((c) => c.skill).filter((s) => !installed.has(s));
455
455
  if (missing.length)
@@ -30,3 +30,4 @@ export declare function packageVenuesDir(): string;
30
30
  export declare const SHIPPED_SKILLS_DIR: string;
31
31
  export declare const PACKAGE_NAME: "paperlint";
32
32
  export declare const LEGACY_PACKAGE_NAME: "research-paper-pipeline";
33
+ export declare const BIN_FILE: "bin/paperlint.mjs";
@@ -43,6 +43,7 @@ import {
43
43
  pipelineScripts,
44
44
  scriptsRoot,
45
45
  PACKAGE_NAME,
46
+ BIN_FILE,
46
47
  } from "./consumer.mjs";
47
48
 
48
49
  const HERE = dirname(fileURLToPath(import.meta.url));
@@ -63,6 +64,11 @@ const HERE = dirname(fileURLToPath(import.meta.url));
63
64
  [PACKAGE_NAME],
64
65
  "the package exposes exactly one command, named like the package",
65
66
  );
67
+ assert.equal(
68
+ manifest.bin?.[PACKAGE_NAME],
69
+ BIN_FILE,
70
+ "package.json `bin` points at BIN_FILE from consumer.mjs — the file every hook command runs",
71
+ );
66
72
  const wiring = JSON.parse(
67
73
  readFileSync(join(root, "plugin", "hooks", "hooks.json"), "utf8"),
68
74
  );
@@ -73,9 +79,9 @@ const HERE = dirname(fileURLToPath(import.meta.url));
73
79
  assert.ok(
74
80
  commands.length > 0 &&
75
81
  commands.every((c) =>
76
- c.includes(`/node_modules/${PACKAGE_NAME}/bin/rpp.mjs`),
82
+ c.includes(`/node_modules/${PACKAGE_NAME}/${BIN_FILE}`),
77
83
  ),
78
- `every command in plugin/hooks/hooks.json runs node_modules/${PACKAGE_NAME}/bin/rpp.mjs`,
84
+ `every command in plugin/hooks/hooks.json runs node_modules/${PACKAGE_NAME}/${BIN_FILE}`,
79
85
  );
80
86
  }
81
87
  const TMP = realpathSync(mkdtempSync(join(tmpdir(), "consumer-harness-")));
@@ -472,7 +478,7 @@ assert.equal(
472
478
  );
473
479
  }
474
480
 
475
- // ── XI. WHICH SKILLS ARE INSTALLED — a link counts, a dangling link is REFUSED (rpp#62) ────────
481
+ // ── XI. WHICH SKILLS ARE INSTALLED — a link counts, a dangling link is REFUSED (paperlint#62) ────────
476
482
  // `paperlint init` installs every skill as a SYMLINK `.claude/skills/<name> -> …/skills/<name>`. A
477
483
  // `Dirent` from `readdirSync(…, { withFileTypes: true })` describes the entry ITSELF and does not
478
484
  // follow links, so `e.isDirectory()` is false for every one of them: the eval preflights that
@@ -506,7 +512,7 @@ assert.equal(
506
512
  const names = installedSkills(home);
507
513
  assert.ok(
508
514
  names.includes("linked"),
509
- "a SYMLINKED skill was not counted as installed. This is rpp#62: `paperlint init` installs every " +
515
+ "a SYMLINKED skill was not counted as installed. This is paperlint#62: `paperlint init` installs every " +
510
516
  "skill as a link, so a reader that does not follow links sees none of them in any consumer.",
511
517
  );
512
518
  assert.deepEqual(
@@ -151,6 +151,13 @@ export const PACKAGE_NAME = "paperlint";
151
151
  */
152
152
  export const LEGACY_PACKAGE_NAME = "research-paper-pipeline";
153
153
 
154
+ /**
155
+ * The package's one executable, relative to the package root — what `bin` in package.json names
156
+ * and what every hook command runs. Named like the package; `consumer.harness.mjs` checks it
157
+ * against the manifest, so the file and the hook commands cannot drift apart.
158
+ */
159
+ export const BIN_FILE = `bin/${PACKAGE_NAME}.mjs`;
160
+
154
161
  /**
155
162
  * Where the package keeps the skills it ships, relative to the package root. One constant, read by
156
163
  * the linker (`src/link-skills.ts`) and the install e2e, so the two cannot disagree; `files` in
@@ -168,7 +175,7 @@ export function consumerSkillsDir(opts) {
168
175
  * sorted by name. The one answer to "which skills are here" — for the consumer's
169
176
  * `.claude/skills/`, for this package's own declared skills directory, and for a copy of either.
170
177
  *
171
- * 🔴 IT FOLLOWS SYMLINKS, AND THAT IS THE WHOLE POINT (rpp#62). `paperlint init` (`src/link-skills.ts`)
178
+ * 🔴 IT FOLLOWS SYMLINKS, AND THAT IS THE WHOLE POINT (paperlint#62). `paperlint init` (`src/link-skills.ts`)
172
179
  * installs every skill as a link `.claude/skills/<name> -> …/skills/<name>`. A `Dirent` from
173
180
  * `readdirSync(dir, { withFileTypes: true })` describes the entry itself, so `isDirectory()` is
174
181
  * false for every link: five eval preflights asked the question that way, saw zero skills in
@@ -117,7 +117,7 @@ const MUTATIONS = [
117
117
  "the boundary between the pipeline's scripts and a directory merely named like them",
118
118
  ],
119
119
 
120
- // ── `installedSkills` (rpp#62). Its defects fail toward a WRONG ROSTER, not an error: a reader
120
+ // ── `installedSkills` (paperlint#62). Its defects fail toward a WRONG ROSTER, not an error: a reader
121
121
  // that does not follow links sees zero skills in every consumer; one that skips a dangling link
122
122
  // reports a broken install as "not installed"; one that counts it hands out an unreadable skill.
123
123
  [
@@ -4,7 +4,7 @@ description: Compile a LaTeX paper (ACM/IEEE/arXiv) to PDF and render its pages
4
4
  allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, SendUserFile]
5
5
  ---
6
6
 
7
- <!-- vigiles:sha256:e8f75efdcbdd7890 compiled from skills/render-paper/SKILL.md.spec.ts -->
7
+ <!-- vigiles:sha256:0943963cae95f8f1 compiled from skills/render-paper/SKILL.md.spec.ts -->
8
8
 
9
9
  # render-paper — .tex → PDF → readable page PNGs
10
10
 
@@ -38,7 +38,7 @@ That list was replaced on 2026-08-03; nothing about the toolchain below changed.
38
38
  ## Toolchain (one command, not a checklist)
39
39
  - **Compiler:** TeX Live, installed by paperlint with exactly the packages the venue profiles declare:
40
40
  ```
41
- npx paperlint toolchain # into ~/.cache/rpp/texlive (RPP_TEXLIVE_DIR overrides); idempotent
41
+ npx paperlint toolchain # into ~/.cache/paperlint/texlive (PAPERLINT_TEXLIVE_DIR overrides); idempotent
42
42
  npx paperlint toolchain --check # report what is missing, change nothing
43
43
  ```
44
44
  Measured 2026-09-24: 3 min from an empty directory, 269 MB, TeX Live 2026, 47 packages verified
@@ -50,7 +50,7 @@ That list was replaced on 2026-08-03; nothing about the toolchain below changed.
50
50
  package gets it there — never in a script, never here. A TeX Live without `libertine` builds an
51
51
  acmart paper GREEN in Computer Modern; that is why the files are checked, not the exit code.
52
52
  - **Reading the PDF** needs nothing installed: `paperlint build` and `extract-pdf-facts.mjs` use pdf.js,
53
- which comes with rpp. Page size, columns and font sizes come from banal (HotCRP's page-geometry
53
+ which comes with paperlint. Page size, columns and font sizes come from banal (HotCRP's page-geometry
54
54
  script), which `npx paperlint toolchain` installs and which runs on pdf.js output — it needs `perl`,
55
55
  and no poppler. Without banal those facts are `null` and the build says so.
56
56
  - **Renderer:** `pip install --quiet pymupdf` (system rasterizers and ghostscript are often missing or
@@ -44,7 +44,7 @@ That list was replaced on 2026-08-03; nothing about the toolchain below changed.
44
44
  ## Toolchain (one command, not a checklist)
45
45
  - **Compiler:** TeX Live, installed by paperlint with exactly the packages the venue profiles declare:
46
46
  \`\`\`
47
- npx paperlint toolchain # into ~/.cache/rpp/texlive (RPP_TEXLIVE_DIR overrides); idempotent
47
+ npx paperlint toolchain # into ~/.cache/paperlint/texlive (PAPERLINT_TEXLIVE_DIR overrides); idempotent
48
48
  npx paperlint toolchain --check # report what is missing, change nothing
49
49
  \`\`\`
50
50
  Measured 2026-09-24: 3 min from an empty directory, 269 MB, TeX Live 2026, 47 packages verified
@@ -56,7 +56,7 @@ That list was replaced on 2026-08-03; nothing about the toolchain below changed.
56
56
  package gets it there — never in a script, never here. A TeX Live without \`libertine\` builds an
57
57
  acmart paper GREEN in Computer Modern; that is why the files are checked, not the exit code.
58
58
  - **Reading the PDF** needs nothing installed: \`paperlint build\` and \`extract-pdf-facts.mjs\` use pdf.js,
59
- which comes with rpp. Page size, columns and font sizes come from banal (HotCRP's page-geometry
59
+ which comes with paperlint. Page size, columns and font sizes come from banal (HotCRP's page-geometry
60
60
  script), which \`npx paperlint toolchain\` installs and which runs on pdf.js output — it needs \`perl\`,
61
61
  and no poppler. Without banal those facts are \`null\` and the build says so.
62
62
  - **Renderer:** \`pip install --quiet pymupdf\` (system rasterizers and ghostscript are often missing or
@@ -36,7 +36,7 @@ const check = (label, cond, detail = "") => {
36
36
  n++;
37
37
  };
38
38
 
39
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-extract-")));
39
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-extract-")));
40
40
  /**
41
41
  * Run the shim from `root`, with no banal anywhere unless `env` names one. `HOME` is the temp root:
42
42
  * without it Node falls back to the account's home, where `paperlint toolchain` may have installed banal.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://github.com/zernie/research-paper-pipeline/venue-profile.schema.json",
3
+ "$id": "https://github.com/zernie/paperlint/venue-profile.schema.json",
4
4
  "title": "Venue profile",
5
5
  "description": "One venue's format profile (venues/<venue>.jsonc) and the TeX base set (venues/tex-base.jsonc). The format numbers are read by the consumer's pdf/profile rule; `tex` is read by `paperlint toolchain` and `paperlint build`.",
6
6
  "type": "object",
@@ -54,7 +54,7 @@ test("with $BANAL: banal gets the .xml and a quoted $PDFTOHTML, and the geometry
54
54
  [2, 1],
55
55
  );
56
56
  const c = run.calls[0];
57
- // Guards: the poppler-free path — banal is handed rpp's XML, never the PDF.
57
+ // Guards: the poppler-free path — banal is handed paperlint's XML, never the PDF.
58
58
  assert.match(c?.args.at(-1) ?? "", /\.xml$/);
59
59
  assert.match(c?.env["PDFTOHTML"] ?? "", /^'.*pdftohtml'$/);
60
60
  });
@@ -22,7 +22,7 @@ import {
22
22
  import type { LocatedBanal } from "./locate.ts";
23
23
  import { XML_DIALECT } from "./xml.ts";
24
24
 
25
- test("the staging holds BOTH files: the XML and the -v stub answering the dialect rpp writes", () => {
25
+ test("the staging holds BOTH files: the XML and the -v stub answering the dialect paperlint writes", () => {
26
26
  const [xml, stub] = stageBanalInput([]).files;
27
27
  assert.equal(xml.name, "paper.xml");
28
28
  assert.match(xml.content, /^<\?xml/);
@@ -50,14 +50,14 @@ test("shQuote survives a single quote", () => {
50
50
  assert.equal(shQuote("it's"), `'it'"'"'s'`);
51
51
  });
52
52
 
53
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-stage-test-")));
53
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-stage-test-")));
54
54
  after(() => rmSync(root, { recursive: true, force: true }));
55
55
 
56
56
  test("🔴 stage: on a real scratch directory the stub answers `-v` through the shell, from a path with a space and a quote", () => {
57
57
  const awkward = join(root, "it's a dir");
58
58
  mkdirSync(awkward);
59
59
  const { workspace } = nodeAdapters({ tmpDir: awkward });
60
- workspace.within("rpp-banal-", (s) => {
60
+ workspace.within("paperlint-banal-", (s) => {
61
61
  const staged = stage(s, stageBanalInput([]));
62
62
  assert.ok(existsSync(staged.xml));
63
63
  // Guards: banal runs `$PDFTOHTML -v 2>&1 |` through /bin/sh, unquoted.
@@ -30,13 +30,13 @@ export const shQuote = (s: string): ShQuoted =>
30
30
  /** The stub standing in for `pdftohtml`: it answers `-v` and refuses to convert anything. */
31
31
  export const PDFTOHTML_STUB = [
32
32
  "#!/bin/sh",
33
- "# research-paper-pipeline: banal reads the banal input XML rpp writes, never a PDF. banal still",
34
- "# asks `pdftohtml -v` which dialect to expect; this answers with the one rpp writes.",
33
+ "# paperlint: banal reads the banal input XML it writes, never a PDF. banal still",
34
+ "# asks `pdftohtml -v` which dialect to expect; this answers with the one paperlint writes.",
35
35
  'if [ "$1" = "-v" ]; then',
36
36
  ` echo "pdftohtml version ${XML_DIALECT.version}"`,
37
37
  " exit 0",
38
38
  "fi",
39
- 'echo "rpp: this pdftohtml only answers -v; banal was given a PDF instead of the banal input XML" >&2',
39
+ 'echo "paperlint: this pdftohtml only answers -v; banal was given a PDF instead of the banal input XML" >&2',
40
40
  "exit 1",
41
41
  "",
42
42
  ].join("\n");
@@ -8,7 +8,7 @@ import { parseBanalSettings } from "./settings.ts";
8
8
 
9
9
  const dirs = { home: "/h", tmp: "/t", cwd: "/w" };
10
10
  const project = "/p" as AbsolutePath;
11
- const cache = `/h/.cache/rpp/banal/${BANAL_PIN.commit.slice(0, 12)}/banal`;
11
+ const cache = `/h/.cache/paperlint/banal/${BANAL_PIN.commit.slice(0, 12)}/banal`;
12
12
  const on =
13
13
  (...files: string[]) =>
14
14
  (p: string) =>
@@ -22,11 +22,11 @@ test("installedBanal: one directory per HotCRP commit", () => {
22
22
  assert.equal(installedBanal(parseBanalSettings({}, dirs)), cache);
23
23
  });
24
24
 
25
- test("rpp's own copy when nothing else is named", () => {
25
+ test("paperlint's own copy when nothing else is named", () => {
26
26
  assert.equal(kind(pick({}, on(cache))), "cache");
27
27
  });
28
28
 
29
- test("a project's vendor/banal before rpp's copy", () => {
29
+ test("a project's vendor/banal before paperlint's copy", () => {
30
30
  // Guards: a project that vendors banal keeps using its own copy.
31
31
  assert.equal(kind(pick({}, on(cache, "/p/vendor/banal"))), "vendor");
32
32
  });
@@ -44,7 +44,7 @@ test("🔴 $BANAL naming a missing file finds NOTHING — no fallback to another
44
44
  );
45
45
  });
46
46
 
47
- test("nothing anywhere: not installed, naming where rpp's copy would be", () => {
47
+ test("nothing anywhere: not installed, naming where paperlint's copy would be", () => {
48
48
  assert.deepEqual(
49
49
  pick({}, on()),
50
50
  err({ kind: "not-installed", installed: cache }),
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * ── THE LICENCE BOUNDARY ────────────────────────────────────────────────────────
10
10
  * banal is GPL-2.0-or-later; paperlint is MIT. paperlint therefore never contains banal: `paperlint toolchain`
11
- * DOWNLOADS it from HotCRP at a pinned commit, checks its sha256, and stores it in rpp's cache, and
11
+ * DOWNLOADS it from HotCRP at a pinned commit, checks its sha256, and stores it in paperlint's cache, and
12
12
  * paperlint EXECUTES it as a separate program (`perl banal …`), reading its JSON output. Nothing of banal
13
13
  * is copied, vendored, linked or translated into this package.
14
14
  *
@@ -96,7 +96,7 @@ function runBanal(
96
96
  banal: LocatedBanal,
97
97
  pages: readonly PageLayout[],
98
98
  ): Result<BanalMeasurement, BanalFailure> {
99
- return d.workspace.within("rpp-banal-", (scratch) =>
99
+ return d.workspace.within("paperlint-banal-", (scratch) =>
100
100
  parseBanalOutput(
101
101
  d.run.run(
102
102
  banalCommand(
@@ -4,22 +4,28 @@ import { parseBanalSettings } from "./settings.ts";
4
4
 
5
5
  const dirs = { home: "/h", tmp: "/t", cwd: "/w" };
6
6
 
7
- test("cacheDir: $RPP_BANAL_DIR, else $XDG_CACHE_HOME/rpp/banal, else ~/.cache/rpp/banal", () => {
7
+ test("cacheDir: $PAPERLINT_BANAL_DIR, else $XDG_CACHE_HOME/paperlint/banal, else ~/.cache/paperlint/banal", () => {
8
8
  assert.equal(
9
- parseBanalSettings({ RPP_BANAL_DIR: "/x" }, dirs).cacheDir,
9
+ parseBanalSettings({ PAPERLINT_BANAL_DIR: "/x" }, dirs).cacheDir,
10
10
  "/x",
11
11
  );
12
12
  assert.equal(
13
13
  parseBanalSettings({ XDG_CACHE_HOME: "/c" }, dirs).cacheDir,
14
- "/c/rpp/banal",
14
+ "/c/paperlint/banal",
15
+ );
16
+ assert.equal(
17
+ parseBanalSettings({}, dirs).cacheDir,
18
+ "/h/.cache/paperlint/banal",
15
19
  );
16
- assert.equal(parseBanalSettings({}, dirs).cacheDir, "/h/.cache/rpp/banal");
17
20
  });
18
21
 
19
22
  test("an empty variable is unset, and a relative one is relative to the cwd the root read", () => {
20
- const s = parseBanalSettings({ BANAL: "b/banal", RPP_BANAL_DIR: "" }, dirs);
23
+ const s = parseBanalSettings(
24
+ { BANAL: "b/banal", PAPERLINT_BANAL_DIR: "" },
25
+ dirs,
26
+ );
21
27
  assert.equal(s.explicit, "/w/b/banal");
22
- assert.equal(s.cacheDir, "/h/.cache/rpp/banal");
28
+ assert.equal(s.cacheDir, "/h/.cache/paperlint/banal");
23
29
  assert.equal(parseBanalSettings({}, dirs).explicit, null);
24
30
  });
25
31
 
@@ -11,15 +11,15 @@ import {
11
11
  } from "../../domain/host.ts";
12
12
  import type { AbsolutePath } from "../../domain/paths.ts";
13
13
 
14
- /** An explicit banal to use instead of rpp's (a path to the script). */
14
+ /** An explicit banal to use instead of paperlint's (a path to the script). */
15
15
  export const BANAL_ENV = "BANAL";
16
- /** Where `paperlint toolchain` stores banal. Default: `$XDG_CACHE_HOME/rpp/banal`, else `~/.cache/rpp/banal`. */
17
- export const BANAL_DIR_ENV = "RPP_BANAL_DIR";
16
+ /** Where `paperlint toolchain` stores banal. Default: `$XDG_CACHE_HOME/paperlint/banal`, else `~/.cache/paperlint/banal`. */
17
+ export const BANAL_DIR_ENV = "PAPERLINT_BANAL_DIR";
18
18
 
19
19
  export interface BanalSettings {
20
20
  /** `$BANAL`: the user's explicit choice; null when unset. */
21
21
  readonly explicit: AbsolutePath | null;
22
- /** `$RPP_BANAL_DIR` › `$XDG_CACHE_HOME/rpp/banal` › `<home>/.cache/rpp/banal`. */
22
+ /** `$PAPERLINT_BANAL_DIR` › `$XDG_CACHE_HOME/paperlint/banal` › `<home>/.cache/paperlint/banal`. */
23
23
  readonly cacheDir: AbsolutePath;
24
24
  /** Where scratch directories go. */
25
25
  readonly tmpDir: AbsolutePath;
@@ -37,7 +37,11 @@ const set = (v: string | undefined): string | null => (v ? v : null);
37
37
  function cacheDirOf(env: Environment, d: HostDirs): string {
38
38
  return (
39
39
  set(env[BANAL_DIR_ENV]) ??
40
- join(set(env["XDG_CACHE_HOME"]) ?? join(d.home, ".cache"), "rpp", "banal")
40
+ join(
41
+ set(env["XDG_CACHE_HOME"]) ?? join(d.home, ".cache"),
42
+ "paperlint",
43
+ "banal",
44
+ )
41
45
  );
42
46
  }
43
47
 
@@ -47,7 +47,7 @@ const page = (boxes: TextBox[], widthPt = 612, heightPt = 792): PageLayout => ({
47
47
  const expected = [
48
48
  '<?xml version="1.0" encoding="UTF-8"?>',
49
49
  '<!DOCTYPE pdf2xml SYSTEM "pdf2xml.dtd">',
50
- '<pdf2xml producer="research-paper-pipeline (pdf.js)" version="24.02.0">',
50
+ '<pdf2xml producer="paperlint (pdf.js)" version="24.02.0">',
51
51
  '<page number="1" position="absolute" top="0" left="0" height="2376" width="1836">',
52
52
  '\t<fontspec id="0" size="30" family="LinLibertineT" color="#000000"/>',
53
53
  '<text top="30.000000" left="60.000000" width="90.000000" height="36.000000" font="0">Hello</text>',
@@ -104,7 +104,7 @@ export function pdf2xml(pages: readonly PageLayout[]): string {
104
104
  const lines = [
105
105
  '<?xml version="1.0" encoding="UTF-8"?>',
106
106
  '<!DOCTYPE pdf2xml SYSTEM "pdf2xml.dtd">',
107
- `<pdf2xml producer="research-paper-pipeline (pdf.js)" version="${XML_DIALECT.version}">`,
107
+ `<pdf2xml producer="paperlint (pdf.js)" version="${XML_DIALECT.version}">`,
108
108
  ...pages.flatMap((p, i) => pageXml(p, i + 1, fonts)),
109
109
  "</pdf2xml>",
110
110
  ];
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `Download` through `curl -fsSL`. Not Node's `fetch`: undici's `fetch` ignores `HTTPS_PROXY` unless
3
- * a proxy agent is installed, and rpp's CI and the containers it runs in reach the network only
3
+ * a proxy agent is installed, and paperlint's CI and the containers it runs in reach the network only
4
4
  * through a proxy. curl reads the proxy variables itself. (A claim from documentation, not measured
5
5
  * here — the port is what makes trying the other adapter cheap.)
6
6
  */
@@ -41,7 +41,9 @@ export function curlDownload(o: {
41
41
  }): Download {
42
42
  return {
43
43
  fetch(url: string, timeoutMs: number) {
44
- const dir = realpathSync(mkdtempSync(join(o.tmpDir, "rpp-download-")));
44
+ const dir = realpathSync(
45
+ mkdtempSync(join(o.tmpDir, "paperlint-download-")),
46
+ );
45
47
  const out = join(dir, "body");
46
48
  try {
47
49
  const seconds = String(Math.ceil(timeoutMs / 1000));
@@ -19,7 +19,9 @@ import type { AbsolutePath } from "../../domain/paths.ts";
19
19
  import { curlDownload } from "./download.io.ts";
20
20
  import { spawnProcess } from "../node/index.ts";
21
21
 
22
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-download-test-")));
22
+ const root = realpathSync(
23
+ mkdtempSync(join(tmpdir(), "paperlint-download-test-")),
24
+ );
23
25
  after(() => rmSync(root, { recursive: true, force: true }));
24
26
  const at = (...p: string[]) => join(root, ...p) as AbsolutePath;
25
27
 
@@ -7,7 +7,7 @@ import { afterAll as after, test } from "vitest";
7
7
  import type { AbsolutePath } from "../../domain/paths.ts";
8
8
  import { nodeFiles } from "./files.io.ts";
9
9
 
10
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-files-test-")));
10
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-files-test-")));
11
11
  after(() => rmSync(root, { recursive: true, force: true }));
12
12
  const at = (...p: string[]) => join(root, ...p) as AbsolutePath;
13
13
 
@@ -19,8 +19,8 @@ const sh = (script: string, timeoutMs = 10_000): Command => ({
19
19
  test("a program that is not there is `not-found`, and names the program", () => {
20
20
  // Guards: ENOENT is the executable itself — "perl is missing" rather than "banal failed".
21
21
  assert.deepEqual(
22
- run.run({ ...sh(""), file: "rpp-no-such-program-xyz", args: [] }),
23
- { kind: "not-found", file: "rpp-no-such-program-xyz" },
22
+ run.run({ ...sh(""), file: "paperlint-no-such-program-xyz", args: [] }),
23
+ { kind: "not-found", file: "paperlint-no-such-program-xyz" },
24
24
  );
25
25
  });
26
26
 
@@ -19,12 +19,12 @@ import { join } from "node:path";
19
19
  import { afterAll as after, test } from "vitest";
20
20
  import { tmpWorkspace } from "./workspace.io.ts";
21
21
 
22
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-ws-test-")));
22
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-ws-test-")));
23
23
  after(() => rmSync(root, { recursive: true, force: true }));
24
24
 
25
25
  test("the directory exists inside the scope and is gone after it", () => {
26
26
  const ws = tmpWorkspace(root);
27
- const dir = ws.within("rpp-banal-", (s) => {
27
+ const dir = ws.within("paperlint-banal-", (s) => {
28
28
  assert.ok(existsSync(s.dir));
29
29
  return s.dir;
30
30
  });
@@ -35,7 +35,7 @@ test("the directory exists inside the scope and is gone after it", () => {
35
35
  test("the directory is gone after a throw too", () => {
36
36
  let seen = "";
37
37
  assert.throws(() =>
38
- tmpWorkspace(root).within("rpp-banal-", (s) => {
38
+ tmpWorkspace(root).within("paperlint-banal-", (s) => {
39
39
  seen = s.dir;
40
40
  throw new Error("boom");
41
41
  }),
@@ -46,7 +46,7 @@ test("the directory is gone after a throw too", () => {
46
46
  test("🔴 write: the file lands in the scope's directory, and `exec` makes it runnable — from a path with a space and a quote", () => {
47
47
  const awkward = join(root, "it's a dir");
48
48
  mkdirSync(awkward);
49
- tmpWorkspace(awkward).within("rpp-ws-", (s) => {
49
+ tmpWorkspace(awkward).within("paperlint-ws-", (s) => {
50
50
  const plain = s.write("data.txt", "hello", "read");
51
51
  assert.equal(readFileSync(plain, "utf8"), "hello");
52
52
  const script = s.write("run", "#!/bin/sh\necho ran\n", "exec");
@@ -89,7 +89,7 @@ for (const [a, yes] of [
89
89
  check(`isYes(${JSON.stringify(a)}) is ${yes}`, E.isYes(a) === yes);
90
90
 
91
91
  // ── 2. prepareEngine ────────────────────────────────────────────────────────────────────
92
- const work = realpathSync(mkdtempSync(join(tmpdir(), "rpp-engine-h-")));
92
+ const work = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-engine-h-")));
93
93
  const TEX = {
94
94
  packages: { acmart: ["acmart.cls"], libertine: ["libertine.sty"] },
95
95
  tools: {},
@@ -144,7 +144,7 @@ const go = async (o) => {
144
144
  };
145
145
  };
146
146
  const envWith = (cacheRoot, path) => ({
147
- RPP_TEXLIVE_DIR: cacheRoot,
147
+ PAPERLINT_TEXLIVE_DIR: cacheRoot,
148
148
  PATH: path,
149
149
  });
150
150
 
@@ -86,7 +86,7 @@ function withDefaults(o: EngineOptions): Resolved {
86
86
  }
87
87
 
88
88
  /**
89
- * The facts `resolveEngine` decides on: rpp's cache and the PATH's TeX Live, each probed. The cache
89
+ * The facts `resolveEngine` decides on: paperlint's cache and the PATH's TeX Live, each probed. The cache
90
90
  * may hold one tree per TeX Live year; the one reported is the newest COMPLETE one, else the newest
91
91
  * (so a new year whose install was interrupted never wins over a complete older year).
92
92
  */