paperlint 2.1.0 → 3.0.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 (191) hide show
  1. package/CLAUDE.md +6 -1
  2. package/README.md +233 -224
  3. package/action.yml +4 -6
  4. package/dist/adapters/references/index.d.ts +13 -0
  5. package/dist/adapters/references/index.d.ts.map +1 -0
  6. package/dist/adapters/references/index.js +55 -0
  7. package/dist/adapters/references/index.js.map +1 -0
  8. package/dist/adapters/references/reach.io.d.ts +3 -0
  9. package/dist/adapters/references/reach.io.d.ts.map +1 -0
  10. package/dist/adapters/references/reach.io.js +14 -0
  11. package/dist/adapters/references/reach.io.js.map +1 -0
  12. package/dist/build.d.ts +15 -0
  13. package/dist/build.d.ts.map +1 -1
  14. package/dist/build.js +38 -4
  15. package/dist/build.js.map +1 -1
  16. package/dist/cli.d.ts +28 -25
  17. package/dist/cli.d.ts.map +1 -1
  18. package/dist/cli.js +277 -265
  19. package/dist/cli.js.map +1 -1
  20. package/dist/doctor.d.ts.map +1 -1
  21. package/dist/doctor.js +40 -72
  22. package/dist/doctor.js.map +1 -1
  23. package/dist/facts-file.d.ts +2 -2
  24. package/dist/facts-file.js +2 -2
  25. package/dist/hooks-settings.d.ts +0 -21
  26. package/dist/hooks-settings.d.ts.map +1 -1
  27. package/dist/hooks-settings.js +17 -135
  28. package/dist/hooks-settings.js.map +1 -1
  29. package/dist/init.d.ts +39 -29
  30. package/dist/init.d.ts.map +1 -1
  31. package/dist/init.js +143 -172
  32. package/dist/init.js.map +1 -1
  33. package/dist/link-skills.d.ts +1 -1
  34. package/dist/link-skills.d.ts.map +1 -1
  35. package/dist/link-skills.js +11 -29
  36. package/dist/link-skills.js.map +1 -1
  37. package/dist/new-paper.d.ts.map +1 -1
  38. package/dist/new-paper.js +2 -13
  39. package/dist/new-paper.js.map +1 -1
  40. package/dist/paper-files.d.ts +63 -0
  41. package/dist/paper-files.d.ts.map +1 -0
  42. package/dist/paper-files.js +69 -0
  43. package/dist/paper-files.js.map +1 -0
  44. package/dist/paper-settings.d.ts +13 -41
  45. package/dist/paper-settings.d.ts.map +1 -1
  46. package/dist/paper-settings.js +71 -71
  47. package/dist/paper-settings.js.map +1 -1
  48. package/dist/ports/check-references.d.ts +31 -0
  49. package/dist/ports/check-references.d.ts.map +1 -0
  50. package/dist/ports/check-references.js +7 -0
  51. package/dist/ports/check-references.js.map +1 -0
  52. package/dist/presets.d.ts +1 -1
  53. package/dist/presets.d.ts.map +1 -1
  54. package/dist/presets.js +4 -6
  55. package/dist/presets.js.map +1 -1
  56. package/dist/reference-rules.d.ts +47 -0
  57. package/dist/reference-rules.d.ts.map +1 -0
  58. package/dist/reference-rules.js +156 -0
  59. package/dist/reference-rules.js.map +1 -0
  60. package/dist/references.d.ts +36 -0
  61. package/dist/references.d.ts.map +1 -0
  62. package/dist/references.js +91 -0
  63. package/dist/references.js.map +1 -0
  64. package/dist/rules-config.d.ts +5 -3
  65. package/dist/rules-config.d.ts.map +1 -1
  66. package/dist/rules-config.js +15 -6
  67. package/dist/rules-config.js.map +1 -1
  68. package/dist/structure.js +3 -3
  69. package/dist/structure.js.map +1 -1
  70. package/dist/tex-requirements.js +2 -2
  71. package/dist/tex-requirements.js.map +1 -1
  72. package/dist/types.d.ts +20 -18
  73. package/dist/types.d.ts.map +1 -1
  74. package/dist/venue-rules.d.ts.map +1 -1
  75. package/dist/venue-rules.js +11 -9
  76. package/dist/venue-rules.js.map +1 -1
  77. package/docs/configuration.md +119 -104
  78. package/docs/e2e.md +1 -1
  79. package/docs/install.md +25 -40
  80. package/docs/optional-rules.md +5 -4
  81. package/docs/rules.md +60 -23
  82. package/eslint-rules/bib-reachable-entry.mjs +75 -0
  83. package/eslint-rules/latex-language.mjs +29 -1
  84. package/eslint-rules/paper-stages.harness.mjs +1 -117
  85. package/eslint-rules/paper-stages.mjs +0 -120
  86. package/eslint-rules/paper-stages.mutations.mjs +0 -76
  87. package/eslint-rules/paper-typography.mjs +267 -192
  88. package/eslint-rules/paper-typography.test.mjs +297 -0
  89. package/eslint-rules/papers.harness.mjs +8 -12
  90. package/eslint-rules/papers.mjs +17 -31
  91. package/eslint-rules/pdf-last-page-balance.mjs +4 -7
  92. package/eslint-rules/review-frontmatter.mjs +118 -0
  93. package/eslint-rules/review-frontmatter.schema.json +36 -0
  94. package/eslint-rules/review-frontmatter.test.mjs +118 -0
  95. package/eslint-rules/sibling-frontmatter.mjs +19 -0
  96. package/eslint-rules/sibling-frontmatter.schema.json +14 -0
  97. package/eslint.config.mjs +0 -15
  98. package/fixtures/real-markdown-paper/baseline.json +6 -7
  99. package/hooks/hooks.harness.mjs +39 -98
  100. package/hooks/hooks.mutations.mjs +6 -6
  101. package/hooks/paper-edit-guard.hook.d.mts +2 -3
  102. package/hooks/paper-edit-guard.hook.mjs +31 -42
  103. package/hooks/paper-skills-nudge.hook.mjs +21 -19
  104. package/hooks/paper-status-gates.hook.mjs +21 -19
  105. package/hooks/paper-status-gates.sh +8 -13
  106. package/lib/paper-config.d.mts +15 -25
  107. package/lib/paper-config.harness.mjs +18 -35
  108. package/lib/paper-config.mjs +83 -105
  109. package/lib/paper-config.mutations.mjs +2 -2
  110. package/lib/skill-checks.mjs +1 -1
  111. package/lib/skill-trigger-cases.harness.mjs +9 -8
  112. package/lib/skill-trigger-cases.mjs +7 -15
  113. package/package.json +1 -1
  114. package/scripts/eslint-report-guard.mjs +1 -1
  115. package/scripts/harness-api.frozen.json +0 -3
  116. package/scripts/mutation-batteries.frozen.json +4 -13
  117. package/skills/analyze-sibling-paper/SKILL.md +16 -12
  118. package/skills/analyze-sibling-paper/SKILL.md.spec.ts +15 -11
  119. package/skills/cold-read-diff/SKILL.md +20 -1
  120. package/skills/cold-read-diff/SKILL.md.spec.ts +19 -0
  121. package/skills/grade-paper-writing/SKILL.md +20 -1
  122. package/skills/grade-paper-writing/SKILL.md.spec.ts +19 -0
  123. package/skills/paper-pipeline/scripts/consumer.d.mts +0 -1
  124. package/skills/paper-pipeline/scripts/consumer.harness.mjs +8 -12
  125. package/skills/paper-pipeline/scripts/consumer.mjs +19 -33
  126. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +6 -4
  127. package/skills/paper-pipeline/scripts/run-mechanical.mjs +2 -11
  128. package/skills/plan-paper-timeline/SKILL.md +2 -2
  129. package/skills/plan-paper-timeline/SKILL.md.spec.ts +1 -1
  130. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +1 -1
  131. package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +2 -2
  132. package/skills/render-paper/check-render.sh +2 -3
  133. package/skills/render-paper/render-paper.harness.mjs +2 -2
  134. package/skills/verify-citations/SKILL.md +10 -1
  135. package/skills/verify-citations/SKILL.md.spec.ts +9 -0
  136. package/skills/verify-citations/scripts/bib-authors.mjs +48 -26
  137. package/src/adapters/references/index.ts +102 -0
  138. package/src/adapters/references/reach.io.ts +12 -0
  139. package/src/build.harness.mjs +8 -4
  140. package/src/build.ts +62 -6
  141. package/src/cli.harness.mjs +147 -292
  142. package/src/cli.mutations.mjs +35 -44
  143. package/src/cli.ts +344 -304
  144. package/src/doctor.harness.mjs +41 -84
  145. package/src/doctor.mutations.mjs +9 -9
  146. package/src/doctor.ts +44 -89
  147. package/src/facts-file.test.ts +0 -7
  148. package/src/facts-file.ts +2 -2
  149. package/src/hooks-settings.harness.mjs +7 -92
  150. package/src/hooks-settings.mutations.mjs +2 -17
  151. package/src/hooks-settings.ts +18 -162
  152. package/src/init.test.ts +206 -0
  153. package/src/init.ts +186 -230
  154. package/src/link-skills.harness.mjs +1 -38
  155. package/src/link-skills.mutations.mjs +9 -5
  156. package/src/link-skills.ts +10 -34
  157. package/src/lint-fix.test.ts +120 -0
  158. package/src/new-paper.test.ts +1 -13
  159. package/src/new-paper.ts +2 -17
  160. package/src/paper-files.test.ts +290 -0
  161. package/src/paper-files.ts +144 -0
  162. package/src/paper-settings-commands.test.ts +84 -121
  163. package/src/paper-settings.test.ts +93 -64
  164. package/src/paper-settings.ts +99 -97
  165. package/src/ports/check-references.ts +28 -0
  166. package/src/presets.ts +5 -15
  167. package/src/reference-rules.ts +229 -0
  168. package/src/references.test.ts +209 -0
  169. package/src/references.ts +138 -0
  170. package/src/rules-config.harness.mjs +24 -8
  171. package/src/rules-config.ts +19 -7
  172. package/src/structure.harness.mjs +3 -1
  173. package/src/structure.ts +3 -3
  174. package/src/tex-requirements.ts +2 -2
  175. package/src/types.ts +30 -18
  176. package/src/venue-rules.test.ts +20 -11
  177. package/src/venue-rules.ts +11 -13
  178. package/eslint-rules/doc-fields.harness.mjs +0 -336
  179. package/eslint-rules/doc-fields.mjs +0 -186
  180. package/eslint-rules/doc-fields.mutations.mjs +0 -96
  181. package/eslint-rules/paper-typography.harness.mjs +0 -291
  182. package/eslint-rules/paper-typography.mutations.mjs +0 -131
  183. package/eslint-rules/review-findings-cause.harness.mjs +0 -228
  184. package/eslint-rules/review-findings-cause.mjs +0 -135
  185. package/eslint-rules/review-findings-cause.mutations.mjs +0 -72
  186. package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +0 -16
  187. package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +0 -17
  188. package/fixtures/review-findings-cause/clean.md +0 -17
  189. package/fixtures/review-findings-cause/defect.md +0 -14
  190. package/fixtures/review-findings-cause/old-debt.md +0 -14
  191. package/fixtures/review-findings-cause/quiet-in-fence.md +0 -16
