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
package/src/cli.ts CHANGED
@@ -10,13 +10,13 @@
10
10
  *
11
11
  * Two entry points into the tool, and both are whole now:
12
12
  * npx paperlint lint ← here
13
- * uses: zernie/research-paper-pipeline@<sha> ← action.yml
13
+ * uses: zernie/paperlint@<sha> ← action.yml
14
14
  *
15
15
  * ⚠️ THE BOUNDARY THIS UTILITY HAS NO RIGHT TO ERASE: the consumer's data stays with the consumer.
16
16
  * The typography debt, the marker of the author-list check run, the field dictionary — all of that
17
17
  * is about ONE corpus, and wiring it into the package would repeat the defect that put the path
18
18
  * `.claude/skills/verify-citations/...` into a rule's message. So they live in the consumer's
19
- * `rpp.json`.
19
+ * `package.json`, under the `paperlint` key.
20
20
  *
21
21
  * 🔴 WHY THE COMMAND IS CALLED `lint` AND NOT `check`. It does exactly what everyone else calls by
22
22
  * that word: reads files, changes nothing, prints findings, exits non-zero. `check` is taken in the
@@ -35,7 +35,7 @@ import markdown from "@eslint/markdown";
35
35
  // Types come from consumer.d.mts beside it, the same arrangement as lib/paper-config.d.mts.
36
36
  import { isMain } from "../skills/paper-pipeline/scripts/consumer.mjs";
37
37
  export { isMain };
