paperlint 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/.github/workflows/ci.yml +4 -4
  2. package/CLAUDE.md +3 -3
  3. package/CONTRIBUTING.md +21 -0
  4. package/LICENSE +1 -1
  5. package/README.md +81 -24
  6. package/action.harness.mjs +10 -10
  7. package/action.mutations.mjs +3 -3
  8. package/action.yml +11 -11
  9. package/bin/{rpp.mjs → paperlint.mjs} +2 -2
  10. package/dist/adapters/banal/invocation.js +3 -3
  11. package/dist/adapters/banal/invocation.js.map +1 -1
  12. package/dist/adapters/banal/run.d.ts +1 -1
  13. package/dist/adapters/banal/run.js +1 -1
  14. package/dist/adapters/banal/run.js.map +1 -1
  15. package/dist/adapters/banal/settings.d.ts +4 -4
  16. package/dist/adapters/banal/settings.d.ts.map +1 -1
  17. package/dist/adapters/banal/settings.js +4 -4
  18. package/dist/adapters/banal/settings.js.map +1 -1
  19. package/dist/adapters/banal/xml.js +1 -1
  20. package/dist/adapters/banal/xml.js.map +1 -1
  21. package/dist/adapters/curl/download.io.d.ts.map +1 -1
  22. package/dist/adapters/curl/download.io.js +2 -2
  23. package/dist/adapters/curl/download.io.js.map +1 -1
  24. package/dist/build-engine.d.ts +1 -1
  25. package/dist/build-engine.d.ts.map +1 -1
  26. package/dist/build-engine.js +6 -3
  27. package/dist/build-engine.js.map +1 -1
  28. package/dist/build.d.ts +1 -1
  29. package/dist/build.d.ts.map +1 -1
  30. package/dist/build.js +5 -4
  31. package/dist/build.js.map +1 -1
  32. package/dist/cli.d.ts +29 -30
  33. package/dist/cli.d.ts.map +1 -1
  34. package/dist/cli.js +174 -98
  35. package/dist/cli.js.map +1 -1
  36. package/dist/doctor.d.ts.map +1 -1
  37. package/dist/doctor.js +21 -6
  38. package/dist/doctor.js.map +1 -1
  39. package/dist/engine.d.ts +2 -2
  40. package/dist/engine.d.ts.map +1 -1
  41. package/dist/engine.js +1 -1
  42. package/dist/facts-file.d.ts +33 -9
  43. package/dist/facts-file.d.ts.map +1 -1
  44. package/dist/facts-file.js +117 -17
  45. package/dist/facts-file.js.map +1 -1
  46. package/dist/hooks-settings.d.ts +0 -18
  47. package/dist/hooks-settings.d.ts.map +1 -1
  48. package/dist/hooks-settings.js +70 -42
  49. package/dist/hooks-settings.js.map +1 -1
  50. package/dist/init.d.ts +13 -10
  51. package/dist/init.d.ts.map +1 -1
  52. package/dist/init.js +74 -42
  53. package/dist/init.js.map +1 -1
  54. package/dist/link-skills.js +2 -2
  55. package/dist/link-skills.js.map +1 -1
  56. package/dist/new-paper.d.ts +2 -2
  57. package/dist/new-paper.d.ts.map +1 -1
  58. package/dist/new-paper.js +17 -2
  59. package/dist/new-paper.js.map +1 -1
  60. package/dist/paper-settings.d.ts +68 -0
  61. package/dist/paper-settings.d.ts.map +1 -0
  62. package/dist/paper-settings.js +144 -0
  63. package/dist/paper-settings.js.map +1 -0
  64. package/dist/presets.d.ts +84 -0
  65. package/dist/presets.d.ts.map +1 -0
  66. package/dist/presets.js +186 -0
  67. package/dist/presets.js.map +1 -0
  68. package/dist/rules-config.d.ts +8 -1
  69. package/dist/rules-config.d.ts.map +1 -1
  70. package/dist/rules-config.js +13 -8
  71. package/dist/rules-config.js.map +1 -1
  72. package/dist/structure.d.ts.map +1 -1
  73. package/dist/structure.js +2 -1
  74. package/dist/structure.js.map +1 -1
  75. package/dist/tex-requirements.d.ts +63 -7
  76. package/dist/tex-requirements.d.ts.map +1 -1
  77. package/dist/tex-requirements.js +91 -24
  78. package/dist/tex-requirements.js.map +1 -1
  79. package/dist/toolchain.d.ts +2 -2
  80. package/dist/toolchain.d.ts.map +1 -1
  81. package/dist/toolchain.js +8 -8
  82. package/dist/toolchain.js.map +1 -1
  83. package/dist/types.d.ts +4 -4
  84. package/dist/types.d.ts.map +1 -1
  85. package/dist/venue-rules.d.ts +109 -0
  86. package/dist/venue-rules.d.ts.map +1 -0
  87. package/dist/venue-rules.js +387 -0
  88. package/dist/venue-rules.js.map +1 -0
  89. package/docs/configuration.md +73 -17
  90. package/docs/e2e.md +3 -4
  91. package/docs/install.md +11 -3
  92. package/docs/optional-rules.md +26 -25
  93. package/docs/rules.md +132 -3
  94. package/docs/toolchain.md +5 -5
  95. package/eslint-rules/paper-typography.mjs +1 -1
  96. package/eslint-rules/paper-typography.mutations.mjs +1 -1
  97. package/eslint-rules/papers.mjs +6 -2
  98. package/eslint-rules/pdf-last-page-balance.harness.mjs +5 -3
  99. package/eslint-rules/pdf-last-page-balance.mjs +23 -2
  100. package/fixtures/build-e2e/acmart/paperlint.json +1 -0
  101. package/fixtures/build-e2e/guards/paper.tex +1 -1
  102. package/fixtures/pdf-facts/README.md +1 -1
  103. package/fixtures/real-markdown-paper/baseline.json +1 -1
  104. package/fixtures/real-markdown-paper/baseline.mjs +2 -2
  105. package/fixtures/toolchain-mirror/install-tl +2 -2
  106. package/fixtures/toolchain-mirror/stub-pdflatex +1 -1
  107. package/fixtures/toolchain-mirror/stub-tlmgr +1 -1
  108. package/hooks/hooks.harness.mjs +1 -1
  109. package/lib/paper-config.d.mts +7 -0
  110. package/lib/paper-config.harness.mjs +3 -3
  111. package/lib/paper-config.mjs +34 -3
  112. package/lib/skill-trigger-cases.harness.mjs +1 -1
  113. package/package.json +4 -4
  114. package/plugin/hooks/hooks.json +3 -3
  115. package/scripts/check.mjs +1 -1
  116. package/scripts/harness-api.frozen.json +1 -1
  117. package/scripts/harness-api.test.ts +2 -2
  118. package/scripts/layer-legacy.frozen.json +1 -1
  119. package/scripts/mutation-batteries-frozen.harness.mjs +1 -1
  120. package/scripts/mutation-batteries.frozen.json +2 -2
  121. package/scripts/release-config.test.ts +1 -1
  122. package/skills/find-venue/SKILL.md +5 -1
  123. package/skills/find-venue/SKILL.md.spec.ts +4 -0
  124. package/skills/paper-pipeline/PIPELINE-MAP.md +14 -4
  125. package/skills/paper-pipeline/SKILL.md +4 -3
  126. package/skills/paper-pipeline/SKILL.md.spec.ts +3 -2
  127. package/skills/paper-pipeline/description-language.eval.mjs +1 -1
  128. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +1 -1
  129. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +2 -2
  130. package/skills/paper-pipeline/pipeline-firing.eval.mjs +1 -1
  131. package/skills/paper-pipeline/pipeline-language.eval.mjs +1 -1
  132. package/skills/paper-pipeline/scripts/consumer.d.mts +1 -0
  133. package/skills/paper-pipeline/scripts/consumer.harness.mjs +10 -4
  134. package/skills/paper-pipeline/scripts/consumer.mjs +8 -1
  135. package/skills/paper-pipeline/scripts/consumer.mutations.mjs +1 -1
  136. package/skills/render-paper/SKILL.md +3 -3
  137. package/skills/render-paper/SKILL.md.spec.ts +2 -2
  138. package/skills/render-paper/extract-pdf-facts.harness.mjs +6 -6
  139. package/skills/render-paper/extract-pdf-facts.mjs +4 -4
  140. package/skills/study-accepted-papers/SKILL.md +2 -1
  141. package/skills/study-accepted-papers/SKILL.md.spec.ts +1 -0
  142. package/skills/submit-paper/SKILL.md +9 -1
  143. package/skills/submit-paper/SKILL.md.spec.ts +8 -0
  144. package/skills/submit-paper/references/venues/acm-sigconf.jsonc +86 -0
  145. package/skills/submit-paper/references/venues/agenticdev.jsonc +34 -102
  146. package/skills/submit-paper/references/venues/aisec.jsonc +21 -82
  147. package/skills/submit-paper/references/venues/realm.jsonc +45 -43
  148. package/skills/submit-paper/references/venues/tex-base.jsonc +1 -1
  149. package/skills/submit-paper/references/venues/venue-profile.schema.json +120 -30
  150. package/src/adapters/banal/index.test.ts +1 -1
  151. package/src/adapters/banal/invocation.test.ts +3 -3
  152. package/src/adapters/banal/invocation.ts +3 -3
  153. package/src/adapters/banal/locate.test.ts +4 -4
  154. package/src/adapters/banal/run.ts +2 -2
  155. package/src/adapters/banal/settings.test.ts +12 -6
  156. package/src/adapters/banal/settings.ts +9 -5
  157. package/src/adapters/banal/xml.test.ts +1 -1
  158. package/src/adapters/banal/xml.ts +1 -1
  159. package/src/adapters/curl/download.io.ts +4 -2
  160. package/src/adapters/curl/download.test.ts +3 -1
  161. package/src/adapters/node/files.test.ts +1 -1
  162. package/src/adapters/node/process.test.ts +2 -2
  163. package/src/adapters/node/workspace.test.ts +4 -4
  164. package/src/build-engine.harness.mjs +2 -2
  165. package/src/build-engine.ts +11 -3
  166. package/src/build.harness.mjs +27 -22
  167. package/src/build.mutations.mjs +3 -2
  168. package/src/build.ts +6 -5
  169. package/src/cli.harness.mjs +72 -131
  170. package/src/cli.mutations.mjs +7 -32
  171. package/src/cli.ts +204 -121
  172. package/src/doctor.harness.mjs +9 -25
  173. package/src/doctor.ts +28 -5
  174. package/src/engine.harness.mjs +4 -2
  175. package/src/engine.ts +2 -2
  176. package/src/facts-file.test.ts +54 -7
  177. package/src/facts-file.ts +145 -24
  178. package/src/hooks-settings.harness.mjs +24 -15
  179. package/src/hooks-settings.mutations.mjs +2 -4
  180. package/src/hooks-settings.ts +92 -49
  181. package/src/init.ts +88 -52
  182. package/src/latex-log.harness.mjs +1 -1
  183. package/src/link-skills.harness.mjs +3 -1
  184. package/src/link-skills.mutations.mjs +1 -1
  185. package/src/link-skills.ts +2 -2
  186. package/src/new-paper.harness.mjs +10 -7
  187. package/src/new-paper.test.ts +145 -0
  188. package/src/new-paper.ts +22 -2
  189. package/src/paper-settings-commands.test.ts +311 -0
  190. package/src/paper-settings.test.ts +220 -0
  191. package/src/paper-settings.ts +206 -0
  192. package/src/pdf-facts.harness.mjs +1 -1
  193. package/src/presets.test.ts +236 -0
  194. package/src/presets.ts +307 -0
  195. package/src/rules-config.ts +14 -9
  196. package/src/structure.harness.mjs +4 -4
  197. package/src/structure.ts +2 -1
  198. package/src/tex-requirements.harness.mjs +12 -18
  199. package/src/tex-requirements.ts +179 -29
  200. package/src/toolchain.harness.mjs +34 -25
  201. package/src/toolchain.ts +14 -8
  202. package/src/types.ts +4 -4
  203. package/src/venue-rules.test.ts +545 -0
  204. package/src/venue-rules.ts +573 -0
  205. package/templates/paper/paperlint.json +4 -0
  206. package/fixtures/build-e2e/acmart/venue.json +0 -1
  207. package/plugin/.claude-plugin/plugin.json +0 -8