@@ -24,18 +24,16 @@ import {
24
24
  realpathSync,
25
25
  symlinkSync,
26
26
  readdirSync,
27
+ renameSync,
27
28
  } from "node:fs";
28
29
  import { spawnSync } from "node:child_process";
29
30
  import { tmpdir } from "node:os";
30
31
  import { join, dirname } from "node:path";
31
32
  import { fileURLToPath } from "node:url";
32
- import {
33
- OLD_PAPERS_DIR_FIELD,
34
- PAPERS_DIR_FIELD,
35
- } from "../lib/paper-config.mjs";
33
+ import { PAPERS_DIR_FIELD, findProjectRoot } from "../lib/paper-config.mjs";
36
34
 
37
35
  const HERE = dirname(fileURLToPath(import.meta.url));
38
- const { run, parseArgs, buildConfig, nextSteps, findConfig, toPaths, runHook } =
36
+ const { run, parseArgs, buildConfig, nextSteps, toPaths, runHook } =
39
37
  await import(join(HERE, "cli.ts"));
40
38
  const { init, choosePapers, offerWorkflow, missingPrograms, WORKFLOW_PATH } =
41
39
  await import(join(HERE, "init.ts"));
@@ -133,19 +131,16 @@ check(
133
131
 
134
132
  // ── config is assembled, and consumer data gets through ────────────────────────────────────
135
133
  {
136
- const cfg = buildConfig(
137
- { typographyDebt: { x: { sectionSign: 3 } }, authorListCommand: "run-me" },
138
- null,
139
- );
134
+ const cfg = buildConfig({}, null);
140
135
  // Blocks with `files` — the global `ignores` block below is not a rule block.
141
136
  const ruleBlocks = (c) => c.filter((b) => Array.isArray(b.files));
142
137
  check(
143
138
  "without a LaTeX language the config still gets built — a corpus with no .tex is not a reason to refuse",
144
- Array.isArray(cfg) && ruleBlocks(cfg).length === 3,
139
+ Array.isArray(cfg) && ruleBlocks(cfg).length === 4,
145
140
  );
146
141
  check(
147
- "with a LaTeX language a fourth block is added",
148
- ruleBlocks(buildConfig({}, {})).length === 4,
142
+ "with a LaTeX language a fifth block is added",
143
+ ruleBlocks(buildConfig({}, {})).length === 5,
149
144
  );
150
145
  check(
151
146
  "🔴 the project's paper TEMPLATE directory is ignored — flat config does not skip dot-directories",
@@ -156,12 +151,13 @@ check(
156
151
  b.ignores.includes("**/.template/"),
157
152
  ),
158
153
  );
159
- const status = ruleBlocks(cfg).find((c) =>
160
- c.files.some((f) => f.includes("PIPELINE-STATUS")),
154
+ const siblings = ruleBlocks(cfg).find((c) =>
155
+ c.files.some((f) => f.includes("siblings")),
161
156
  );
162
157
  check(
163
- "the command from options gets through to the rule",
164
- status.rules["paper/author-list"][1].command === "run-me",
158
+ "sibling cards are checked, and the siblings index is not a card",
159
+ siblings.rules["sibling/frontmatter"] === "warn" &&
160
+ siblings.ignores.some((g) => g.endsWith("siblings/README.md")),
165
161
  );
166
162
  }
167
163
 
@@ -175,12 +171,10 @@ check(
175
171
  }
176
172
  // ── ONE DECLARATION, READ BY THE SAME THING THAT WRITES IT ─────────────────────────────
177
173
  //
178
- // 🔴 WITHOUT THIS BLOCK THE REWRITTEN `init` WOULD PRODUCE A BROKEN INSTALL. It writes one
179
- // declaration — into `package.json`, the file the hooks know how to read (a hook does not
180
- // import code and cannot walk up the tree; it can only read a path it is able to name). The
181
- // utility, though, read ONLY its own config file, so right after `paperlint init` its `lint` would say
182
- // "nothing to lint". I.e. the install command and the check command would be looking at
183
- // different files — exactly the defect it exists to close, just from the other side.
174
+ // 🔴 WITHOUT THIS BLOCK `init` COULD PRODUCE A BROKEN INSTALL. It writes one declaration, into
175
+ // the root `paperlint.json` — the file the hooks read at `$CLAUDE_PROJECT_DIR`. If the utility
176
+ // read another file, the install command and the check command would be looking at different
177
+ // files — exactly the defect it exists to close, just from the other side.
184
178
  {
185
179
  const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-carrier-")));
186
180
  try {
@@ -198,129 +192,47 @@ check(
198
192
  );
199
193
  writeFileSync(
200
194
  join(root, "package.json"),
201
- JSON.stringify(
202
- {
203
- name: "c",
204
- version: "1.0.0",
205
- paperlint: { [PAPERS_DIR_FIELD]: "papers" },
206
- },
207
- null,
208
- 2,
209
- ),
210
- );
211
-
212
- const r = await cli(["lint"], root);
213
- check(
214
- "🔴 THE UTILITY READS THE DECLARATION FROM package.json — otherwise `paperlint init` sets up something `paperlint lint` cannot see",
215
- r.code === 0 && /no findings/.test(r.out),
216
- );
217
- check(
218
- "and NAMES the carrier out loud — swapping settings is never silent",
219
- /config: package\.json/.test(r.out),
220
- );
221
- check(
222
- "package.json carries no deprecation notice — it is not the one that is deprecated",
223
- !/is deprecated/.test(r.out),
195
+ JSON.stringify({ name: "c", version: "1.0.0" }),
224
196
  );
225
197
 
226
- // Guards: settings under the key's old name (before 2.0.0) keep working, and say so.
227
- writeFileSync(
228
- join(root, "package.json"),
229
- JSON.stringify({
230
- name: "c",
231
- version: "1.0.0",
232
- "research-paper-pipeline": { [PAPERS_DIR_FIELD]: "papers" },
233
- }),
234
- );
235
- const legacy = await cli(["lint"], root);
198
+ // No paperlint.json at all: every default, papers in `papers/`, and no config line.
199
+ const bare = await cli(["lint"], root);
236
200
  check(
237
- "🔴 the OLD key is still read — the same clean run — and a deprecation line names the new one",
238
- legacy.code === 0 &&
239
- /no findings/.test(legacy.out) &&
240
- /"research-paper-pipeline" in package\.json is the old name .* rename it to "paperlint"/.test(
241
- legacy.out,
242
- ),
243
- );
244
- // Guards: both keys, different contents — refused, since there is no telling which is meant.
245
- writeFileSync(
246
- join(root, "package.json"),
247
- JSON.stringify({
248
- name: "c",
249
- version: "1.0.0",
250
- paperlint: { [PAPERS_DIR_FIELD]: "papers" },
251
- "research-paper-pipeline": { [PAPERS_DIR_FIELD]: "elsewhere" },
252
- }),
201
+ "🔴 NO paperlint.json IS A VALID PROJECT — the default papers directory is linted",
202
+ bare.code === 0 && /no findings/.test(bare.out),
253
203
  );
254
- const both = await cli(["lint"], root);
255
204
  check(
256
- "🔴 both keys with different contents — refused (exit 2), naming both",
257
- both.code === 2 &&
258
- /has both "paperlint" and "research-paper-pipeline", and they differ/.test(
259
- both.out,
260
- ),
205
+ "and no config line is printed when there is no file to name",
206
+ !/config:/.test(bare.out),
261
207
  );
262
208
 
263
- // The key is present, the papers-directory field inside it is not: this is not an "empty config" but an
264
- // unfinished one, and the failure must name the EXACT shape that needs adding.
265
- writeFileSync(
266
- join(root, "package.json"),
267
- JSON.stringify({ name: "c", version: "1.0.0", paperlint: {} }, null, 2),
268
- );
269
- const noPapers = await cli(["lint"], root);
209
+ // An empty root file is a file with every default, not an unfinished one.
210
+ writeFileSync(join(root, "paperlint.json"), "{}");
211
+ const empty = await cli(["lint"], root);
270
212
  check(
271
- `a key with no \`${PAPERS_DIR_FIELD}\` — a failure, and the shape is shown INSIDE package.json`,
272
- noPapers.code === 2 &&
273
- noPapers.out.includes(`must declare \`${PAPERS_DIR_FIELD}\``) &&
274
- noPapers.out.includes(
275
- `"paperlint": { "${PAPERS_DIR_FIELD}": "papers" }`,
276
- ),
213
+ `an empty paperlint.json — no \`${PAPERS_DIR_FIELD}\`, so the default`,
214
+ empty.code === 0 && /no findings/.test(empty.out),
277
215
  );
278
216
 
279
- // The field's OLD name is refused with one clear sentence, not read as a fallback. The
280
- // new name sits beside it here on purpose: even then the leftover old name is an error.
217
+ // 🔴 THE DECLARED DIRECTORY IS NOT THE DEFAULT: with `papers` declared, a CLI that ignored
218
+ // the file would lint the same tree and pass for the wrong reason.
219
+ renameSync(join(root, "papers"), join(root, "writing"));
281
220
  writeFileSync(
282
- join(root, "package.json"),
283
- JSON.stringify({
284
- name: "c",
285
- version: "1.0.0",
286
- paperlint: {
287
- [OLD_PAPERS_DIR_FIELD]: "papers",
288
- [PAPERS_DIR_FIELD]: "papers",
289
- },
290
- }),
221
+ join(root, "paperlint.json"),
222
+ JSON.stringify({ [PAPERS_DIR_FIELD]: "writing" }),
291
223
  );
292
- const oldName = await cli(["lint"], root);
224
+ const r = await cli(["lint"], root);
293
225
  check(
294
- `the old field name "${OLD_PAPERS_DIR_FIELD}" fails with a message that names the new one`,
295
- oldName.code === 2 &&
296
- oldName.out.includes(
297
- `"${OLD_PAPERS_DIR_FIELD}" was renamed to "${PAPERS_DIR_FIELD}" in package.json → "paperlint"`,
298
- ),
299
- );
300
- writeFileSync(
301
- join(root, "package.json"),
302
- JSON.stringify({
303
- name: "c",
304
- version: "1.0.0",
305
- paperlint: { [OLD_PAPERS_DIR_FIELD]: "papers" },
306
- }),
226
+ "🔴 THE UTILITY READS THE DECLARATION FROM paperlint.json — the file `paperlint init` writes and the hooks read",
227
+ r.code === 0 && /no findings/.test(r.out),
307
228
  );
308
229
  check(
309
- "the old field name alone fails the same way — it is never used as the papers directory",
310
- (await cli(["lint"], root)).code === 2,
230
+ "and NAMES the carrier out loud — swapping settings is never silent",
231
+ /config: paperlint\.json/.test(r.out),
311
232
  );
312
233
 
313
- // 🔴 A package.json WITHOUT the key does not stop the walk upward. Otherwise the search
314
- // would end at the first project going up the tree — and every project has a package.json —
315
- // and would never find anything, ever.
316
- writeFileSync(
317
- join(root, "package.json"),
318
- JSON.stringify({
319
- name: "c",
320
- version: "1.0.0",
321
- paperlint: { [PAPERS_DIR_FIELD]: "papers" },
322
- }),
323
- );
234
+ // 🔴 A package.json does not stop the walk upward while a root paperlint.json sits above:
235
+ // a nested package (a tools/ workspace) still belongs to the project.
324
236
  const inner = join(root, "tools");
325
237
  mkdirSync(inner, { recursive: true });
326
238
  writeFileSync(
@@ -328,13 +240,13 @@ check(
328
240
  JSON.stringify({ name: "tools", version: "1.0.0" }),
329
241
  );
330
242
  check(
331
- "a keyless package.json does not intercept the search — the one above it is found",
332
- findConfig(inner) === join(root, "package.json"),
243
+ "a nested package.json does not intercept the search — the root paperlint.json above it is found",
244
+ findProjectRoot(inner) === root,
333
245
  );
334
246
  const viaInner = await cli(["lint"], inner);
335
247
  check(
336
248
  "and `lint` run from under it uses that declaration",
337
- viaInner.code === 0 && /config: \.\.\/package\.json/.test(viaInner.out),
249
+ viaInner.code === 0 && /config: \.\.\/paperlint\.json/.test(viaInner.out),
338
250
  );
339
251
  } finally {
340
252
  rmSync(root, { recursive: true, force: true });
@@ -356,7 +268,11 @@ check(
356
268
  const workRoot = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-init-")));
357
269
  const project = (
358
270
  name,
359
- { pkg = { name: "consumer", version: "1.0.0" }, papers = [] } = {},
271
+ {
272
+ pkg = { name: "consumer", version: "1.0.0" },
273
+ papers = [],
274
+ settings = null,
275
+ } = {},
360
276
  ) => {
361
277
  const dir = join(workRoot, name);
362
278
  mkdirSync(dir, { recursive: true });
@@ -372,6 +288,11 @@ check(
372
288
  join(dir, "package.json"),
373
289
  JSON.stringify(pkg, null, 2) + "\n",
374
290
  );
291
+ if (settings)
292
+ writeFileSync(
293
+ join(dir, "paperlint.json"),
294
+ JSON.stringify(settings, null, 2) + "\n",
295
+ );
375
296
  return dir;
376
297
  };
377
298
  const say = () => {
@@ -382,8 +303,11 @@ check(
382
303
  text: () => lines.join("\n"),
383
304
  };
384
305
  };
306
+ /** The root paperlint.json init left behind, or undefined when it wrote none. */
385
307
  const declared = (dir) =>
386
- JSON.parse(readFileSync(join(dir, "package.json"), "utf8"))["paperlint"];
308
+ existsSync(join(dir, "paperlint.json"))
309
+ ? JSON.parse(readFileSync(join(dir, "paperlint.json"), "utf8"))
310
+ : undefined;
387
311
  // No check should depend on what is installed ON THIS MACHINE: `command -v` is faked,
388
312
  // otherwise "no external programs" would read as a finding about init.
389
313
  const haveAll = () => ({ status: 0 });
@@ -400,8 +324,15 @@ check(
400
324
  interactive: false,
401
325
  run: haveAll,
402
326
  });
327
+ // 🔴 THE REPORT IS CHECKED FIRST: a directory that was GUESSED lands on the default,
328
+ // and the default is not written — so the file check below would fail for a guess too,
329
+ // and could not tell "not measured" from "not written".
330
+ check(
331
+ "and it says HOW it was measured — otherwise a guess reads as a fact",
332
+ /measured: its subdirectories carry/.test(out.text()),
333
+ );
403
334
  check(
404
- "🔴 THE DECLARATION SHOWS UP IN package.json — the file the hooks read",
335
+ "🔴 THE DECLARATION SHOWS UP IN paperlint.json — the file the hooks read",
405
336
  declared(dir) !== undefined &&
406
337
  typeof declared(dir)[PAPERS_DIR_FIELD] === "string",
407
338
  );
@@ -409,10 +340,6 @@ check(
409
340
  "🔴 and its value is MEASURED, not taken from the `papers` default",
410
341
  declared(dir)[PAPERS_DIR_FIELD] === "writing/drafts",
411
342
  );
412
- check(
413
- "and it says HOW it was measured — otherwise a guess reads as a fact",
414
- /measured: its subdirectories carry/.test(out.text()),
415
- );
416
343
  check(
417
344
  "init ends with doctor's report: the install vouches for its OWN state",
418
345
  /paperlint doctor — what is wired/.test(out.text()),
@@ -434,13 +361,14 @@ check(
434
361
  run: haveAll,
435
362
  });
436
363
  check(
437
- "nothing to measure — the documented default is taken",
438
- declared(dir)[PAPERS_DIR_FIELD] === "papers",
364
+ "🔴 nothing to measure — the default is taken, and NO paperlint.json is written for it",
365
+ declared(dir) === undefined,
439
366
  );
440
367
  check(
441
- "🔴 and it is MARKED as a guess, not presented as a measurement",
442
- /A GUESS/.test(out.text()) &&
443
- /Nothing here looks like a papers directory/.test(out.text()),
368
+ "and it is called the default, not presented as a measurement",
369
+ /papers — the default\. Nothing here looks like a papers directory/.test(
370
+ out.text(),
371
+ ),
444
372
  );
445
373
  // Guards: a fresh project with no papers anywhere is not a broken install. The guard has
446
374
  // nothing to protect yet, and `init --yes` (an agent, CI) must finish green with a next step.
@@ -453,80 +381,13 @@ check(
453
381
  );
454
382
  }
455
383
 
456
- // ── 2½. SETTINGS UNDER THE OLD KEY ARE MOVED TO THE NEW ONE ───────────────────────
457
- {
458
- const dir = project("old-key", {
459
- pkg: {
460
- name: "consumer",
461
- "research-paper-pipeline": {
462
- [PAPERS_DIR_FIELD]: "writing",
463
- minFindings: 3,
464
- },
465
- version: "1.0.0",
466
- },
467
- papers: ["writing"],
468
- });
469
- const out = say();
470
- await init(dir, {
471
- log: out.log,
472
- err: out.log,
473
- interactive: false,
474
- run: haveAll,
475
- });
476
- const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
477
- check(
478
- "🔴 init moves the old key's settings to the new key, whole, and removes the old key",
479
- JSON.stringify(pkg.paperlint) ===
480
- JSON.stringify({ [PAPERS_DIR_FIELD]: "writing", minFindings: 3 }) &&
481
- !("research-paper-pipeline" in pkg),
482
- );
483
- check(
484
- "in the same position in the file, so the diff is a one-word rename",
485
- Object.keys(pkg).join() === "name,paperlint,version",
486
- );
487
- check(
488
- "and it says so",
489
- /moved the settings from "research-paper-pipeline" \(the old key\) to "paperlint"/.test(
490
- out.text(),
491
- ),
492
- );
493
- }
494
- {
495
- const dir = project("both-keys", {
496
- pkg: {
497
- name: "consumer",
498
- paperlint: { [PAPERS_DIR_FIELD]: "writing" },
499
- "research-paper-pipeline": { [PAPERS_DIR_FIELD]: "other" },
500
- },
501
- papers: ["writing"],
502
- });
503
- const out = say();
504
- const code = await init(dir, {
505
- log: out.log,
506
- err: out.log,
507
- interactive: false,
508
- run: haveAll,
509
- });
510
- check(
511
- "both keys with different contents — init refuses (exit 2) and writes nothing",
512
- code === 2 &&
513
- /they differ/.test(out.text()) &&
514
- "research-paper-pipeline" in
515
- JSON.parse(readFileSync(join(dir, "package.json"), "utf8")),
516
- );
517
- }
518
-
519
384
  // ── 3. SOMEONE ELSE'S VALUE DOES NOT GET OVERWRITTEN ─────────────────────────────
520
385
  {
521
386
  const dir = project("mine", {
522
- pkg: {
523
- name: "c",
524
- version: "1.0.0",
525
- paperlint: { [PAPERS_DIR_FIELD]: "mine" },
526
- },
387
+ settings: { [PAPERS_DIR_FIELD]: "mine" },
527
388
  papers: ["writing"],
528
389
  });
529
- const before = readFileSync(join(dir, "package.json"), "utf8");
390
+ const before = readFileSync(join(dir, "paperlint.json"), "utf8");
530
391
  const out = say();
531
392
  await init(dir, {
532
393
  log: out.log,
@@ -536,7 +397,7 @@ check(
536
397
  });
537
398
  check(
538
399
  "🔴 an already-declared value stays intact byte for byte — silently replacing a setting is worse than doing nothing",
539
- readFileSync(join(dir, "package.json"), "utf8") === before,
400
+ readFileSync(join(dir, "paperlint.json"), "utf8") === before,
540
401
  );
541
402
  check(
542
403
  "and it says so out loud, rather than skipping it",
@@ -546,24 +407,22 @@ check(
546
407
  );
547
408
  }
548
409
 
549
- // ── 4. NOWHERE TO WRITE — A FAILURE WITH A REMEDY ─────────────────────────────
410
+ // ── 4. NO package.json — the settings have their own file, so nothing is missing ─
550
411
  {
551
412
  const dir = project("nopkg", { pkg: null, papers: ["writing"] });
552
413
  const out = say();
553
- const code = await init(dir, {
414
+ await init(dir, {
554
415
  log: out.log,
555
416
  err: out.log,
556
417
  interactive: false,
557
418
  run: haveAll,
558
419
  });
559
420
  check(
560
- "without package.json init FAILS and carries a remedy, not just a diagnosis",
561
- code === 2 &&
562
- /npm init -y/.test(out.text()) &&
563
- /nowhere to put the declaration/.test(out.text()),
421
+ "without package.json init still declares the measured directory, in paperlint.json",
422
+ declared(dir)?.[PAPERS_DIR_FIELD] === "writing",
564
423
  );
565
424
  check(
566
- "and creates nothing in its place",
425
+ "and creates no package.json",
567
426
  !existsSync(join(dir, "package.json")),
568
427
  );
569
428
  }
@@ -748,9 +607,15 @@ check(
748
607
  "every absence is NAMED, and the count matches the names",
749
608
  /✗ 6 of 6 missing: pdflatex, bibtex/.test(own),
750
609
  );
610
+ // The programs section alone: the TeX Live step above it and the "next:" lines below it
611
+ // name `npx paperlint toolchain` too, and would answer for a section that lost its remedy.
612
+ const programs = own.slice(
613
+ own.indexOf("external programs"),
614
+ own.indexOf("next:"),
615
+ );
751
616
  check(
752
617
  "🔴 and it carries the INSTALL COMMAND — a remedy, not just a diagnosis",
753
- /npx paperlint toolchain/.test(own),
618
+ /npx paperlint toolchain/.test(programs),
754
619
  );
755
620
  check(
756
621
  "and it says outright that nothing is installed on the user's behalf",
@@ -847,18 +712,17 @@ check(
847
712
  );
848
713
  }
849
714
  {
850
- // A "." default would give a green run over whatever happens to be lying around — the same
851
- // contract as the action, and the same reason. An empty directory with no config is exactly
852
- // that case.
715
+ // A "." default would give a green run over whatever happens to be lying around. The default
716
+ // is `papers/`, and an empty directory with no config has none: a refusal naming BOTH ways out.
853
717
  const bare = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-bare-")));
854
718
  try {
855
719
  const r = await cli(["lint"], bare);
856
720
  check(
857
- "`lint` with NO path AND no config refuses and names BOTH ways out",
721
+ "`lint` with NO path, no config and no papers/ refuses and names BOTH ways out",
858
722
  r.code === 2 &&
859
- /nothing to lint/.test(r.out) &&
860
- /paperlint init/.test(r.out) &&
861
- /paperlint lint papers/.test(r.out),
723
+ /no papers in papers\//.test(r.out) &&
724
+ /paperlint new <name>/.test(r.out) &&
725
+ /set "papersDir" in paperlint\.json/.test(r.out),
862
726
  );
863
727
  } finally {
864
728
  rmSync(bare, { recursive: true, force: true });
@@ -968,14 +832,14 @@ check(
968
832
  `---\nstages:\n - stage: submitted\n date: 2026-07-22\n pdf: versions/2026-07-22-submitted.pdf\n bytes: ${bytes}\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`;
969
833
  writeFileSync(join(paper, "PIPELINE-STATUS.md"), status(100));
970
834
 
971
- // findConfig — kept separate from the run so the failure is distinguishable
835
+ // findProjectRoot — kept separate from the run so the failure is distinguishable
972
836
  writeFileSync(
973
- join(root, "package.json"),
974
- JSON.stringify({ paperlint: { [PAPERS_DIR_FIELD]: "papers" } }),
837
+ join(root, "paperlint.json"),
838
+ JSON.stringify({ [PAPERS_DIR_FIELD]: "papers" }),
975
839
  );
976
840
  check(
977
- "findConfig walks up from a subdirectory and finds the file at the root",
978
- findConfig(paper) === join(root, "package.json"),
841
+ "findProjectRoot walks up from inside a paper and finds the root file, not the paper's",
842
+ findProjectRoot(paper) === root,
979
843
  );
980
844
  // There can be no tautology here: a neighboring tree must NOT pick up our file. Asserting
981
845
  // `=== null` against a live filesystem is not safe — someone else's package.json may sit further
@@ -986,7 +850,7 @@ check(
986
850
  );
987
851
  check(
988
852
  "the config does NOT leak into a neighboring tree — the search goes up, not sideways",
989
- findConfig(sibling) !== join(root, "package.json"),
853
+ findProjectRoot(sibling) !== root,
990
854
  );
991
855
  rmSync(sibling, { recursive: true, force: true });
992
856
 
@@ -997,7 +861,7 @@ check(
997
861
  );
998
862
  check(
999
863
  "and the found file is NAMED out loud — swapping settings silently is not acceptable",
1000
- /config: package\.json/.test(found.out),
864
+ /config: paperlint\.json/.test(found.out),
1001
865
  );
1002
866
 
1003
867
  // 🔴 THE RESOLUTION DISCRIMINATOR. Run FROM the paper's directory: same config, and
@@ -1028,26 +892,23 @@ check(
1028
892
  /nothing was linted under elsewhere/.test(override.out),
1029
893
  );
1030
894
 
1031
- // 🔴 THE PAPERS DIRECTORY — A REQUIRED FIELD. A config without it is not an "empty config" but an
1032
- // unfinished one: silently falling back to "." means running the rules over the whole checkout.
895
+ // The papers directory is OPTIONAL: a config without it takes `papers`, never ".".
1033
896
  writeFileSync(
1034
- join(root, "package.json"),
1035
- JSON.stringify({ paperlint: { minFindings: 3 } }),
897
+ join(root, "paperlint.json"),
898
+ JSON.stringify({ structure: false }),
1036
899
  );
1037
900
  const noPapers = await cli(["lint"], root);
1038
901
  check(
1039
- "a config WITHOUT the papers-directory field — a failure, and the field is named by name",
1040
- noPapers.code === 2 &&
1041
- noPapers.out.includes(`must declare \`${PAPERS_DIR_FIELD}\``) &&
1042
- /cannot guess/.test(noPapers.out),
902
+ "a config WITHOUT the papers-directory field lints the default `papers/`",
903
+ noPapers.code === 0 && /no findings/.test(noPapers.out),
1043
904
  );
1044
905
  check(
1045
- 'an empty string as the papers directory counts as absent, not as the directory ""',
906
+ 'an empty string as the papers directory is refused, not read as the directory ""',
1046
907
  (
1047
908
  await (async () => {
1048
909
  writeFileSync(
1049
- join(root, "package.json"),
1050
- JSON.stringify({ paperlint: { [PAPERS_DIR_FIELD]: "" } }),
910
+ join(root, "paperlint.json"),
911
+ JSON.stringify({ [PAPERS_DIR_FIELD]: "" }),
1051
912
  );
1052
913
  return await cli(["lint"], root);
1053
914
  })()
@@ -1073,7 +934,7 @@ check(
1073
934
  join(paper, "versions", "2026-07-22-submitted.pdf"),
1074
935
  "x".repeat(100),
1075
936
  );
1076
- // Three § signs — the `paper/typography` rule, warn level and only warn.
937
+ // Three § signs — `paper/section-word`, warn level and only warn.
1077
938
  writeFileSync(
1078
939
  join(paper, "paper.md"),
1079
940
  "# Intro\n\nRQ1: does it hold?\n\nSee \u00a7 5 and \u00a7 6 and \u00a7 7.\n",
@@ -1083,8 +944,8 @@ check(
1083
944
  `---\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`,
1084
945
  );
1085
946
  writeFileSync(
1086
- join(root, "package.json"),
1087
- JSON.stringify({ paperlint: { [PAPERS_DIR_FIELD]: "papers" } }),
947
+ join(root, "paperlint.json"),
948
+ JSON.stringify({ [PAPERS_DIR_FIELD]: "papers" }),
1088
949
  );
1089
950
 
1090
951
  const lax = await cli(["lint"], root);
@@ -1099,7 +960,7 @@ check(
1099
960
  );
1100
961
  check(
1101
962
  'and the failure names the NUMBER and the THRESHOLD, not just "too many"',
1102
- /1 warning\(s\) exceed the --max-warnings limit of 0/.test(strict.out),
963
+ /3 warning\(s\) exceed the --max-warnings limit of 0/.test(strict.out),
1103
964
  );
1104
965
  const generous = await cli(["lint", "--max-warnings", "5"], root);
1105
966
  check(
@@ -1124,8 +985,8 @@ check(
1124
985
  // The marker is present, the scorecard is not: NOT ONE pipeline rule runs over this directory.
1125
986
  writeFileSync(join(paper, "paper.tex"), "\\documentclass{article}\n");
1126
987
  writeFileSync(
1127
- join(root, "package.json"),
1128
- JSON.stringify({ paperlint: { [PAPERS_DIR_FIELD]: "papers" } }),
988
+ join(root, "paperlint.json"),
989
+ JSON.stringify({ [PAPERS_DIR_FIELD]: "papers" }),
1129
990
  );
1130
991
 
1131
992
  const r = await cli(["lint"], root);
@@ -1158,7 +1019,7 @@ check(
1158
1019
  );
1159
1020
  check(
1160
1021
  "and the line about the found config is not lost — it went to stderr",
1161
- /config: package\.json/.test(j.stderr) && !/config:/.test(j.stdout),
1022
+ /config: paperlint\.json/.test(j.stderr) && !/config:/.test(j.stdout),
1162
1023
  );
1163
1024
  check(
1164
1025
  "`--json` gives back ONE array, where the missing-file finding sits next to the rule findings",
@@ -1233,7 +1094,7 @@ check(
1233
1094
  check(
1234
1095
  "the inside run is the reference: it has both a structure and a rule finding",
1235
1096
  findings(inside).some((f) => f.startsWith("structure/required-file")) &&
1236
- findings(inside).some((f) => f.startsWith("paper/typography")),
1097
+ findings(inside).some((f) => f.startsWith("paper/section-word")),
1237
1098
  );
1238
1099
  check(
1239
1100
  "and from outside it reports the SAME findings with the SAME exit code",
@@ -1241,44 +1102,44 @@ check(
1241
1102
  outside.code === inside.code,
1242
1103
  );
1243
1104
 
1244
- // The debt, keyed from the config's directory, covers the two `§`.
1105
+ // A `rules` block, its `files` relative to the config's directory, turns the `§` rule off
1106
+ // for this paper — and it must reach the paper from wherever the command runs.
1245
1107
  writeFileSync(
1246
- join(tree, "package.json"),
1108
+ join(tree, "paperlint.json"),
1247
1109
  JSON.stringify({
1248
- name: "x",
1249
- paperlint: {
1250
- [PAPERS_DIR_FIELD]: "papers",
1251
- typographyDebt: { "papers/p": { sectionSign: 2 } },
1252
- },
1110
+ [PAPERS_DIR_FIELD]: "papers",
1111
+ rules: [
1112
+ { files: ["papers/p/**"], rules: { "paper/section-word": "off" } },
1113
+ ],
1253
1114
  }),
1254
1115
  );
1255
1116
  const debtHonoured = (r) =>
1256
1117
  r.code !== 99 &&
1257
- !findings(r).some((f) => f.startsWith("paper/typography"));
1118
+ !findings(r).some((f) => f.startsWith("paper/section-word"));
1258
1119
  check(
1259
- "from the config's own directory the declared debt silences the `§` finding",
1120
+ "from the config's own directory the block turns the `§` finding off",
1260
1121
  debtHonoured(await cli(["lint", "--json"], tree)),
1261
1122
  );
1262
1123
  check(
1263
- "from a SUBDIRECTORY (config found by walking up) the same debt still applies",
1124
+ "from a SUBDIRECTORY (config found by walking up) the same block still applies",
1264
1125
  debtHonoured(await cli(["lint", "--json"], join(tree, "papers"))),
1265
1126
  );
1266
1127
  check(
1267
1128
  "and with `--config` from an unrelated directory it applies too",
1268
1129
  debtHonoured(
1269
1130
  await cli(
1270
- ["lint", "--json", "--config", join(tree, "package.json")],
1131
+ ["lint", "--json", "--config", join(tree, "paperlint.json")],
1271
1132
  elsewhere,
1272
1133
  ),
1273
1134
  ),
1274
1135
  );
1275
1136
  // A RELATIVE `--config` stays relative in `configPath`, while every paper path is absolute:
1276
- // without resolving it first, the common root of `.` and `/tmp/…` is `/`, the debt keys
1277
- // no longer match, and the declared debt comes back as new warnings (Codex on #45).
1137
+ // without resolving it first, the common root of `.` and `/tmp/…` is `/`, and globs written
1138
+ // relative to the config no longer match (Codex on #45).
1278
1139
  check(
1279
- "and with a RELATIVE `--config package.json` it applies too",
1140
+ "and with a RELATIVE `--config paperlint.json` it applies too",
1280
1141
  debtHonoured(
1281
- await cli(["lint", "--json", "--config", "package.json"], tree),
1142
+ await cli(["lint", "--json", "--config", "paperlint.json"], tree),
1282
1143
  ),
1283
1144
  );
1284
1145
  } finally {
@@ -1678,8 +1539,8 @@ console.log(
1678
1539
  // step, and every later step must use the kept value, not the directory init guessed.
1679
1540
  const dir = bare("declared");
1680
1541
  writeFileSync(
1681
- join(dir, "package.json"),
1682
- `{"name":"c","version":"1.0.0","paperlint":{"${PAPERS_DIR_FIELD}":"writing"}}\n`,
1542
+ join(dir, "paperlint.json"),
1543
+ `{"${PAPERS_DIR_FIELD}":"writing"}\n`,
1683
1544
  );
1684
1545
  const made = [];
1685
1546
  await init(dir, {
@@ -1743,8 +1604,8 @@ console.log(
1743
1604
  const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-template-")));
1744
1605
  try {
1745
1606
  writeFileSync(
1746
- join(root, "package.json"),
1747
- `{"paperlint":{"${PAPERS_DIR_FIELD}":"papers"}}\n`,
1607
+ join(root, "paperlint.json"),
1608
+ `{"${PAPERS_DIR_FIELD}":"papers"}\n`,
1748
1609
  );
1749
1610
  mkdirSync(join(root, "papers", "p1"), { recursive: true });
1750
1611
  writeFileSync(join(root, "papers", "p1", "paper.md"), "# P\n");
@@ -1784,14 +1645,14 @@ console.log(
1784
1645
  {
1785
1646
  const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-new-cli-")));
1786
1647
  try {
1787
- const none = await cli(["new", "demo"], root);
1648
+ const none = await cli(["new", "first"], root);
1788
1649
  check(
1789
- "without a declaration there is nowhere to put a paper — refused, with the remedy",
1790
- none.code === 2 && /Run `npx paperlint init` first/.test(none.out),
1650
+ "without a declaration the paper goes into the default `papers/`",
1651
+ none.code === 0 && existsSync(join(root, "papers", "first", "paper.tex")),
1791
1652
  );
1792
1653
  writeFileSync(
1793
- join(root, "package.json"),
1794
- `{"paperlint":{"${PAPERS_DIR_FIELD}":"writing"}}\n`,
1654
+ join(root, "paperlint.json"),
1655
+ `{"${PAPERS_DIR_FIELD}":"writing"}\n`,
1795
1656
  );
1796
1657
  const r = await cli(["new", "demo"], root);
1797
1658
  check(
@@ -1842,12 +1703,8 @@ console.log(
1842
1703
  );
1843
1704
  const settings = (extra) =>
1844
1705
  writeFileSync(
1845
- join(root, "package.json"),
1846
- JSON.stringify({
1847
- name: "c",
1848
- version: "1.0.0",
1849
- paperlint: { [PAPERS_DIR_FIELD]: "papers", ...extra },
1850
- }),
1706
+ join(root, "paperlint.json"),
1707
+ JSON.stringify({ [PAPERS_DIR_FIELD]: "papers", ...extra }),
1851
1708
  );
1852
1709
  const balanceOn = (files) => ({
1853
1710
  rules: [{ files, rules: { "pdf/last-page-balance": "error" } }],
@@ -1858,9 +1715,7 @@ console.log(
1858
1715
  check(
1859
1716
  "🔴 an unknown key is REFUSED by name — a typo must not read as 'not set'",
1860
1717
  typo.code === 2 &&
1861
- typo.out.includes(
1862
- 'package.json → "paperlint": unknown key "typographyDept"',
1863
- ),
1718
+ typo.out.includes('paperlint.json: unknown key "typographyDept"'),
1864
1719
  );
1865
1720
  settings({
1866
1721
  ledger: "x.jsonl",
@@ -1874,10 +1729,10 @@ console.log(
1874
1729
  "the keys the skill scripts read are known keys, not typos",
1875
1730
  (await cli(["lint"], root)).code !== 2,
1876
1731
  );
1877
- settings({ rules: { "pdf/last-page-balance": "error" } });
1732
+ settings({ rules: "pdf/last-page-balance" });
1878
1733
  check(
1879
- "`rules` that is not a list is refused, naming the key",
1880
- /\.rules must be a list of blocks/.test((await cli(["lint"], root)).out),
1734
+ "`rules` that is neither { id: severity } nor a list is refused, naming the key",
1735
+ /paperlint\.json → "rules" must be/.test((await cli(["lint"], root)).out),
1881
1736
  );
1882
1737
  settings({
1883
1738
  rules: [{ files: ["papers/**"], rules: { "pdf/nope": "error" } }],
@@ -1887,7 +1742,7 @@ console.log(
1887
1742
  "a rule paperlint does not ship is refused, naming the block and the rule",
1888
1743
  unknownRule.code === 2 &&
1889
1744
  unknownRule.out.includes(
1890
- '.rules[0].rules: "pdf/nope" is not a rule paperlint ships',
1745
+ 'rules[0].rules: "pdf/nope" is not a rule paperlint ships',
1891
1746
  ),
1892
1747
  );
1893
1748
  settings({
@@ -1932,7 +1787,7 @@ console.log(
1932
1787
  ),
1933
1788
  );
1934
1789
  settings({
1935
- rules: [{ files: ["papers/**"], rules: { "paper/typography": "off" } }],
1790
+ rules: [{ files: ["papers/**"], rules: { "paper/section-word": "off" } }],
1936
1791
  });
1937
1792
  check(
1938
1793
  "a built-in rule's severity can be changed the same way",