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
@@ -27,6 +27,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
27
27
  import { basename, dirname, join, posix } from "node:path";
28
28
  import { fileURLToPath } from "node:url";
29
29
  import {
30
+ BIN_FILE,
30
31
  LEGACY_PACKAGE_NAME,
31
32
  PACKAGE_NAME,
32
33
  } from "../skills/paper-pipeline/scripts/consumer.mjs";
@@ -44,13 +45,7 @@ export const WIRING_FILE = fileURLToPath(
44
45
  * and `${CLAUDE_PROJECT_DIR}/` before comparing, so this is the same file as the spelling in
45
46
  * `hooks.json`, and nothing machine-specific lands in a committed file.
46
47
  */
47
- export const MANAGED_BY = `node_modules/${PACKAGE_NAME}/bin/rpp.mjs`;
48
-
49
- /**
50
- * The same token as an install under the package's old name wrote it. A command spelled this way
51
- * is ours from before the rename: `init` removes it and writes the current one, `doctor` names it.
52
- */
53
- export const LEGACY_MANAGED_BY = `node_modules/${LEGACY_PACKAGE_NAME}/bin/rpp.mjs`;
48
+ export const MANAGED_BY = `node_modules/${PACKAGE_NAME}/${BIN_FILE}`;
54
49
 
55
50
  /** The names the package's directory under `node_modules` has had. */
56
51
  const PKG_DIRS: readonly string[] = [PACKAGE_NAME, LEGACY_PACKAGE_NAME];
@@ -81,50 +76,91 @@ function bare(token: string): string {
81
76
  return unquoted.replace(/^\$\{?CLAUDE_PROJECT_DIR\}?[/\\]/, "");
82
77
  }
83
78
 
79
+ /** A hook command reduced to what it runs: the hook, and the path token that runs it. */
80
+ interface Run {
81
+ readonly name: string;
82
+ readonly ours: boolean;
83
+ readonly legacy: boolean;
84
+ readonly path: string;
85
+ }
86
+
84
87
  /**
85
88
  * Which paperlint hook a command runs, if any, and whether it is spelled the way `init` writes it.
86
89
  *
87
90
  * Recognised spellings — each is ONE lexeme of a shell command, which is what a command is:
88
- * `node <…>/node_modules/paperlint/bin/rpp.mjs hook <name>` ours when the path is exactly
89
- * MANAGED_BY
90
- * the same under `node_modules/research-paper-pipeline/` `legacy`: ours before the rename
91
- * `npx paperlint hook <name>`, `node_modules/.bin/paperlint hook <name>` (or the old bin names)
92
- * another spelling
91
+ * `node <…>/node_modules/paperlint/bin/paperlint.mjs hook <name>`
92
+ * ours when the path is exactly MANAGED_BY
93
+ * `node node_modules/<pkg>/bin/<any other file> hook <name>`, <pkg> this name or the old one
94
+ * `legacy`: the project-relative form `init` writes, pointing at a file
95
+ * this version does not install — what an older `init` left behind
96
+ * (1.x: `research-paper-pipeline/bin/rpp.mjs`; 2.0.0: `paperlint/bin/rpp.mjs`)
97
+ * `npx paperlint hook <name>`, `node_modules/.bin/paperlint hook <name>` (or under the old package name),
98
+ * an absolute path into the package's `bin/`
99
+ * another spelling
93
100
  * `vigiles … run-program <…>/node_modules/<pkg>/hooks/<name>.hook.mjs`
94
- * another spelling
101
+ * another spelling
95
102
  * The last is how the one real consumer wired all three by hand before `init` could.
103
+ *
104
+ * `legacy` is decided by the SHAPE `init` writes, not by a list of past file names: any file in
105
+ * our `bin/` other than BIN_FILE is one that no longer ships, so a future rename is covered too.
96
106
  */
107
+ function parseRun(command: string): Run | null {
108
+ const tokens = command.trim().split(/\s+/).map(bare);
109
+ for (let i = 0; i < tokens.length; i++) {
110
+ const run = runAt(tokens, i);
111
+ if (run) return run;
112
+ }
113
+ return null;
114
+ }
115
+
116
+ /** The path segments after `node_modules/<pkg>/`, and whether the token starts there. */
117
+ function insidePackage(parts: readonly string[]): {
118
+ readonly inside: readonly string[];
119
+ readonly relative: boolean;
120
+ } {
121
+ const at = parts.findIndex(
122
+ (p, j) => p === "node_modules" && PKG_DIRS.includes(parts[j + 1] ?? ""),
123
+ );
124
+ return {
125
+ inside: at === -1 ? [] : parts.slice(at + 2),
126
+ relative: at === 0,
127
+ };
128
+ }
129
+
130
+ /** The run token `i` starts, if it names a paperlint hook. */
131
+ function runAt(tokens: readonly string[], i: number): Run | null {
132
+ const t = posix.normalize(tokens[i] ?? "");
133
+ const { inside, relative } = insidePackage(t.split("/"));
134
+ const inBin = inside[0] === "bin" && inside.length === 2;
135
+ const name = tokens[i + 2];
136
+ if (
137
+ (inBin || PKG_DIRS.includes(basename(t))) &&
138
+ tokens[i + 1] === "hook" &&
139
+ name
140
+ )
141
+ return {
142
+ name,
143
+ ours: t === MANAGED_BY,
144
+ legacy: inBin && relative && t !== MANAGED_BY,
145
+ path: t,
146
+ };
147
+ if (inside[0] === "hooks" && inside[1]?.endsWith(".hook.mjs"))
148
+ return {
149
+ name: basename(inside[1], ".hook.mjs"),
150
+ ours: false,
151
+ legacy: false,
152
+ path: t,
153
+ };
154
+ return null;
155
+ }
156
+
97
157
  export function hookRun(command: string): {
98
158
  readonly name: string;
99
159
  readonly ours: boolean;
100
160
  readonly legacy: boolean;
101
161
  } | null {
102
- const tokens = command.trim().split(/\s+/).map(bare);
103
- for (let i = 0; i < tokens.length; i++) {
104
- const t = posix.normalize(tokens[i] ?? "");
105
- const parts = t.split("/");
106
- const at = parts.findIndex(
107
- (p, j) => p === "node_modules" && PKG_DIRS.includes(parts[j + 1] ?? ""),
108
- );
109
- const inside = at === -1 ? [] : parts.slice(at + 2);
110
- const isBin =
111
- (inside[0] === "bin" && inside[1] === "rpp.mjs") ||
112
- ["rpp", ...PKG_DIRS].includes(basename(t));
113
- const name = tokens[i + 2];
114
- if (isBin && tokens[i + 1] === "hook" && name)
115
- return {
116
- name,
117
- ours: t === MANAGED_BY,
118
- legacy: t === LEGACY_MANAGED_BY,
119
- };
120
- if (inside[0] === "hooks" && inside[1]?.endsWith(".hook.mjs"))
121
- return {
122
- name: basename(inside[1], ".hook.mjs"),
123
- ours: false,
124
- legacy: false,
125
- };
126
- }
127
- return null;
162
+ const run = parseRun(command);
163
+ return run && { name: run.name, ours: run.ours, legacy: run.legacy };
128
164
  }
129
165
 
130
166
  /** Every command in a settings object, with the event it hangs off. */
@@ -356,16 +392,23 @@ export const FRESH_CLONE_NOTE = `the commands point into node_modules/${PACKAGE_
356
392
  /** The instruction for a project that also enables the plugin (it only ever existed under the old name). */
357
393
  export const UNINSTALL_PLUGIN = `/plugin uninstall ${LEGACY_PACKAGE_NAME}@${LEGACY_PACKAGE_NAME}`;
358
394
 
359
- /** The doctor lines for hook commands an install under the old package name left behind. */
360
- function legacyLines(
361
- counts: ReadonlyMap<string, { readonly legacy: number }>,
362
- ): string[] {
363
- let legacy = 0;
364
- for (const c of counts.values()) legacy += c.legacy;
365
- return legacy === 0
395
+ /**
396
+ * The doctor lines for hook commands an older install left behind: each names the file it runs,
397
+ * which this version does not install, so the hook silently does nothing.
398
+ */
399
+ function legacyLines(settings: Settings): string[] {
400
+ const stale = new Map<string, number>();
401
+ for (const { command } of commandsIn(settings)) {
402
+ const run = parseRun(command);
403
+ if (run?.legacy) stale.set(run.path, (stale.get(run.path) ?? 0) + 1);
404
+ }
405
+ return stale.size === 0
366
406
  ? []
367
407
  : [
368
- ` ⚠ ${String(legacy)} hook command(s) still point into node_modules/${LEGACY_PACKAGE_NAME}/, the package's old name — they no longer run.`,
408
+ ...[...stale].map(
409
+ ([path, n]) =>
410
+ ` ⚠ ${String(n)} hook command(s) run ${path}, which this version does not install — they no longer run.`,
411
+ ),
369
412
  ` \`npx ${PACKAGE_NAME} init\` replaces them`,
370
413
  ];