38
- import type { Args, RppConfig, ConfigRead } from "./types.ts";
38
+ import type { Args, PaperlintConfig, ConfigRead } from "./types.ts";
39
39
  import {
40
40
  checkStructure,
41
41
  formatStructure,
@@ -123,7 +123,7 @@ const USAGE = `paperlint — machine-checkable gates for a paper kept in git
123
123
  package the venue declares; on a terminal it offers to install
124
124
  one, without a terminal it stops and names \`npx paperlint toolchain\`
125
125
  npx paperlint toolchain [--check] install TeX Live with every package the venue profiles declare
126
- into ~/.cache/rpp/texlive (RPP_TEXLIVE_DIR overrides); a second
126
+ into ~/.cache/paperlint/texlive (PAPERLINT_TEXLIVE_DIR overrides); a second
127
127
  run does nothing. --check: report what is missing, change nothing
128
128
  npx paperlint doctor say what is actually wired — and what only LOOKS wired
129
129
  npx paperlint hook <name> run an editor hook (.claude/settings.json calls this)
@@ -149,8 +149,8 @@ lint:
149
149
  most findings here are advisory and a gate that fails on advice gets muted
150
150
 
151
151
  settings — the \`paperlint\` key of your package.json, found by walking up from the
152
- current directory, the way every other tool in the stack finds its config. \`rpp.json\` is still
153
- read as a deprecated fallback and the run says so. \`papersDir\` is required; the rest is optional:
152
+ current directory, the way every other tool in the stack finds its config. \`--config\` names
153
+ another file of the same shape. \`papersDir\` is required; the rest is optional:
154
154
 
155
155
  "paperlint": {
156
156
  "papersDir": "papers",
@@ -164,14 +164,14 @@ read as a deprecated fallback and the run says so. \`papersDir\` is required; th
164
164
  "rules": { "pdf/last-page-balance": "error" } } ]
165
165
  }
166
166
 
167
- "rules" takes ESLint flat-config blocks (files, ignores, rules), appended after rpp's own, with
167
+ "rules" takes ESLint flat-config blocks (files, ignores, rules), appended after paperlint's own, with
168
168
  files relative to the file holding the settings. Optional rules (off unless turned on there):
169
169
  pdf/last-page-balance. An unknown key, anywhere in the settings, is an error.
170
170
  `;
171
171
 
172
172
  /** The config the user would otherwise write by hand. The data comes from `opts`, the mechanism is here. */
173
173
  export function buildConfig(
174
- opts: RppConfig = {},
174
+ opts: PaperlintConfig = {},
175
175
  texLanguage: unknown,
176
176
  ): unknown[] {
177
177
  const paperRules = { ...researchQuestion.rules, ...typography.rules };
@@ -272,8 +272,8 @@ export function buildConfig(
272
272
  }
273
273
 
274
274
  /**
275
- * The rule ids a consumer may name in `rules`: every rule rpp's own config defines, read off that
276
- * config rather than listed again. `@eslint/markdown` is a dependency's plugin, not rpp's.
275
+ * The rule ids a consumer may name in `rules`: every rule paperlint's own config defines, read off that
276
+ * config rather than listed again. `@eslint/markdown` is a dependency's plugin, not paperlint's.
277
277
  */
278
278
  export const SHIPPED_RULES: ReadonlySet<string> = shippedRuleIds(
279
279
  buildConfig({}, { sentinel: "tex language" }),
@@ -288,7 +288,7 @@ const isOn = (entry: unknown): boolean => {
288
288
 
289
289
  /**
290
290
  * Rules paperlint ships and turns on for no file itself — the ones a consumer opts into with `rules`.
291
- * Derived: every shipped rule that no block of rpp's own config names.
291
+ * Derived: every shipped rule that no block of paperlint's own config names.
292
292
  */
293
293
  export const OPTIONAL_RULES: ReadonlySet<string> = new Set(
294
294
  [...SHIPPED_RULES].filter(
@@ -309,7 +309,7 @@ export const OPTIONAL_RULES: ReadonlySet<string> = new Set(
309
309
  export async function silentOptionalRules(
310
310
  eslint: ESLint,
311
311
  lintedFiles: readonly string[],
312
- opts: RppConfig,
312
+ opts: PaperlintConfig,
313
313
  ): Promise<string[]> {
314
314
  const turnedOn = new Set(
315
315
  (opts.rules ?? []).flatMap((b) =>
@@ -333,10 +333,10 @@ export async function silentOptionalRules(
333
333
  * config blocks. Nothing after this sees the raw object.
334
334
  */
335
335
  export function parseSettings(
336
- opts: RppConfig,
336
+ opts: PaperlintConfig,
337
337
  where: string,
338
338
  baseDir: string,
339
- ): Parsed<RppConfig> {
339
+ ): Parsed<PaperlintConfig> {
340
340
  const raw = opts as Record<string, unknown>;
341
341
  const unknown = unknownKeys(raw);
342
342
  if (unknown.length > 0)
@@ -431,7 +431,6 @@ export function parseArgs(argv: readonly string[]): Args {
431
431
  return out;
432
432
  }
433
433
 
434
- export const CONFIG_NAME = "rpp.json";
435
434
  export const PKG_NAME = "package.json";
436
435
 
437
436
  /**
@@ -450,36 +449,20 @@ function ownVersion(): string | undefined {
450
449
  }
451
450
  }
452
451
 
453
- /** Where the consumer's settings were found, and in which of the two carriers. */
454
- export interface Declaration {
455
- readonly path: string;
456
- readonly kind: "package.json" | "rpp.json";
457
- }
458
-
459
452
  /**
460
- * 🔴 THE CLI HAD TO LEARN TO READ `package.json`, AND THAT IS NOT A SIDE ERRAND. `paperlint init` now
461
- * writes ONE declaration, into the `package.json` key that the three hooks and `eslint-rules`
462
- * already read. Without this walker the install it produces would not work at all: `paperlint lint`
463
- * would find no `rpp.json`, report "nothing to lint", and the consumer would be back to
464
- * declaring the same directory twice — the defect the single declaration removes (issue #33,
453
+ * 🔴 THE CLI READS `package.json`, THE ONE DECLARATION. `paperlint init` writes the settings under
454
+ * the `package.json` key that the three hooks and `eslint-rules` already read; the CLI reads the
455
+ * same file, so the install and the check cannot look at different files (issue #33,
465
456
  * `docs/install.md`).
466
457
  *
467
- * `rpp.json` stays readable as a DEPRECATED fallback, and the read says so out loud. Silently
468
- * dropping a file this command used to write would break working setups on upgrade.
469
- *
470
458
  * The walk goes up to the filesystem root, the way eslint, prettier and tsc find theirs, so a run
471
- * from inside one paper sees the same settings as a run from the repository root. At each level
472
- * `package.json` wins over `rpp.json`: it is the carrier every other reader uses, so preferring
473
- * it is what keeps "one declaration" true rather than merely intended.
459
+ * from inside one paper sees the same settings as a run from the repository root.
474
460
  */
475
- export function findDeclaration(startDir: string): Declaration | null {
461
+ export function findConfig(startDir: string): string | null {
476
462
  let dir = resolve(startDir);
477
463
  for (;;) {
478
464
  const pkg = join(dir, PKG_NAME);
479
- if (existsSync(pkg) && declaresSettings(pkg))
480
- return { path: pkg, kind: "package.json" };
481
- const rpp = join(dir, CONFIG_NAME);
482
- if (existsSync(rpp)) return { path: rpp, kind: "rpp.json" };
465
+ if (existsSync(pkg) && declaresSettings(pkg)) return pkg;
483
466
  const up = dirname(dir);
484
467
  if (up === dir) return null;
485
468
  dir = up;
@@ -500,11 +483,6 @@ const declaresSettings = (pkgPath: string): boolean => {
500
483
  }
501
484
  };
502
485
 
503
- /** Kept as the one-line question "which file holds the settings" — callers that only need a path. */
504
- export function findConfig(startDir: string): string | null {
505
- return findDeclaration(startDir)?.path ?? null;
506
- }
507
-
508
486
  /**
509
487
  * Reading the config, ONE reader for all commands. Pulled out of `run()` the moment a second
510
488
  * command needed the same config (`build`): two copies of this block would have drifted apart on
@@ -525,24 +503,17 @@ export function readConfig(
525
503
  } = {},
526
504
  ): ConfigRead {
527
505
  // 🔴 THE CONFIG FINDS ITSELF. An explicit `--config` beats the discovered one — it was named out
528
- // loud, and a substitution is never silent. For an explicit path the FILE NAME decides the
529
- // carrier: the path here is a value, not a text to make guesses about, and `package.json` holds
530
- // the settings under a key.
531
- const decl: Declaration | null = a.config
532
- ? {
533
- path: a.config,
534
- kind: basename(a.config) === PKG_NAME ? "package.json" : "rpp.json",
535
- }
536
- : findDeclaration(cwd);
537
- const configPath = decl?.path ?? null;
506
+ // loud, and a substitution is never silent. Either way the file has the shape of a
507
+ // `package.json`: the settings sit under the key.
508
+ const configPath = a.config ?? findConfig(cwd);
538
509
  if (a.config && !existsSync(a.config)) {
539
510
  err(`config file not found: ${a.config}`);
540
511
  return { code: 2 };
541
512
  }
542
513
 
543
- let opts: RppConfig = {};
514
+ let opts: PaperlintConfig = {};
544
515
  let legacyKey = false;
545
- if (decl && configPath) {
516
+ if (configPath) {
546
517
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #49: replace with a real type
547
518
  let parsed: any;
548
519
  try {
@@ -551,15 +522,13 @@ export function readConfig(
551
522
  err(`${configPath} is not valid JSON: ${(e as Error).message}`);
552
523
  return { code: 2 };
553
524
  }
554
- if (decl.kind === "package.json") {
555
- const d = declaredSettings(parsed);
556
- if (d.conflict !== null) {
557
- err(d.conflict);
558
- return { code: 2 };
559
- }
560
- opts = (d.settings ?? {}) as RppConfig;
561
- legacyKey = d.legacy;
562
- } else opts = parsed;
525
+ const d = declaredSettings(parsed);
526
+ if (d.conflict !== null) {
527
+ err(d.conflict);
528
+ return { code: 2 };
529
+ }
530
+ opts = (d.settings ?? {}) as PaperlintConfig;
531
+ legacyKey = d.legacy;
563
532
  // The discovered config is NAMED out loud. Otherwise a run from someone else's directory picks
564
533
  // up someone else's file and does not say so — and a typography-debt mismatch looks like a finding.
565
534
  //
@@ -568,30 +537,19 @@ export function readConfig(
568
537
  // test but by an attempt to wire our own action to this output; in the harness I first WORKED
569
538
  // AROUND this line (stripped the first line before JSON.parse) — that is, the workaround hid
570
539
  // the defect exactly where it should have been shouting.
571
- (a.json ? err : log)(`config: ${relative(cwd, configPath) || CONFIG_NAME}`);
572
- // 🔴 THE DEPRECATED CARRIER IS NAMED OUT LOUD, IT DOES NOT STOP BEING READ. The hooks read ONLY
573
- // package.json, so a consumer whose settings stayed in rpp.json lints one directory and guards
574
- // another — and both states look equally green.
575
- if (decl.kind === "rpp.json")
576
- (a.json ? err : log)(
577
- ` ⚠ ${CONFIG_NAME} is deprecated — move these keys under "${CONFIG_KEY}" in ${PKG_NAME}; ` +
578
- `the hooks read only that file. \`npx paperlint init\` does it for you.`,
579
- );
540
+ (a.json ? err : log)(`config: ${relative(cwd, configPath) || PKG_NAME}`);
580
541
  if (legacyKey) (a.json ? err : log)(` ⚠ ${LEGACY_KEY_MESSAGE}`);
581
542
  }
582
543
 
583
544
  // The old field name is refused before anything else is read from the settings: falling back
584
545
  // to it would keep it working forever, and this package has no released users to migrate.
585
- const where =
586
- decl?.kind === "package.json"
587
- ? `${PKG_NAME} → "${legacyKey ? LEGACY_CONFIG_KEY : CONFIG_KEY}"`
588
- : (decl?.path ?? CONFIG_NAME);
589
- const renamed = decl ? renamedFieldMessage(opts, where) : null;
546
+ const where = `${configPath ? basename(configPath) : PKG_NAME} → "${legacyKey ? LEGACY_CONFIG_KEY : CONFIG_KEY}"`;
547
+ const renamed = configPath ? renamedFieldMessage(opts, where) : null;
590
548
  if (renamed) {
591
549
  err(renamed);
592
550
  return { code: 2 };
593
551
  }
594
- if (decl && configPath) {
552
+ if (configPath) {
595
553
  const parsed = parseSettings(
596
554
  opts,
597
555
  where,
@@ -607,15 +565,11 @@ export function readConfig(
607
565
  // 🔴 THE PAPERS DIRECTORY IS A REQUIRED FIELD. The papers directory is the one thing without which the tool
608
566
  // does not know what it works on, and the one thing that cannot be guessed: a default of "." runs
609
567
  // the rules over the whole checkout and exits green over a scope nobody chose.
610
- if (decl && !hasPapers(opts)) {
568
+ if (configPath && !hasPapers(opts)) {
611
569
  err(
612
- decl.kind === "package.json"
613
- ? `${decl.path} must declare \`${PAPERS_DIR_FIELD}\` — the directory your papers live in, e.g.\n` +
614
- ` { "${CONFIG_KEY}": { "${PAPERS_DIR_FIELD}": "papers" } }\n` +
615
- `It is the one thing this tool cannot guess. \`npx paperlint init\` writes it for you.`
616
- : `${decl.path} must declare \`${PAPERS_DIR_FIELD}\` — the directory your papers live in, e.g.\n` +
617
- ` { "${PAPERS_DIR_FIELD}": "papers" }\n` +
618
- `It is the one thing this tool cannot guess.`,
570
+ `${configPath} must declare \`${PAPERS_DIR_FIELD}\` — the directory your papers live in, e.g.\n` +
571
+ ` { "${CONFIG_KEY}": { "${PAPERS_DIR_FIELD}": "papers" } }\n` +
572
+ `It is the one thing this tool cannot guess. \`npx paperlint init\` writes it for you.`,
619
573
  );
620
574
  return { code: 2 };
621
575
  }
@@ -623,7 +577,7 @@ export function readConfig(
623
577
  }
624
578
 
625
579
  /** The papers directory field of the settings, read by its one declared name. */
626
- export function papersDirOf(opts: RppConfig): unknown {
580
+ export function papersDirOf(opts: PaperlintConfig): unknown {
627
581
  return (opts as Record<string, unknown>)[PAPERS_DIR_FIELD];
628
582
  }
629
583
 
@@ -635,7 +589,7 @@ export function toPaths(papers: unknown): string[] {
635
589
  return [];
636
590
  }
637
591
 
638
- const hasPapers = (opts: RppConfig): boolean =>
592
+ const hasPapers = (opts: PaperlintConfig): boolean =>
639
593
  toPaths(papersDirOf(opts)).length > 0;
640
594
 
641
595
  /**
@@ -703,7 +657,7 @@ export function runHook(
703
657
  throw new Error(`resolved vigiles, but no cli.js beside it: ${runtime}`);
704
658
  } catch {
705
659
  err(
706
- `rpp: the hook runtime (vigiles) is not resolvable from ${fileURLToPath(new URL(".", import.meta.url))}.\n` +
660
+ `paperlint: the hook runtime (vigiles) is not resolvable from ${fileURLToPath(new URL(".", import.meta.url))}.\n` +
707
661
  `The \`${name}\` hook is NOT running. Everything else — \`paperlint lint\`, CI — is unaffected.\n` +
708
662
  `Reinstall this package so its dependencies are present.`,
709
663
  );
@@ -1048,7 +1002,7 @@ export async function run(
1048
1002
  );
1049
1003
  if (paths.length === 0) {
1050
1004
  err(
1051
- `nothing to lint: no path was given and no ${CONFIG_NAME} was found.\n` +
1005
+ `nothing to lint: no path was given and no "${CONFIG_KEY}" key was found in a ${PKG_NAME}.\n` +
1052
1006
  `Run \`npx paperlint init\` here, or pass the directory: \`paperlint lint papers\`.`,
1053
1007
  );
1054
1008
  return 2;
@@ -1133,7 +1087,7 @@ async function reportLint(
1133
1087
  log: typeof console.log;
1134
1088
  err: typeof console.error;
1135
1089
  where: string;
1136
- opts: RppConfig;
1090
+ opts: PaperlintConfig;
1137
1091
  },
1138
1092
  ): Promise<number> {
1139
1093
  const silent = await silentOptionalRules(
@@ -1180,7 +1134,7 @@ async function reportLint(
1180
1134
 
1181
1135
  // 🔴 `isMain`, NOT A STRING COMPARISON. The first version wrote
1182
1136
  // if (import.meta.url === `file://${process.argv[1]}`)
1183
- // and the utility, launched via `node_modules/.bin/rpp`, SILENTLY EXITED WITH ZERO: npm puts a
1137
+ // and the utility, launched via `node_modules/.bin/paperlint`, SILENTLY EXITED WITH ZERO: npm puts a
1184
1138
  // SYMLINK there, `process.argv[1]` stays the symlink's path while `import.meta.url` is the real
1185
1139
  // path, and the condition is false. That is, the only way a real consumer launches the utility did
1186
1140
  // not work at all — and it looked like a clean run.
@@ -41,9 +41,9 @@ const check = (label, cond) => {
41
41
  assert.ok(cond, label);
42
42
  };
43
43
 
44
- /** A consumer on disk: a papers directory, declarations in one or both places. */
45
- function consumer({ papersDir, pkgKey, rppJson, makeDir = true }) {
46
- const dir = realpathSync(mkdtempSync(join(tmpdir(), "rpp-doctor-")));
44
+ /** A consumer on disk: a papers directory, and maybe a declaration in package.json. */
45
+ function consumer({ papersDir, pkgKey, makeDir = true }) {
46
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-doctor-")));
47
47
  if (makeDir && papersDir) {
48
48
  mkdirSync(join(dir, papersDir, "some-paper"), { recursive: true });
49
49
  writeFileSync(
@@ -54,11 +54,6 @@ function consumer({ papersDir, pkgKey, rppJson, makeDir = true }) {
54
54
  const pkg = { name: "consumer", version: "1.0.0" };
55
55
  if (pkgKey !== undefined) pkg["paperlint"] = { [PAPERS_DIR_FIELD]: pkgKey };
56
56
  writeFileSync(join(dir, "package.json"), JSON.stringify(pkg, null, 2));
57
- if (rppJson !== undefined)
58
- writeFileSync(
59
- join(dir, "rpp.json"),
60
- JSON.stringify({ [PAPERS_DIR_FIELD]: rppJson }, null, 2),
61
- );
62
57
  return dir;
63
58
  }
64
59
 
@@ -113,13 +108,10 @@ const runDoctor = (
113
108
  }
114
109
 
115
110
  // ── II. THE VERY DEFECT: AN INSTALL FOLLOWING THE DOCS ─────────────────────────────────────
116
- // `paperlint init` writes rpp.json and does not touch package.json; the hook reads package.json.
111
+ // The old `init` wrote its own config file and did not touch package.json; the hook reads package.json.
117
112
  // Measured 09-18.
118
113
  {
119
- const dir = consumer({
120
- papersDir: "writing/drafts",
121
- rppJson: "writing/drafts",
122
- });
114
+ const dir = consumer({ papersDir: "writing/drafts" });
123
115
  const r = runDoctor(dir, { cliPapers: "writing/drafts" });
124
116
  check(
125
117
  "an install that follows the docs — a FAILURE, not a cheerful report",
@@ -150,7 +142,7 @@ const runDoctor = (
150
142
  // It must not fail here (an error-level false positive costs more than a miss), but it must
151
143
  // not stay silent either.
152
144
  {
153
- const dir = consumer({ papersDir: "papers", rppJson: "papers" });
145
+ const dir = consumer({ papersDir: "papers" });
154
146
  const r = runDoctor(dir, { cliPapers: "papers" });
155
147
  check("an install that works by coincidence does NOT crash", r.code === 0);
156
148
  check(
@@ -220,18 +212,10 @@ const runDoctor = (
220
212
 
221
213
  // ── III. TWO DECLARATIONS HAVE DRIFTED APART ────────────────────────────────────────────────
222
214
  {
223
- const dir = consumer({
224
- papersDir: "writing/drafts",
225
- pkgKey: "papers",
226
- rppJson: "writing/drafts",
227
- });
215
+ const dir = consumer({ papersDir: "writing/drafts", pkgKey: "papers" });
228
216
  mkdirSync(join(dir, "papers"), { recursive: true });
229
217
  const r = runDoctor(dir, { cliPapers: "writing/drafts" });
230
218
  check("both declarations exist, but differ — a FAILURE", r.code === 2);
231
- check(
232
- "the stale rpp.json is called out ⚠",
233
- /rpp\.json is present/.test(r.out),
234
- );
235
219
  rmSync(dir, { recursive: true, force: true });
236
220
  }
237
221
 
@@ -307,7 +291,7 @@ const runDoctor = (
307
291
 
308
292
  // ── VII. DETECTING THE PAPERS DIRECTORY ─────────────────────────────────────────────────────
309
293
  {
310
- const dir = realpathSync(mkdtempSync(join(tmpdir(), "rpp-detect-")));
294
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-detect-")));
311
295
  mkdirSync(join(dir, "writing", "drafts", "p1"), { recursive: true });
312
296
  writeFileSync(join(dir, "writing", "drafts", "p1", "paper.tex"), "x");
313
297
  mkdirSync(join(dir, "node_modules", "pkg", "papers", "p"), {
@@ -387,7 +371,7 @@ const runDoctor = (
387
371
  check("a program that really exists is found", found("node") === true);
388
372
  check(
389
373
  "a made-up one is not (otherwise the check answers the form, not the subject)",
390
- found("rpp-definitely-not-a-real-binary-xyz") === false,
374
+ found("paperlint-definitely-not-a-real-binary-xyz") === false,
391
375
  );
392
376
  }
393
377
 
package/src/doctor.ts CHANGED
@@ -282,8 +282,6 @@ export function doctor({
282
282
  out.push(...declaration.lines);
283
283
  bad += declaration.bad;
284
284
  }
285
- if (existsSync(join(root, "rpp.json")))
286
- out.push(` ⚠ rpp.json is present — deprecated; the hooks never read it`);
287
285
 
288
286
  out.push("", "papers directory");
289
287
  const hookRoot = rawPkg ? papersRoot(rawPkg) : null;
@@ -53,7 +53,7 @@ const facts = (o) => ({
53
53
  ...o,
54
54
  });
55
55
  const ROWS = [
56
- // Guards: the order — rpp's own verified cache first, so a build does not depend on whatever TeX
56
+ // Guards: the order — paperlint's own verified cache first, so a build does not depend on whatever TeX
57
57
  // Live the machine happens to carry.
58
58
  [
59
59
  "a complete cache wins, even over a complete system TeX",
@@ -198,7 +198,9 @@ check(
198
198
  // to start, while `paperlint toolchain --check` reported the tree verified. Checked on disk, not
199
199
  // through an injected predicate, because the predicate is exactly what was wrong.
200
200
  {
201
- const bin = realpathSync(mkdtempSync(join(tmpdir(), "rpp-engine-bin-")));
201
+ const bin = realpathSync(
202
+ mkdtempSync(join(tmpdir(), "paperlint-engine-bin-")),
203
+ );
202
204
  try {
203
205
  mkdirSync(join(bin, "texcount"));
204
206
  writeFileSync(join(bin, "checkcites"), "#!/bin/sh\n");
package/src/engine.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * TeX Live qualifies only when `kpsewhich` finds every file the venue declares (issue #26).
9
9
  *
10
10
  * The order (the Playwright pattern, decided 2026-09-24):
11
- * 1. rpp's own cache (`paperlint toolchain` installs it) — when it has everything;
11
+ * 1. paperlint's own cache (`paperlint toolchain` installs it) — when it has everything;
12
12
  * 2. a TeX Live already on PATH — when IT has everything;
13
13
  * 3. neither: on a terminal, ask once and install; without one (CI, an agent), refuse in one line
14
14
  * that names `npx paperlint toolchain` and the missing packages. Never a silent install without a
@@ -41,7 +41,7 @@ export interface TreeProbe {
41
41
  export interface EngineFacts {
42
42
  /** install-tl-unx runs here (Linux, macOS). */
43
43
  readonly supported: boolean;
44
- /** rpp's cached TeX Live, probed — or null when there is none. */
44
+ /** paperlint's cached TeX Live, probed — or null when there is none. */
45
45
  readonly cache: TreeProbe | null;
46
46
  /** The `pdflatex` on PATH, probed — or null when there is none. */
47
47
  readonly system: TreeProbe | null;
@@ -40,7 +40,6 @@ const {
40
40
  doctorHooks,
41
41
  readSettings,
42
42
  MANAGED_BY,
43
- LEGACY_MANAGED_BY,
44
43
  SETTINGS_PATH,
45
44
  } = await import(join(HERE, "hooks-settings.ts"));
46
45
  const { claudeCodeHookProtocol } = await import("vigiles/claude-code");
@@ -66,7 +65,7 @@ check(
66
65
  wiring.names.join() ===
67
66
  "paper-edit-guard,paper-skills-nudge,paper-status-gates",
68
67
  );
69
- const work = realpathSync(mkdtempSync(join(tmpdir(), "rpp-hooks-")));
68
+ const work = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-hooks-")));
70
69
  try {
71
70
  const empty = join(work, "empty-hooks.json");
72
71
  writeFileSync(empty, '{"hooks":{}}');
@@ -89,11 +88,16 @@ try {
89
88
  `node "$CLAUDE_PROJECT_DIR/${MANAGED_BY}" hook paper-status-gates`,
90
89
  { name: "paper-status-gates", ours: true, legacy: false },
91
90
  ],
92
- // Guards: what an install under the old package name wrote is recognised as ours-but-stale.
91
+ // Guards: what an older install wrote is recognised as ours-but-stale. Spelled literally, not
92
+ // built from a constant: these are what 1.x and 2.0.0 actually put in users' settings files.
93
93
  [
94
- `node "$CLAUDE_PROJECT_DIR/${LEGACY_MANAGED_BY}" hook paper-edit-guard`,
94
+ `node "$CLAUDE_PROJECT_DIR/node_modules/research-paper-pipeline/bin/rpp.mjs" hook paper-edit-guard`,
95
95
  { name: "paper-edit-guard", ours: false, legacy: true },
96
96
  ],
97
+ [
98
+ `node "\${CLAUDE_PROJECT_DIR}/node_modules/paperlint/bin/rpp.mjs" hook paper-status-gates`,
99
+ { name: "paper-status-gates", ours: false, legacy: true },
100
+ ],
97
101
  [
98
102
  `node "$CLAUDE_PROJECT_DIR/node_modules/vigiles/dist/cli.js" hook-runtime run-program "$CLAUDE_PROJECT_DIR/node_modules/paperlint/hooks/paper-edit-guard.hook.mjs"`,
99
103
  { name: "paper-edit-guard", ours: false, legacy: false },
@@ -107,11 +111,11 @@ try {
107
111
  { name: "paper-skills-nudge", ours: false, legacy: false },
108
112
  ],
109
113
  [
110
- `node /abs/proj/node_modules/paperlint/bin/rpp.mjs hook paper-edit-guard`,
114
+ `node /abs/proj/node_modules/paperlint/bin/paperlint.mjs hook paper-edit-guard`,
111
115
  { name: "paper-edit-guard", ours: false, legacy: false },
112
116
  ],
113
117
  [`node my-own-lint.mjs`, null],
114
- [`node node_modules/paperlint/bin/rpp.mjs lint`, null],
118
+ [`node node_modules/paperlint/bin/paperlint.mjs lint`, null],
115
119
  ];
116
120
  for (const [cmd, want] of cases)
117
121
  check(
@@ -154,37 +158,42 @@ try {
154
158
  );
155
159
  }
156
160
 
157
- // ── an install under the old package name is migrated, the user's own command kept ─────
158
- {
161
+ // ── what an older install wrote is migrated, the user's own command kept ─────────────────
162
+ // Two real histories, spelled literally: 1.x wrote the old package directory, 2.0.0 wrote the
163
+ // new directory with the old entry file name. Neither file exists after an upgrade.
164
+ for (const [release, stale] of [
165
+ ["1.x", "node_modules/research-paper-pipeline/bin/rpp.mjs"],
166
+ ["2.0.0", "node_modules/paperlint/bin/rpp.mjs"],
167
+ ]) {
159
168
  const legacyWired = JSON.parse(
160
169
  JSON.stringify(merge({}, wiring.compiled, MANAGED_BY)).replaceAll(
161
170
  MANAGED_BY,
162
- LEGACY_MANAGED_BY,
171
+ stale,
163
172
  ),
164
173
  );
165
174
  legacyWired.hooks.PostToolUse[0].hooks.push({
166
175
  type: "command",
167
176
  command: "node my-own-lint.mjs",
168
177
  });
169
- const dir = project("legacy", legacyWired);
178
+ const dir = project(`legacy-${release}`, legacyWired);
170
179
  const before = doctorHooks(dir, wiring).join("\n");
171
180
  check(
172
- "🔴 doctor names hook commands left under the old package name, and the fix",
173
- /still point into node_modules\/research-paper-pipeline\//.test(before) &&
181
+ `🔴 doctor names hook commands ${release} left behind — the missing file, and the fix`,
182
+ before.includes(`run ${stale}, which this version does not install`) &&
174
183
  /npx paperlint init` replaces them/.test(before),
175
184
  );
176
185
  const r = wireHooks(dir, merge, wiring);
177
186
  const s = JSON.parse(text(dir));
178
187
  const counts = [...wiredCounts(s, wiring.names).values()];
179
188
  check(
180
- "🔴 init replaces them: every hook wired once under the new name, none left under the old",
189
+ `🔴 init replaces what ${release} wrote: every hook wired once, none left at the old path`,
181
190
  r.status === "written" &&
182
191
  r.replaced === wiring.names.length &&
183
192
  counts.every((c) => c.ours === 1 && c.legacy === 0 && c.other === 0) &&
184
- !text(dir).includes("research-paper-pipeline"),
193
+ !text(dir).includes(stale),
185
194
  );
186
195
  check(
187
- "and the user's own command in the same matcher survives the migration",
196
+ `and the user's own command in the same matcher survives the ${release} migration`,
188
197
  text(dir).includes("node my-own-lint.mjs"),
189
198
  );
190
199
  }
@@ -27,11 +27,9 @@ process.exit(
27
27
  expect: '→ {"name":"paper-edit-guard","ours":false,"legacy":true}',
28
28
  disables:
29
29
  "the duplicate guard for every bin spelling but ours. A hook wired by hand as " +
30
- "`npx paperlint hook …` or through an absolute rpp.mjs path reads as ours, init merges its own " +
30
+ "`npx paperlint hook …` or through an absolute path to the bin reads as ours, init merges its own " +
31
31
  "copy beside it, and the hook runs twice per event",
32
- edits: [
33
- [SRC, " ours: t === MANAGED_BY,", " ours: true,"],
34
- ],
32
+ edits: [[SRC, " ours: t === MANAGED_BY,", " ours: true,"]],
35
33
  },
36
34
  {
37
35
  name: "another spelling no longer stops the write",