package/src/build.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * ── THE SHAPE: an ordered list of STEPS, composed, not configured ────────────
14
14
  * Each step says whether it applies to THIS paper and why — decided from FACTS parsed out of the
15
- * paper (its `\documentclass` and options, the venue named in `venue.json`), never from a config
15
+ * paper (its `\documentclass` and options, the venue named in `paperlint.json`), never from a config
16
16
  * flag. `paperlint build` prints that plan before running anything, and `--dry-run` prints only the
17
17
  * plan. Adding a step is one entry in `STEPS`.
18
18
  *
@@ -33,7 +33,7 @@
33
33
  * ── WHAT IS PURE AND WHAT IS NOT ─────────────────────────────────────────────
34
34
  * The decision of the LaTeX loop is `latex-loop.ts`, reading the log is `latex-log.ts`; both are
35
35
  * pure. This file is the shell: it runs processes through the injected `run` (the existing port —
36
- * `spawnSync` by default) and reads the files a pass left behind. Where rpp's own files live is
36
+ * `spawnSync` by default) and reads the files a pass left behind. Where paperlint's own files live is
37
37
  * answered by `consumer.mjs`, the one module allowed to know it (rule 10).
38
38
  */
39
39
  // eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
@@ -45,6 +45,7 @@ import { delimiter, join, relative } from "node:path";
45
45
  // eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