371
414
  }
@@ -400,7 +443,7 @@ export function doctorHooks(
400
443
  out.push(` ⚠ wired TWICE — each of these runs more than once per event:`);
401
444
  for (const n of twice) out.push(` ${n} ×${String(total(n))}`);
402
445
  out.push(
403
- ` keep one command per hook; \`npx paperlint init\` writes the rpp.mjs form`,
446
+ ` keep one command per hook; \`npx paperlint init\` writes the ${MANAGED_BY} form`,
404
447
  );
405
448
  }
406
449
  if (missing.length === wiring.names.length)
@@ -418,7 +461,7 @@ export function doctorHooks(
418
461
  ` ✓ wired — ${wiring.names.join(", ")}, once each`,
419
462
  ` ${FRESH_CLONE_NOTE}`,
420
463
  );
421
- out.push(...legacyLines(counts));
464
+ out.push(...legacyLines(read.settings));
422
465
  const plugin = pluginEnabledHere(read.settings);
423
466
  if (plugin.length > 0) {
424
467
  out.push(
package/src/init.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * `paperlint init` — the whole install, in the terminal it was typed in.
3
3
  *
4
4
  * 🔴 WHAT THIS COMMAND USED TO DO, AND WHY THAT WAS A DEFECT RATHER THAN A SHORTFALL. It wrote
5
- * `rpp.json` with a GUESSED `"papers": "papers"` and never touched `package.json`. The three hooks
5
+ * a separate config file with a GUESSED `"papers": "papers"` and never touched `package.json`. The three hooks
6
6
  * read the papers directory out of `package.json` and nothing else, so a consumer who followed the
7
7
  * documented install got a `paper-edit-guard` watching a directory that did not exist — and a guard
8
8
  * watching nothing is byte-identical, from outside, to a guard that is working (issue #33).
@@ -15,7 +15,7 @@
15
15
  * carry a paper marker. Several hits is the only case a human is asked about.
16
16
  * declaration WRITTEN into `package.json`, merged, never overwriting a value that is
17
17
  * already there. Prior art: husky's `init` edits the consumer's package.json
18
- * to add `prepare`. `rpp.json` is no longer created at all.
18
+ * to add `prepare`. No second config file is created.
19
19
  * skills LINKED — one relative symlink per shipped skill into `.claude/skills/`, the
20
20
  * only place Claude Code looks for project skills (`link-skills.ts`). An
21
21
  * entry of the same name that paperlint did not make is reported, never replaced.
@@ -204,8 +204,8 @@ export type DeclarationResult =
204
204
  *
205
205
  * 🔴 A HOOK CANNOT IMPORT CODE AND CANNOT WALK UP A TREE LOOKING FOR A CONFIG. It can read a path
206
206
  * it is able to spell, and the only path it can always spell is the project's own `package.json`.
207
- * That asymmetry is the whole reason the declaration moved here rather than the readers moving to
208
- * `rpp.json`: many readers against one (`docs/install.md`, "One declaration").
207
+ * That asymmetry is the whole reason the declaration lives here rather than in a file of its own:
208
+ * many readers against one (`docs/install.md`, "One declaration").
209
209
  *
210
210
  * ⚠️ Merged, not rewritten, and never over a value the consumer set — an `init` that silently
211
211
  * replaces a setting is worse than an `init` that does nothing, because the consumer keeps
@@ -259,35 +259,6 @@ function renameKey(
259
259
  );
260
260
  }
261
261
 
262
- export type RppJsonResult = "absent" | "kept" | "filled" | "unparsable";
263
-
264
- /**
265
- * `rpp.json` is no longer CREATED — but a consumer who already has one keeps it working, and it
266
- * gets the same `papers` value rather than being left to disagree with `package.json` in silence.
267
- * Two declarations that disagree is the defect `paperlint doctor` was written to catch; writing the
268
- * second one on purpose would be handing it new work.
269
- */
270
- export function syncRppJson(root: string, papers: string): RppJsonResult {
271
- const path = join(root, "rpp.json");
272
- if (!existsSync(path)) return "absent";
273
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #49: replace with a real type
274
- let cfg: Record<string, any>;
275
- const raw = readFileSync(path, "utf8");
276
- try {
277
- cfg = JSON.parse(raw);
278
- } catch {
279
- return "unparsable";
280
- }
281
- if (cfg?.[PAPERS_DIR_FIELD] !== undefined) return "kept";
282
- cfg[PAPERS_DIR_FIELD] = papers;
283
- writeFileSync(
284
- path,
285
- JSON.stringify(cfg, null, 2) + (raw.endsWith("\n") ? "\n" : ""),
286
- "utf8",
287
- );
288
- return "filled";
289
- }
290
-
291
262
  export const WORKFLOW_PATH = join(".github", "workflows", "papers.yml");
292
263
 
293
264
  /** Where the action is pinned when no release tag is known — obviously a placeholder. */
@@ -310,7 +281,7 @@ export function workflowYaml(papers: string, ref: string | null): string {
310
281
  ` runs-on: ubuntu-latest`,
311
282
  ` steps:`,
312
283
  ` - uses: actions/checkout@v4`,
313
- ` - uses: zernie/research-paper-pipeline@${ref ?? UNPINNED_REF}`,
284
+ ` - uses: zernie/paperlint@${ref ?? UNPINNED_REF}`,
314
285
  ` with:`,
315
286
  ` paths: ${papers}`,
316
287
  ``,
@@ -380,7 +351,7 @@ export function reportWorkflow(
380
351
  if (wf !== "written" && wf !== "kept")
381
352
  out.push(
382
353
  ` to run the same checks in CI, add this step to a workflow:`,
383
- ` - uses: zernie/research-paper-pipeline@${ref ?? UNPINNED_REF}`,
354
+ ` - uses: zernie/paperlint@${ref ?? UNPINNED_REF}`,
384
355
  ` with:`,
385
356
  ` paths: ${papersDir}`,
386
357
  );
@@ -772,23 +743,10 @@ export async function init(
772
743
  ` one declaration — the hooks, the rules and the CLI all read this one key`,
773
744
  );
774
745
  // Every step below uses the DECLARED directory. A kept declaration outranks what init
775
- // measured or guessed: otherwise the first paper, the workflow and rpp.json would land in the
746
+ // measured or guessed: otherwise the first paper and the workflow would land in the
776
747
  // guessed directory while lint and the hooks keep reading the declared one.
777
748
  const papersDir =
778
749
  decl.status === "kept" ? (decl.papers as string) : choice.papers;
779
- const rpp = syncRppJson(root, papersDir);
780
- if (rpp === "filled")
781
- log(
782
- ` ⚠ rpp.json was already here — gave it the same ${PAPERS_DIR_FIELD} value; it is deprecated`,
783
- );
784
- else if (rpp === "kept")
785
- log(
786
- ` ⚠ rpp.json was already here and already declares ${PAPERS_DIR_FIELD} — left untouched; it is deprecated`,
787
- );
788
- else if (rpp === "unparsable")
789
- log(
790
- ` ⚠ rpp.json is here and does not parse — left untouched; it is deprecated, delete it`,
791
- );
792
750
 
793
751
  // ── 3. the skills, linked where Claude Code looks for them ─────────────────────────────
794
752
  for (const line of reportSkillLinks(link(root), here)) log(line);
@@ -112,7 +112,7 @@ const undefinedCs = [
112
112
  "Here is how much of TeX's memory you used:",
113
113
  ];
114
114
  const ex = errorExcerpt(undefinedCs);
115
- // Guards: the spelling rpp's own flags produce for most errors — with only the `!` form recognised,
115
+ // Guards: the spelling paperlint's own flags produce for most errors — with only the `!` form recognised,
116
116
  // an undefined control sequence fails with no error line quoted.
117
117
  check(
118
118
  "-file-line-error form: the error line, the l.NNN line and the rest of the source line",
@@ -50,7 +50,9 @@ const check = (label, cond) => {
50
50
  };
51
51
 
52
52
  const SKILLS = ["alpha", "beta", "gamma"];
53
- const work = realpathSync(mkdtempSync(join(tmpdir(), "rpp-link-skills-")));
53
+ const work = realpathSync(
54
+ mkdtempSync(join(tmpdir(), "paperlint-link-skills-")),
55
+ );
54
56
 
55
57
  /** A package as a manager would unpack it: manifest, skills, one non-skill. */
56
58
  function writePackage(dir) {
@@ -27,7 +27,7 @@ process.exit(
27
27
  runner: "node",
28
28
  cases: [
29
29
  {
30
- // The definition lives in the port since rpp#62 (`installedSkills` in consumer.mjs), so
30
+ // The definition lives in the port since paperlint#62 (`installedSkills` in consumer.mjs), so
31
31
  // the defect is planted THERE and must still die in THIS harness — the evidence that the
32
32
  // linker reads it through the shared function rather than through a copy of its own.
33
33
  name: "any directory counts as a skill",
@@ -66,7 +66,7 @@ export function locatePackage(project: string): Located {
66
66
  let manifest: string;
67
67
  try {
68
68
  const req = createRequire(
69
- pathToFileURL(join(project, "__rpp_locate__.js")).href,
69
+ pathToFileURL(join(project, "__paperlint_locate__.js")).href,
70
70
  );
71
71
  manifest = req.resolve(`${PACKAGE_NAME}/package.json`);
72
72
  } catch (e) {
@@ -100,7 +100,7 @@ export function shippedSkills(
100
100
  // clean run.
101
101
  if (!existsSync(skillsDir))
102
102
  return { error: `the package's skills directory is missing: ${skillsDir}` };
103
- // The same function every reader of an installed skills directory calls (rpp#62): the writer
103
+ // The same function every reader of an installed skills directory calls (paperlint#62): the writer
104
104
  // and the readers of this fact share one definition of "a skill is here", links included.
105
105
  try {
106
106
  return { skillsDir, names: installedSkills(skillsDir) };
@@ -52,7 +52,7 @@ const check = (label, cond) => {
52
52
  assert.ok(cond, label);
53
53
  };
54
54
 
55
- const work = realpathSync(mkdtempSync(join(tmpdir(), "rpp-new-")));
55
+ const work = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-new-")));
56
56
  const read = (...p) => readFileSync(join(...p), "utf8");
57
57
  const front = (text) => load(frontmatterBlock(text) ?? "") ?? {};
58
58
 
@@ -116,7 +116,7 @@ const named = (facts, name) =>
116
116
  JSON.stringify(r),
117
117
  );
118
118
  }
119
- const tmp = realpathSync(mkdtempSync(join(tmpdir(), "rpp-pdf-facts-")));
119
+ const tmp = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-pdf-facts-")));
120
120
  try {
121
121
  const notPdf = join(tmp, "paper.pdf");
122
122
  writeFileSync(notPdf, "%PDF-stale\n");
@@ -9,7 +9,7 @@
9
9
  * "rules": [ { "files": ["papers/agenticdev-2026/**"],
10
10
  * "rules": { "pdf/last-page-balance": ["error", { "tolerancePt": 120 }] } } ]
11
11
  *
12
- * The blocks are appended AFTER rpp's built-in config, so a later block wins, exactly as in ESLint.
12
+ * The blocks are appended AFTER paperlint's built-in config, so a later block wins, exactly as in ESLint.
13
13
  * `files` and `ignores` are resolved relative to the directory of the file that holds the settings,
14
14
  * as ESLint resolves them relative to its config file: each block gets that directory as ESLint's
15
15
  * own `basePath`, so paperlint matches nothing itself.
@@ -159,7 +159,7 @@ export function parseRuleBlocks(
159
159
 
160
160
  /**
161
161
  * The rule ids a config defines through its own plugins, as `<plugin>/<rule>`. Plugins passed in
162
- * `foreign` (a dependency's plugin, such as `@eslint/markdown`) are not rpp's and are skipped.
162
+ * `foreign` (a dependency's plugin, such as `@eslint/markdown`) are not paperlint's and are skipped.
163
163
  */
164
164
  export function shippedRuleIds(
165
165
  config: readonly unknown[],
@@ -28,7 +28,7 @@ const check = (label, cond) => {
28
28
  n++;
29
29
  };
30
30
 
31
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-struct-")));
31
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-struct-")));
32
32
  const papers = join(root, "papers");
33
33
  const paper = (name, files) => {
34
34
  const dir = join(papers, name);
@@ -116,7 +116,7 @@ for (const [t, vs] of byTemplate)
116
116
  );
117
117
 
118
118
  // ── 2. the schema rejects ───────────────────────────────────────────────────────────────
119
- const tmp = realpathSync(mkdtempSync(join(tmpdir(), "rpp-texreq-h-")));
119
+ const tmp = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-texreq-h-")));
120
120
  copyFileSync(join(VENUES, R.SCHEMA_FILE), join(tmp, R.SCHEMA_FILE));
121
121
  const bad = (text) => throws(() => R.parseProfile(text, "bad.jsonc", tmp));
122
122
  const BAD = [
@@ -4,7 +4,7 @@
4
4
  * 🔴 NOT ONE BYTE OF TeX LIVE IS DOWNLOADED HERE. The mirror is a directory served by `file://`
5
5
  * URLs: the real `curl` fetches from it, the real `tar` lists and unpacks it, and the installer
6
6
  * inside is `fixtures/toolchain-mirror/install-tl`, which lays out a tree whose `kpsewhich` and
7
- * `tlmgr` answer from two text files. So what runs is rpp's own download, fallback, unpack,
7
+ * `tlmgr` answer from two text files. So what runs is paperlint's own download, fallback, unpack,
8
8
  * install and verify logic — the part that decides success — and what is faked is only TeX.
9
9
  * The real TeX Live half is `test/e2e/toolchain.mjs`.
10
10
  *
@@ -78,22 +78,25 @@ check(
78
78
  ) === '["urw-base35"]',
79
79
  );
80
80
  check(
81
- "cacheRoot: RPP_TEXLIVE_DIR wins",
82
- T.cacheRoot({ RPP_TEXLIVE_DIR: "/ci/tl", XDG_CACHE_HOME: "/x" }, "/h") ===
83
- "/ci/tl",
81
+ "cacheRoot: PAPERLINT_TEXLIVE_DIR wins",
82
+ T.cacheRoot(
83
+ { PAPERLINT_TEXLIVE_DIR: "/ci/tl", XDG_CACHE_HOME: "/x" },
84
+ "/h",
85
+ ) === "/ci/tl",
84
86
  );
85
87
  check(
86
88
  "cacheRoot: then XDG_CACHE_HOME",
87
- T.cacheRoot({ XDG_CACHE_HOME: "/x" }, "/h") === "/x/rpp/texlive",
89
+ T.cacheRoot({ XDG_CACHE_HOME: "/x" }, "/h") === "/x/paperlint/texlive",
88
90
  );
89
91
  check(
90
92
  "cacheRoot: then ~/.cache",
91
- T.cacheRoot({}, "/h") === "/h/.cache/rpp/texlive",
93
+ T.cacheRoot({}, "/h") === "/h/.cache/paperlint/texlive",
92
94
  );
93
95
  check(
94
- "mirrorsFrom: RPP_CTAN_MIRROR is the ONLY mirror, trailing slash dropped",
95
- JSON.stringify(T.mirrorsFrom({ RPP_CTAN_MIRROR: "https://m/tlnet/" })) ===
96
- '["https://m/tlnet"]',
96
+ "mirrorsFrom: PAPERLINT_CTAN_MIRROR is the ONLY mirror, trailing slash dropped",
97
+ JSON.stringify(
98
+ T.mirrorsFrom({ PAPERLINT_CTAN_MIRROR: "https://m/tlnet/" }),
99
+ ) === '["https://m/tlnet"]',
97
100
  );
98
101
  check(
99
102
  "mirrorsFrom: several defaults, every one https",
@@ -146,7 +149,9 @@ check(
146
149
  check("usableTree: an empty cache", T.usableTree([], () => true) === null);
147
150
 
148
151
  // ── the fake mirror ────────────────────────────────────────────────────────────────────
149
- const work = realpathSync(mkdtempSync(join(tmpdir(), "rpp-toolchain-h-")));
152
+ const work = realpathSync(
153
+ mkdtempSync(join(tmpdir(), "paperlint-toolchain-h-")),
154
+ );
150
155
  const good = join(work, "good");
151
156
  const garbage = join(work, "garbage");
152
157
  const staging = join(work, "staging", "install-tl-20260924");
@@ -259,14 +264,14 @@ const TEX = {
259
264
  tools: { texcount: ["texcount"] },
260
265
  };
261
266
  const root = join(work, "cache");
262
- // 🔴 banal goes into the work directory, never the developer's cache: without RPP_BANAL_DIR the
263
- // command installs into ~/.cache/rpp/banal, and a harness must not leave files in a home.
267
+ // 🔴 banal goes into the work directory, never the developer's cache: without PAPERLINT_BANAL_DIR the
268
+ // command installs into ~/.cache/paperlint/banal, and a harness must not leave files in a home.
264
269
  const banalDir = join(work, "banal");
265
270
  const env = {
266
271
  ...process.env,
267
- RPP_TEXLIVE_DIR: root,
268
- RPP_CTAN_MIRROR: url(good),
269
- RPP_BANAL_DIR: banalDir,
272
+ PAPERLINT_TEXLIVE_DIR: root,
273
+ PAPERLINT_CTAN_MIRROR: url(good),
274
+ PAPERLINT_BANAL_DIR: banalDir,
270
275
  };
271
276
  // A stand-in banal: a Perl script that prints what banal prints for a one-page, 10 pt probe.
272
277
  const fakeBanal = join(work, "fake-banal");
@@ -446,7 +451,7 @@ const cmd = ({ banal = BANAL, ...over } = {}) => {
446
451
  const r = cmd({
447
452
  env: {
448
453
  ...env,
449
- RPP_TEXLIVE_DIR: fresh,
454
+ PAPERLINT_TEXLIVE_DIR: fresh,
450
455
  FAKE_INSTALL_TL_FAIL: "1",
451
456
  },
452
457
  });
@@ -462,15 +467,15 @@ const cmd = ({ banal = BANAL, ...over } = {}) => {
462
467
  const r = cmd({
463
468
  env: {
464
469
  ...env,
465
- RPP_TEXLIVE_DIR: join(work, "cache-nomirror"),
466
- RPP_CTAN_MIRROR: MISSING,
470
+ PAPERLINT_TEXLIVE_DIR: join(work, "cache-nomirror"),
471
+ PAPERLINT_CTAN_MIRROR: MISSING,
467
472
  },
468
473
  });
469
474
  check(
470
475
  "no mirror answers: exit 1, names the mirrors and the override",
471
476
  r.code === 1 &&
472
477
  r.err.includes("no CTAN mirror returned install-tl") &&
473
- r.err.includes("RPP_CTAN_MIRROR"),
478
+ r.err.includes("PAPERLINT_CTAN_MIRROR"),
474
479
  r.err,
475
480
  );
476
481
  }
@@ -520,7 +525,11 @@ const withLibertine = {
520
525
  };
521
526
  const inYears = (mirror, over = {}) =>
522
527
  cmd({
523
- env: { ...env, RPP_TEXLIVE_DIR: years, RPP_CTAN_MIRROR: mirror },
528
+ env: {
529
+ ...env,
530
+ PAPERLINT_TEXLIVE_DIR: years,
531
+ PAPERLINT_CTAN_MIRROR: mirror,
532
+ },
524
533
  ...over,
525
534
  });
526
535
  check(
@@ -583,12 +592,12 @@ check(
583
592
  {
584
593
  // tlmgr says 2027, but the installer the mirrors hand out is still 2026.
585
594
  const split = join(work, "cache-split");
586
- cmd({ env: { ...env, RPP_TEXLIVE_DIR: split } });
595
+ cmd({ env: { ...env, PAPERLINT_TEXLIVE_DIR: split } });
587
596
  const r = cmd({
588
597
  env: {
589
598
  ...env,
590
- RPP_TEXLIVE_DIR: split,
591
- RPP_CTAN_MIRROR: mirrorFor("split2027", "2027", "2026"),
599
+ PAPERLINT_TEXLIVE_DIR: split,
600
+ PAPERLINT_CTAN_MIRROR: mirrorFor("split2027", "2027", "2026"),
592
601
  },
593
602
  tex: withLibertine,
594
603
  });
@@ -606,7 +615,7 @@ check(
606
615
  // ── 4. banal, the second half of the command ───────────────────────────────────────────
607
616
  {
608
617
  const dir = join(work, "banal-4");
609
- const e = { ...env, RPP_BANAL_DIR: dir };
618
+ const e = { ...env, PAPERLINT_BANAL_DIR: dir };
610
619
  const before = cmd({ env: e, check: true });
611
620
  // Guards: --check covers banal — a complete TeX Live alone must not read as a ready toolchain.
612
621
  check(
@@ -633,7 +642,7 @@ check(
633
642
  }
634
643
  {
635
644
  const r = cmd({
636
- env: { ...env, RPP_BANAL_DIR: join(work, "banal-bad") },
645
+ env: { ...env, PAPERLINT_BANAL_DIR: join(work, "banal-bad") },
637
646
  banal: { ...BANAL, sha256: "0".repeat(64) },
638
647
  });
639
648
  // Guards: the two halves are independent — a banal failure fails the command, and TeX Live still
package/src/toolchain.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `paperlint toolchain` — install upstream TeX Live, with exactly the packages the venue profiles
3
- * declare, into rpp's own cache, and banal (the page-geometry script HotCRP runs) at its pinned
3
+ * declare, into paperlint's own cache, and banal (the page-geometry script HotCRP runs) at its pinned
4
4
  * commit (`adapters/banal/`). `paperlint build` offers the TeX Live install on a terminal.
5
5
  *
6
6
  * 🔴 WHY paperlint INSTALLS TeX AT ALL (rule 11). "Install TeX Live yourself" was a manual step with a
@@ -19,8 +19,8 @@
19
19
  * once reported success having installed nothing: installers' exit codes are not evidence.)
20
20
  * A second run with everything present does nothing and says so; `--check` only reports.
21
21
  *
22
- * Where: `$RPP_TEXLIVE_DIR` when set (CI points its cache there), else
23
- * `$XDG_CACHE_HOME/rpp/texlive`, else `~/.cache/rpp/texlive` — one tree per TeX Live year inside.
22
+ * Where: `$PAPERLINT_TEXLIVE_DIR` when set (CI points its cache there), else
23
+ * `$XDG_CACHE_HOME/paperlint/texlive`, else `~/.cache/paperlint/texlive` — one tree per TeX Live year inside.
24
24
  * Processes run through the injected `run` (the port `build.ts` uses), so the harness drives the
25
25
  * real download/unpack/verify logic against a fake mirror on disk, never the network.
26
26
  */
@@ -56,8 +56,8 @@ import {
56
56
  type TexRequirements,
57
57
  } from "./tex-requirements.ts";
58
58
 
59
- export const CACHE_ENV = "RPP_TEXLIVE_DIR";
60
- export const MIRROR_ENV = "RPP_CTAN_MIRROR";
59
+ export const CACHE_ENV = "PAPERLINT_TEXLIVE_DIR";
60
+ export const MIRROR_ENV = "PAPERLINT_CTAN_MIRROR";
61
61
 
62
62
  /**
63
63
  * The CTAN mirrors, redirector first. 🔴 SEVERAL, AND THAT IS A MEASUREMENT: run 33650803245 died
@@ -99,7 +99,11 @@ export function cacheRoot(
99
99
  home: string = homedir(),
100
100
  ): string {
101
101
  if (env[CACHE_ENV]) return env[CACHE_ENV];
102
- return join(env["XDG_CACHE_HOME"] || join(home, ".cache"), "rpp", "texlive");
102
+ return join(
103
+ env["XDG_CACHE_HOME"] || join(home, ".cache"),
104
+ "paperlint",
105
+ "texlive",
106
+ );
103
107
  }
104
108
 
105
109
  /**
@@ -353,7 +357,9 @@ export function installBase(
353
357
  mirrors: readonly string[],
354
358
  newerThan: string | null = null,
355
359
  ): Step<{ tree: CachedTree; mirror: string }> {
356
- const work = realpathSync(mkdtempSync(join(tmpdir(), "rpp-install-tl-")));
360
+ const work = realpathSync(
361
+ mkdtempSync(join(tmpdir(), "paperlint-install-tl-")),
362
+ );
357
363
  try {
358
364
  io.log(` downloading install-tl…`);
359
365
  const mirror = downloadInstaller(
@@ -409,7 +415,7 @@ function runInstallTl(
409
415
  ): Step<{ tree: CachedTree; mirror: string }> {
410
416
  const dir = join(a.root, a.year);
411
417
  mkdirSync(dir, { recursive: true });
412
- const profile = join(a.work, "rpp.profile");
418
+ const profile = join(a.work, "paperlint.profile");
413
419
  writeFileSync(profile, tlProfile(dir));
414
420
  io.log(
415
421
  ` install-tl: TeX Live ${a.year} scheme-basic from ${a.mirror} (~150 MB, about a minute)…`,
package/src/types.ts CHANGED
@@ -5,8 +5,8 @@ import type { RuleBlock } from "./rules-config.ts";
5
5
  * in someone's head: a typo in a config key read as "field not set", not as an error.
6
6
  */
7
7
 
8
- /** The contents of the consumer's `rpp.json`. */
9
- export interface RppConfig {
8
+ /** The consumer's settings — the `paperlint` key of its `package.json`. */
9
+ export interface PaperlintConfig {
10
10
  /** 🔴 REQUIRED. The paper director(ies), relative to the config file ITSELF. The field name is
11
11
  * `PAPERS_DIR_FIELD` in lib/paper-config.mjs; code reads it through `papersDirOf()` in cli.ts. */
12
12
  papersDir?: string | string[];
@@ -26,7 +26,7 @@ export interface RppConfig {
26
26
  structure?: StructureConfig | false;
27
27
  /** REMOVED — `paperlint build` compiles the paper itself. Still typed so a leftover key can be named. */
28
28
  buildScripts?: unknown;
29
- /** ESLint config blocks appended after rpp's own — PARSED by `parseSettings` in cli.ts. */
29
+ /** ESLint config blocks appended after paperlint's own — PARSED by `parseSettings` in cli.ts. */
30
30
  rules?: readonly RuleBlock[];
31
31
  }
32
32
 
@@ -102,5 +102,5 @@ export interface BuildResult {
102
102
 
103
103
  /** The result of reading the config: either data, or the exit code the caller exits with. */
104
104
  export type ConfigRead =
105
- | { opts: RppConfig; configPath: string | null; code?: undefined }
105
+ | { opts: PaperlintConfig; configPath: string | null; code?: undefined }
106
106
  | { code: number; opts?: undefined; configPath?: undefined };