46
46
  import { getParser } from "@unified-latex/unified-latex-util-parse";
47
47
  import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
48
+ import { PAPER_SETTINGS_FILE } from "../lib/paper-config.mjs";
48
49
  import {
49
50
  declaredVenue,
50
51
  factsPath,
@@ -86,7 +87,7 @@ export const PAPER_MARKERS = [
86
87
  "PIPELINE-STATUS.md",
87
88
  "paper.tex",
88
89
  "paper.md",
89
- "venue.json",
90
+ PAPER_SETTINGS_FILE,
90
91
  ];
91
92
 
92
93
  /** The source paperlint compiles, and the job name every output file carries. */
@@ -114,7 +115,7 @@ export interface PaperFacts {
114
115
  readonly name: string;
115
116
  readonly options: readonly string[];
116
117
  } | null;
117
- /** The `venue` field of `venue.json`, or null. */
118
+ /** The label of the venue preset `paperlint.json` extends, or null. */
118
119
  readonly venue: string | null;
119
120
  /** Paper-supplied build scripts found on disk — reported as ignored. */
120
121
  readonly ignoredScripts: readonly string[];
@@ -222,7 +223,7 @@ export function readFacts(paperDir: string): PaperFacts {
222
223
  return {
223
224
  main,
224
225
  documentclass: documentclassOf(ast),
225
- venue: venue?.venue ?? null,
226
+ venue: venue?.label ?? null,
226
227
  ignoredScripts: IGNORED_SCRIPTS.filter((s) =>
227
228
  existsSync(join(paperDir, s)),
228
229
  ),
@@ -35,24 +35,10 @@ import {
35
35
  } from "../lib/paper-config.mjs";
36
36
 
37
37
  const HERE = dirname(fileURLToPath(import.meta.url));
38
- const {
39
- run,
40
- parseArgs,
41
- buildConfig,
42
- nextSteps,
43
- findConfig,
44
- findDeclaration,
45
- toPaths,
46
- runHook,
47
- } = await import(join(HERE, "cli.ts"));
48
- const {
49
- init,
50
- choosePapers,
51
- offerWorkflow,
52
- syncRppJson,
53
- missingPrograms,
54
- WORKFLOW_PATH,
55
- } = await import(join(HERE, "init.ts"));
38
+ const { run, parseArgs, buildConfig, nextSteps, findConfig, toPaths, runHook } =
39
+ await import(join(HERE, "cli.ts"));
40
+ const { init, choosePapers, offerWorkflow, missingPrograms, WORKFLOW_PATH } =
41
+ await import(join(HERE, "init.ts"));
56
42
  const { PROGRAMS } = await import(join(HERE, "doctor.ts"));
57
43
 
58
44
  let n = 0;
@@ -192,11 +178,11 @@ check(
192
178
  // 🔴 WITHOUT THIS BLOCK THE REWRITTEN `init` WOULD PRODUCE A BROKEN INSTALL. It writes one
193
179
  // declaration — into `package.json`, the file the hooks know how to read (a hook does not
194
180
  // import code and cannot walk up the tree; it can only read a path it is able to name). The
195
- // utility, though, read ONLY `rpp.json`, so right after `paperlint init` its `lint` would say
181
+ // utility, though, read ONLY its own config file, so right after `paperlint init` its `lint` would say
196
182
  // "nothing to lint". I.e. the install command and the check command would be looking at
197
183
  // different files — exactly the defect it exists to close, just from the other side.
198
184
  {
199
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-carrier-")));
185
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-carrier-")));
200
186
  try {
201
187
  const paper = join(root, "papers", "p1");
202
188
  mkdirSync(join(paper, "versions"), { recursive: true });
@@ -327,26 +313,6 @@ check(
327
313
  // 🔴 A package.json WITHOUT the key does not stop the walk upward. Otherwise the search
328
314
  // would end at the first project going up the tree — and every project has a package.json —
329
315
  // and would never find anything, ever.
330
- writeFileSync(
331
- join(root, "package.json"),
332
- JSON.stringify({ name: "c", version: "1.0.0" }),
333
- );
334
- writeFileSync(
335
- join(root, "rpp.json"),
336
- JSON.stringify({ [PAPERS_DIR_FIELD]: "papers" }),
337
- );
338
- const viaRpp = await cli(["lint"], root);
339
- check(
340
- "a keyless package.json does not intercept the search — rpp.json is still found",
341
- viaRpp.code === 0 && /config: rpp\.json/.test(viaRpp.out),
342
- );
343
- check(
344
- "🔴 but a deprecated carrier is NAMED, not just silently read",
345
- /rpp\.json is deprecated/.test(viaRpp.out) &&
346
- /the hooks read only that file/.test(viaRpp.out),
347
- );
348
-
349
- // Both carriers side by side: the one everything ELSE reads wins.
350
316
  writeFileSync(
351
317
  join(root, "package.json"),
352
318
  JSON.stringify({
@@ -355,14 +321,20 @@ check(
355
321
  paperlint: { [PAPERS_DIR_FIELD]: "papers" },
356
322
  }),
357
323
  );
324
+ const inner = join(root, "tools");
325
+ mkdirSync(inner, { recursive: true });
326
+ writeFileSync(
327
+ join(inner, "package.json"),
328
+ JSON.stringify({ name: "tools", version: "1.0.0" }),
329
+ );
358
330
  check(
359
- 'with both carriers present package.json is chosen — that is what "one declaration" means',
360
- findDeclaration(root)?.kind === "package.json" &&
361
- findDeclaration(root)?.path === join(root, "package.json"),
331
+ "a keyless package.json does not intercept the search — the one above it is found",
332
+ findConfig(inner) === join(root, "package.json"),
362
333
  );
334
+ const viaInner = await cli(["lint"], inner);
363
335
  check(
364
- 'and findConfig still answers the question "which file holds the settings"',
365
- findConfig(root) === join(root, "package.json"),
336
+ "and `lint` run from under it uses that declaration",
337
+ viaInner.code === 0 && /config: \.\.\/package\.json/.test(viaInner.out),
366
338
  );
367
339
  } finally {
368
340
  rmSync(root, { recursive: true, force: true });
@@ -372,7 +344,7 @@ check(
372
344
  // ── `init` — THREE ACTIONS INSTEAD OF NINE ────────────────────────────────────────────────
373
345
  //
374
346
  // 🔴 THE DEFECT THIS COMMAND WAS REWRITTEN FOR (issue #33, measured 09-18): init wrote
375
- // `rpp.json` with a GUESSED `"papers": "papers"` and never touched `package.json` — while the
347
+ // a separate config file with a GUESSED `"papers": "papers"` and never touched `package.json` — while the
376
348
  // three hooks read the papers directory from exactly there. A consumer who did everything by
377
349
  // the book got a `paper-edit-guard` guarding a directory that did not exist; from outside that
378
350
  // is indistinguishable from a working guard, because silence is its success state.
@@ -381,7 +353,7 @@ check(
381
353
  // stdin. A harness stuck at an input prompt is not a red test, it is the absence of any answer
382
354
  // at all.
383
355
  {
384
- const workRoot = realpathSync(mkdtempSync(join(tmpdir(), "rpp-init-")));
356
+ const workRoot = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-init-")));
385
357
  const project = (
386
358
  name,
387
359
  { pkg = { name: "consumer", version: "1.0.0" }, papers = [] } = {},
@@ -441,10 +413,6 @@ check(
441
413
  "and it says HOW it was measured — otherwise a guess reads as a fact",
442
414
  /measured: its subdirectories carry/.test(out.text()),
443
415
  );
444
- check(
445
- "🔴 `rpp.json` IS NO LONGER CREATED — a second declaration is what doctor exists to catch",
446
- !existsSync(join(dir, "rpp.json")),
447
- );
448
416
  check(
449
417
  "init ends with doctor's report: the install vouches for its OWN state",
450
418
  /paperlint doctor — what is wired/.test(out.text()),
@@ -622,7 +590,7 @@ check(
622
590
  );
623
591
  check(
624
592
  "and the CI step is still printed — it just gets pasted in by hand",
625
- /uses: zernie\/research-paper-pipeline@/.test(out.text()) &&
593
+ /uses: zernie\/paperlint@/.test(out.text()) &&
626
594
  /paths: writing/.test(out.text()),
627
595
  );
628
596
  }
@@ -639,7 +607,7 @@ check(
639
607
  });
640
608
  check(
641
609
  "🔴 a released version — the printed step is pinned to its tag, no <commit-sha>",
642
- /uses: zernie\/research-paper-pipeline@v1\.2\.3\b/.test(out.text()) &&
610
+ /uses: zernie\/paperlint@v1\.2\.3\b/.test(out.text()) &&
643
611
  !/<commit-sha>/.test(out.text()),
644
612
  );
645
613
  }
@@ -691,7 +659,7 @@ check(
691
659
  check(
692
660
  "🔴 a released version — the written workflow is pinned to its tag, no <commit-sha> left",
693
661
  wf === "written" &&
694
- uses.includes("zernie/research-paper-pipeline@v1.2.3") &&
662
+ uses.includes("zernie/paperlint@v1.2.3") &&
695
663
  !yaml.includes("<commit-sha>"),
696
664
  );
697
665
  }
@@ -847,47 +815,7 @@ check(
847
815
  );
848
816
  }
849
817
 
850
- // ── 8. `rpp.json` FOR THOSE WHO ALREADY HAVE ONE ──────────────────────────────────
851
- {
852
- const dir = project("legacy", { papers: ["writing"] });
853
- writeFileSync(
854
- join(dir, "rpp.json"),
855
- JSON.stringify({ minFindings: 5 }, null, 2) + "\n",
856
- );
857
- const out = say();
858
- await init(dir, {
859
- log: out.log,
860
- err: out.log,
861
- interactive: false,
862
- run: haveAll,
863
- });
864
- check(
865
- "an existing rpp.json gets the SAME value, rather than drifting silently",
866
- JSON.parse(readFileSync(join(dir, "rpp.json"), "utf8"))[
867
- PAPERS_DIR_FIELD
868
- ] === "writing",
869
- );
870
- check(
871
- "and is named deprecated",
872
- /rpp\.json was already here/.test(out.text()),
873
- );
874
-
875
- const own = join(workRoot, "legacy-own");
876
- mkdirSync(own, { recursive: true });
877
- writeFileSync(
878
- join(own, "package.json"),
879
- '{"name":"c","version":"1.0.0"}',
880
- );
881
- writeFileSync(join(own, "rpp.json"), `{"${PAPERS_DIR_FIELD}":"mine"}`);
882
- check(
883
- "and the papers-directory value it already declares stays byte for byte — this too is someone else's value",
884
- syncRppJson(own, "writing") === "kept" &&
885
- readFileSync(join(own, "rpp.json"), "utf8") ===
886
- `{"${PAPERS_DIR_FIELD}":"mine"}`,
887
- );
888
- }
889
-
890
- // ── 9. THE COMMAND IS WIRED TO `run`, NOT MERELY EXPORTED ────────────────────────
818
+ // ── 8. THE COMMAND IS WIRED TO `run`, NOT MERELY EXPORTED ────────────────────────
891
819
  {
892
820
  const dir = project("wired", { papers: ["writing"] });
893
821
  const r = await cli(["init", dir]);
@@ -922,7 +850,7 @@ check(
922
850
  // A "." default would give a green run over whatever happens to be lying around — the same
923
851
  // contract as the action, and the same reason. An empty directory with no config is exactly
924
852
  // that case.
925
- const bare = realpathSync(mkdtempSync(join(tmpdir(), "rpp-bare-")));
853
+ const bare = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-bare-")));
926
854
  try {
927
855
  const r = await cli(["lint"], bare);
928
856
  check(
@@ -939,7 +867,7 @@ check(
939
867
 
940
868
  // ── on live files: both halves ───────────────────────────────────────────────────────
941
869
  {
942
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-cli-")));
870
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-cli-")));
943
871
  try {
944
872
  const paper = join(root, "papers", "p1");
945
873
  mkdirSync(join(paper, "versions"), { recursive: true });
@@ -1021,12 +949,12 @@ check(
1021
949
 
1022
950
  // ── THE CONFIG FINDS ITSELF, AND THE PAPERS DIRECTORY IS DECLARED IN IT ──────────────────
1023
951
  //
1024
- // 🔴 THE DEFECT THIS BLOCK EXISTS FOR: `paperlint init` wrote `rpp.json`, and `rpp check` only read
952
+ // 🔴 THE DEFECT THIS BLOCK EXISTS FOR: `paperlint init` wrote a config file, and `check` only read
1025
953
  // it via an explicit `--options`. I.e. the file the utility itself created had no effect on the
1026
954
  // run — and there was no way to find that out: zero typography debt looks exactly like a
1027
955
  // config that was never found.
1028
956
  {
1029
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-cfg-")));
957
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-cfg-")));
1030
958
  try {
1031
959
  const paper = join(root, "papers", "p1");
1032
960
  mkdirSync(join(paper, "versions"), { recursive: true });
@@ -1042,21 +970,23 @@ check(
1042
970
 
1043
971
  // findConfig — kept separate from the run so the failure is distinguishable
1044
972
  writeFileSync(
1045
- join(root, "rpp.json"),
1046
- JSON.stringify({ [PAPERS_DIR_FIELD]: "papers" }),
973
+ join(root, "package.json"),
974
+ JSON.stringify({ paperlint: { [PAPERS_DIR_FIELD]: "papers" } }),
1047
975
  );
1048
976
  check(
1049
977
  "findConfig walks up from a subdirectory and finds the file at the root",
1050
- findConfig(paper) === join(root, "rpp.json"),
978
+ findConfig(paper) === join(root, "package.json"),
1051
979
  );
1052
980
  // There can be no tautology here: a neighboring tree must NOT pick up our file. Asserting
1053
- // `=== null` against a live filesystem is not safe — someone else's rpp.json may sit further
981
+ // `=== null` against a live filesystem is not safe — someone else's package.json may sit further
1054
982
  // up the chain — so what is asserted is what is actually checkable: our config is not
1055
983
  // visible from there.
1056
- const sibling = realpathSync(mkdtempSync(join(tmpdir(), "rpp-other-")));
984
+ const sibling = realpathSync(
985
+ mkdtempSync(join(tmpdir(), "paperlint-other-")),
986
+ );
1057
987
  check(
1058
988
  "the config does NOT leak into a neighboring tree — the search goes up, not sideways",
1059
- findConfig(sibling) !== join(root, "rpp.json"),
989
+ findConfig(sibling) !== join(root, "package.json"),
1060
990
  );
1061
991
  rmSync(sibling, { recursive: true, force: true });
1062
992
 
@@ -1067,7 +997,7 @@ check(
1067
997
  );
1068
998
  check(
1069
999
  "and the found file is NAMED out loud — swapping settings silently is not acceptable",
1070
- /config: rpp\.json/.test(found.out),
1000
+ /config: package\.json/.test(found.out),
1071
1001
  );
1072
1002
 
1073
1003
  // 🔴 THE RESOLUTION DISCRIMINATOR. Run FROM the paper's directory: same config, and
@@ -1100,7 +1030,10 @@ check(
1100
1030
 
1101
1031
  // 🔴 THE PAPERS DIRECTORY — A REQUIRED FIELD. A config without it is not an "empty config" but an
1102
1032
  // unfinished one: silently falling back to "." means running the rules over the whole checkout.
1103
- writeFileSync(join(root, "rpp.json"), JSON.stringify({ minFindings: 3 }));
1033
+ writeFileSync(
1034
+ join(root, "package.json"),
1035
+ JSON.stringify({ paperlint: { minFindings: 3 } }),
1036
+ );
1104
1037
  const noPapers = await cli(["lint"], root);
1105
1038
  check(
1106
1039
  "a config WITHOUT the papers-directory field — a failure, and the field is named by name",
@@ -1113,8 +1046,8 @@ check(
1113
1046
  (
1114
1047
  await (async () => {
1115
1048
  writeFileSync(
1116
- join(root, "rpp.json"),
1117
- JSON.stringify({ [PAPERS_DIR_FIELD]: "" }),
1049
+ join(root, "package.json"),
1050
+ JSON.stringify({ paperlint: { [PAPERS_DIR_FIELD]: "" } }),
1118
1051
  );
1119
1052
  return await cli(["lint"], root);
1120
1053
  })()
@@ -1131,7 +1064,7 @@ check(
1131
1064
  // the threshold had to show up here too — otherwise it would have been lost SILENTLY: the run
1132
1065
  // would stay green, and the consumer's setting would stop meaning anything.
1133
1066
  {
1134
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-warn-")));
1067
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-warn-")));
1135
1068
  try {
1136
1069
  const paper = join(root, "papers", "p1");
1137
1070
  mkdirSync(join(paper, "versions"), { recursive: true });
@@ -1150,8 +1083,8 @@ check(
1150
1083
  `---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: 100\n source: versions/s.tex\n sourceBytes: 4\nresearchQuestion: "does it hold?"\n---\n# S\n\n| id | note |\n|---|---|\n| cites | bib-authors run |\n`,
1151
1084
  );
1152
1085
  writeFileSync(
1153
- join(root, "rpp.json"),
1154
- JSON.stringify({ [PAPERS_DIR_FIELD]: "papers" }),
1086
+ join(root, "package.json"),
1087
+ JSON.stringify({ paperlint: { [PAPERS_DIR_FIELD]: "papers" } }),
1155
1088
  );
1156
1089
 
1157
1090
  const lax = await cli(["lint"], root);
@@ -1184,15 +1117,15 @@ check(
1184
1117
  // one fact that harness cannot know: that the module is WIRED IN. A correct module forgotten in
1185
1118
  // `run()` gives zero findings and looks like a clean corpus.
1186
1119
  {
1187
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-wired-")));
1120
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-wired-")));
1188
1121
  try {
1189
1122
  const paper = join(root, "papers", "orphan");
1190
1123
  mkdirSync(paper, { recursive: true });
1191
1124
  // The marker is present, the scorecard is not: NOT ONE pipeline rule runs over this directory.
1192
1125
  writeFileSync(join(paper, "paper.tex"), "\\documentclass{article}\n");
1193
1126
  writeFileSync(
1194
- join(root, "rpp.json"),
1195
- JSON.stringify({ [PAPERS_DIR_FIELD]: "papers" }),
1127
+ join(root, "package.json"),
1128
+ JSON.stringify({ paperlint: { [PAPERS_DIR_FIELD]: "papers" } }),
1196
1129
  );
1197
1130
 
1198
1131
  const r = await cli(["lint"], root);
@@ -1225,7 +1158,7 @@ check(
1225
1158
  );
1226
1159
  check(
1227
1160
  "and the line about the found config is not lost — it went to stderr",
1228
- /config: rpp\.json/.test(j.stderr) && !/config:/.test(j.stdout),
1161
+ /config: package\.json/.test(j.stderr) && !/config:/.test(j.stdout),
1229
1162
  );
1230
1163
  check(
1231
1164
  "`--json` gives back ONE array, where the missing-file finding sits next to the rule findings",
@@ -1277,8 +1210,10 @@ check(
1277
1210
  return [`UNPARSABLE: ${r.out}`];
1278
1211
  }
1279
1212
  };
1280
- const tree = realpathSync(mkdtempSync(join(tmpdir(), "rpp-outside-")));
1281
- const elsewhere = realpathSync(mkdtempSync(join(tmpdir(), "rpp-elsewhere-")));
1213
+ const tree = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-outside-")));
1214
+ const elsewhere = realpathSync(
1215
+ mkdtempSync(join(tmpdir(), "paperlint-elsewhere-")),
1216
+ );
1282
1217
  try {
1283
1218
  const paper = join(tree, "papers", "p");
1284
1219
  mkdirSync(paper, { recursive: true });
@@ -1435,13 +1370,13 @@ check(
1435
1370
  //
1436
1371
  // 🔴 npm puts a SYMLINK in `node_modules/.bin/`. The first version compared `import.meta.url`
1437
1372
  // against `file://${process.argv[1]}`: for a symlink those two paths are DIFFERENT, the
1438
- // condition is false, and the utility silently exited zero. A direct `node bin/rpp.mjs` worked
1373
+ // condition is false, and the utility silently exited zero. A direct `node bin/paperlint.mjs` worked
1439
1374
  // fine, meanwhile — i.e. the defect was invisible in exactly the way it is normally checked.
1440
1375
  {
1441
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-link-")));
1376
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-link-")));
1442
1377
  try {
1443
- const link = join(root, "rpp-shim");
1444
- symlinkSync(join(HERE, "..", "bin", "rpp.mjs"), link);
1378
+ const link = join(root, "paperlint-shim");
1379
+ symlinkSync(join(HERE, "..", "bin", "paperlint.mjs"), link);
1445
1380
  const paper = join(root, "papers", "p");
1446
1381
  mkdirSync(join(paper, "versions"), { recursive: true });
1447
1382
  writeFileSync(
@@ -1552,7 +1487,9 @@ console.log(
1552
1487
 
1553
1488
  // ── init wires the hooks, and a human can say no ─────────────────────────────────────────
1554
1489
  {
1555
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-init-hooks-")));
1490
+ const root = realpathSync(
1491
+ mkdtempSync(join(tmpdir(), "paperlint-init-hooks-")),
1492
+ );
1556
1493
  const mk = (name) => {
1557
1494
  const dir = join(root, name);
1558
1495
  mkdirSync(join(dir, "papers", "p1"), { recursive: true });
@@ -1642,7 +1579,9 @@ console.log(
1642
1579
 
1643
1580
  // ── init offers a first paper only where there is none, and only to a human ─────────────
1644
1581
  {
1645
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-init-paper-")));
1582
+ const root = realpathSync(
1583
+ mkdtempSync(join(tmpdir(), "paperlint-init-paper-")),
1584
+ );
1646
1585
  const bare = (name) => {
1647
1586
  const dir = join(root, name);
1648
1587
  mkdirSync(dir, { recursive: true });
@@ -1801,7 +1740,7 @@ console.log(
1801
1740
 
1802
1741
  // ── `paperlint lint` does not sweep the project's paper TEMPLATE as a paper ────────────────────
1803
1742
  {
1804
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-template-")));
1743
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-template-")));
1805
1744
  try {
1806
1745
  writeFileSync(
1807
1746
  join(root, "package.json"),
@@ -1843,7 +1782,7 @@ console.log(
1843
1782
 
1844
1783
  // ── `paperlint new` through the CLI: the papers directory comes from the one declaration ────────
1845
1784
  {
1846
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-new-cli-")));
1785
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-new-cli-")));
1847
1786
  try {
1848
1787
  const none = await cli(["new", "demo"], root);
1849
1788
  check(
@@ -1856,11 +1795,13 @@ console.log(
1856
1795
  );
1857
1796
  const r = await cli(["new", "demo"], root);
1858
1797
  check(
1859
- "🔴 `paperlint new demo` creates writing/demo/ and its lint is the verdict — exit 0, no findings",
1798
+ "🔴 `paperlint new demo` creates writing/demo/ and its lint is the verdict — exit 0, and only the warning that no venue is chosen yet",
1860
1799
  r.code === 0 &&
1861
1800
  existsSync(join(root, "writing", "demo", "PIPELINE-STATUS.md")) &&
1862
1801
  existsSync(join(root, "writing", "demo", "paper.tex")) &&
1863
- /no findings/.test(r.out),
1802
+ existsSync(join(root, "writing", "demo", "paperlint.json")) &&
1803
+ /names no venue preset yet/.test(r.out) &&
1804
+ /\(0 errors, 1 warning\)/.test(r.out),
1864
1805
  );
1865
1806
  const again = await cli(["new", "demo", "--format", "md"], root);
1866
1807
  check(
@@ -1887,7 +1828,7 @@ console.log(
1887
1828
 
1888
1829
  // ── the settings are parsed at the boundary: unknown keys, `rules` blocks ─────────────────
1889
1830
  {
1890
- const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-settings-")));
1831
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-settings-")));
1891
1832
  try {
1892
1833
  const dir = join(root, "papers", "p");
1893
1834
  mkdirSync(dir, { recursive: true });
@@ -1899,13 +1840,13 @@ console.log(
1899
1840
  join(dir, "paper.tex"),
1900
1841
  "\\documentclass{article}\n\\begin{document}x\\end{document}\n",
1901
1842
  );
1902
- const settings = (rpp) =>
1843
+ const settings = (extra) =>
1903
1844
  writeFileSync(
1904
1845
  join(root, "package.json"),
1905
1846
  JSON.stringify({
1906
1847
  name: "c",
1907
1848
  version: "1.0.0",
1908
- paperlint: { [PAPERS_DIR_FIELD]: "papers", ...rpp },
1849
+ paperlint: { [PAPERS_DIR_FIELD]: "papers", ...extra },
1909
1850
  }),
1910
1851
  );
1911
1852
  const balanceOn = (files) => ({
@@ -11,12 +11,12 @@ const HERE = dirname(fileURLToPath(import.meta.url));
11
11
  const ROOT = resolve(HERE, "..");
12
12
  const CLI = join(HERE, "cli.ts");
13
13
  const HARNESS = join(HERE, "cli.harness.mjs");
14
- // The `bin/rpp.mjs` shim — the package's EXECUTABLE file, and after the move to TypeScript the
14
+ // The `bin/paperlint.mjs` shim — the package's EXECUTABLE file, and after the move to TypeScript the
15
15
  // "was I run or was I imported" check lives right there: `dist/cli.js` is now always imported,
16
16
  // so a mutation in it about the symlink proves nothing. The first run after the move showed
17
17
  // this literally — the mutation SURVIVED, and it looked like a hole in the harness, when the
18
18
  // hole was in what the mutation was aimed at.
19
- const SHIM = join(HERE, "..", "bin", "rpp.mjs");
19
+ const SHIM = join(HERE, "..", "bin", "paperlint.mjs");
20
20
  // `init` stopped being twenty lines inside `cli.ts` and became its own module: it has four
21
21
  // decisions, and each must be able to break in a way that EXACTLY its own assertion notices.
22
22
  const INIT = join(HERE, "init.ts");
@@ -35,7 +35,7 @@ process.exit(
35
35
  "the only way a consumer ever calls the utility. npm puts a SYMLINK in .bin, for " +
36
36
  "which process.argv[1] and import.meta.url are different paths; comparing strings " +
37
37
  "makes the condition false and the utility SILENTLY exits zero. A direct " +
38
- "`node bin/rpp.mjs` still works, meanwhile, so the defect is invisible in exactly the " +
38
+ "`node bin/paperlint.mjs` still works, meanwhile, so the defect is invisible in exactly the " +
39
39
  "way it is normally checked",
40
40
  edits: [
41
41
  [
@@ -153,22 +153,6 @@ process.exit(
153
153
  "value, because nobody told them it changed",
154
154
  edits: [[INIT, " if (existing !== undefined) {", " if (false) {"]],
155
155
  },
156
- {
157
- name: "init CREATES the second rpp.json carrier again",
158
- harness: HARNESS,
159
- expect:
160
- "🔴 `rpp.json` IS NO LONGER CREATED — a second declaration is what doctor exists to catch",
161
- disables:
162
- '"one declaration". Two carriers drift apart silently — this is defect #33, ' +
163
- "reintroduced by the very install command meant to fix it",
164
- edits: [
165
- [
166
- INIT,
167
- ' if (!existsSync(path)) return "absent";',
168
- ' if (!existsSync(path)) writeFileSync(path, "{}\\n", "utf8");',
169
- ],
170
- ],
171
- },
172
156
  {
173
157
  name: "the default that was taken stops being named",
174
158
  harness: HARNESS,
@@ -286,31 +270,22 @@ process.exit(
286
270
  ],
287
271
  },
288
272
  {
289
- name: "the utility goes back to reading only rpp.json",
273
+ name: "the utility stops reading package.json",
290
274
  harness: HARNESS,
291
275
  expect:
292
276
  "🔴 THE UTILITY READS THE DECLARATION FROM package.json — otherwise `paperlint init` sets up something `paperlint lint` cannot see",
293
277
  disables:
294
278
  "the link between the install command and the check command. `init` writes one " +
295
- "declaration into package.json, while `lint` looks for it in rpp.json — right after " +
279
+ "declaration into package.json, while `lint` looks for it somewhere else — right after " +
296
280
  'install the run answers "nothing to lint" over a corpus that is right there',
297
281
  edits: [
298
282
  [
299
283
  CLI,
300
- ' const pkg = join(dir, PKG_NAME);\n if (existsSync(pkg) && declaresSettings(pkg))\n return { path: pkg, kind: "package.json" };',
301
- " const pkg = join(dir, PKG_NAME);",
284
+ " if (existsSync(pkg) && declaresSettings(pkg)) return pkg;",
285
+ " if (false) return pkg;",
302
286
  ],
303
287
  ],
304
288
  },
305
- {
306
- name: "a deprecated carrier gets read silently",
307
- harness: HARNESS,
308
- expect: "🔴 but a deprecated carrier is NAMED, not just silently read",
309
- disables:
310
- "the warning that settings live where the hooks do not look. The run is green, one " +
311
- "directory is linted, a different one is guarded — and both states look the same",
312
- edits: [[CLI, ' if (decl.kind === "rpp.json")', " if (false)"]],
313
- },
314
289
  {
315
290
  name: "consumer data stops reaching the rule",
316
291
  harness: HARNESS,