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
@@ -12,10 +12,7 @@
12
12
  * Killed by: src/doctor.mutations.mjs
13
13
  */
14
14
  import assert from "node:assert/strict";
15
- import {
16
- OLD_PAPERS_DIR_FIELD,
17
- PAPERS_DIR_FIELD,
18
- } from "../lib/paper-config.mjs";
15
+ import { PAPERS_DIR_FIELD } from "../lib/paper-config.mjs";
19
16
  import {
20
17
  mkdtempSync,
21
18
  mkdirSync,
@@ -41,8 +38,8 @@ const check = (label, cond) => {
41
38
  assert.ok(cond, label);
42
39
  };
43
40
 
44
- /** A consumer on disk: a papers directory, and maybe a declaration in package.json. */
45
- function consumer({ papersDir, pkgKey, makeDir = true }) {
41
+ /** A consumer on disk: a papers directory, and maybe a declaration in the root paperlint.json. */
42
+ function consumer({ papersDir, declared, makeDir = true }) {
46
43
  const dir = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-doctor-")));
47
44
  if (makeDir && papersDir) {
48
45
  mkdirSync(join(dir, papersDir, "some-paper"), { recursive: true });
@@ -51,9 +48,15 @@ function consumer({ papersDir, pkgKey, makeDir = true }) {
51
48
  "# s\n",
52
49
  );
53
50
  }
54
- const pkg = { name: "consumer", version: "1.0.0" };
55
- if (pkgKey !== undefined) pkg["paperlint"] = { [PAPERS_DIR_FIELD]: pkgKey };
56
- writeFileSync(join(dir, "package.json"), JSON.stringify(pkg, null, 2));
51
+ writeFileSync(
52
+ join(dir, "package.json"),
53
+ JSON.stringify({ name: "consumer", version: "1.0.0" }, null, 2),
54
+ );
55
+ if (declared !== undefined)
56
+ writeFileSync(
57
+ join(dir, "paperlint.json"),
58
+ JSON.stringify({ [PAPERS_DIR_FIELD]: declared }, null, 2),
59
+ );
57
60
  return dir;
58
61
  }
59
62
 
@@ -76,7 +79,7 @@ const runDoctor = (
76
79
 
77
80
  // ── I. A CONSISTENT INSTALL IS SILENT ───────────────────────────────────────────────────────
78
81
  {
79
- const dir = consumer({ papersDir: "papers", pkgKey: "papers" });
82
+ const dir = consumer({ papersDir: "papers", declared: "papers" });
80
83
  const r = runDoctor(dir, { cliPapers: "papers" });
81
84
  check("a consistent install — exit ZERO", r.code === 0);
82
85
  check(
@@ -86,30 +89,9 @@ const runDoctor = (
86
89
  rmSync(dir, { recursive: true, force: true });
87
90
  }
88
91
 
89
- // ── I½. THE OLD FIELD NAME IS A FAILURE, NAMED ─────────────────────────────────────────────
90
- {
91
- const dir = consumer({ papersDir: "papers", pkgKey: "papers" });
92
- writeFileSync(
93
- join(dir, "package.json"),
94
- JSON.stringify({
95
- name: "consumer",
96
- paperlint: { [OLD_PAPERS_DIR_FIELD]: "papers" },
97
- }),
98
- );
99
- const r = runDoctor(dir, { cliPapers: null });
100
- check("the old field name — exit NON-zero", r.code !== 0);
101
- check(
102
- "and doctor says what the field was renamed to",
103
- r.out.includes(
104
- `"${OLD_PAPERS_DIR_FIELD}" was renamed to "${PAPERS_DIR_FIELD}" in package.json → "paperlint"`,
105
- ),
106
- );
107
- rmSync(dir, { recursive: true, force: true });
108
- }
109
-
110
92
  // ── II. THE VERY DEFECT: AN INSTALL FOLLOWING THE DOCS ─────────────────────────────────────
111
- // The old `init` wrote its own config file and did not touch package.json; the hook reads package.json.
112
- // Measured 09-18.
93
+ // The CLI lints one directory and the hook guards another: the install that motivated doctor
94
+ // (issue #33). Here the CLI was pointed elsewhere while the hook took the default.
113
95
  {
114
96
  const dir = consumer({ papersDir: "writing/drafts" });
115
97
  const r = runDoctor(dir, { cliPapers: "writing/drafts" });
@@ -137,21 +119,31 @@ const runDoctor = (
137
119
  rmSync(dir, { recursive: true, force: true });
138
120
  }
139
121
 
140
- // ── II-bis. A MISSING DECLARATION THAT DOES NO HARM YET ────────────────────────────────────
141
- // The papers live exactly where the hook's default points. The install WORKS — by coincidence.
142
- // It must not fail here (an error-level false positive costs more than a miss), but it must
143
- // not stay silent either.
122
+ // ── II-bis. NO paperlint.json — EVERY DEFAULT, AND THAT IS A VALID PROJECT ─────────────────
123
+ // The CLI and the hooks both take `papers` when nothing is declared, so they cannot disagree.
144
124
  {
145
125
  const dir = consumer({ papersDir: "papers" });
146
126
  const r = runDoctor(dir, { cliPapers: "papers" });
147
- check("an install that works by coincidence does NOT crash", r.code === 0);
127
+ check("no paperlint.json — exit ZERO", r.code === 0);
148
128
  check(
149
- "but the missing declaration is NAMED, not skipped",
150
- /⚠ package\.json has no "paperlint"/.test(r.out),
129
+ "and it says the defaults are in play, naming the directory",
130
+ /✓ no paperlint\.json — every setting has its default; papers are in "papers"/.test(
131
+ r.out,
132
+ ),
151
133
  );
134
+ rmSync(dir, { recursive: true, force: true });
135
+ }
136
+
137
+ // ── II-quater. A paperlint.json THAT DOES NOT PARSE — the guard refuses every command ─────
138
+ {
139
+ const dir = consumer({ papersDir: "papers" });
140
+ writeFileSync(join(dir, "paperlint.json"), '{ "papersDir": <<<<<<< HEAD\n');
141
+ const r = runDoctor(dir, { cliPapers: "papers" });
142
+ check("an unparsable paperlint.json — a FAILURE", r.code === 2);
152
143
  check(
153
- "and it says exactly why that is risky — it works only until the directory moves",
154
- /works only while your papers happen to live there/.test(r.out),
144
+ "named, with the way out (a file edit, not a command)",
145
+ /✗ paperlint\.json is not valid JSON/.test(r.out) &&
146
+ /Edit or Write/.test(r.out),
155
147
  );
156
148
  rmSync(dir, { recursive: true, force: true });
157
149
  }
@@ -163,7 +155,7 @@ const runDoctor = (
163
155
  {
164
156
  const dir = consumer({
165
157
  papersDir: "papers",
166
- pkgKey: "papers",
158
+ declared: "papers",
167
159
  makeDir: false,
168
160
  });
169
161
  const r = runDoctor(dir, { cliPapers: "papers" });
@@ -177,42 +169,9 @@ const runDoctor = (
177
169
  rmSync(dir, { recursive: true, force: true });
178
170
  }
179
171
 
180
- // ── II-quater. THE KEY'S OLD NAME IS READ AND NAMED; TWO DIFFERENT KEYS ARE A FAILURE ──────
181
- {
182
- const dir = consumer({ papersDir: "papers" });
183
- writeFileSync(
184
- join(dir, "package.json"),
185
- JSON.stringify({
186
- "research-paper-pipeline": { [PAPERS_DIR_FIELD]: "papers" },
187
- }),
188
- );
189
- const r = runDoctor(dir, { cliPapers: "papers" });
190
- check(
191
- "the old key — read (not a failure), and named with the new one",
192
- r.code === 0 &&
193
- /⚠ "research-paper-pipeline" in package\.json is the old name/.test(
194
- r.out,
195
- ) &&
196
- /the hooks will guard papers/.test(r.out),
197
- );
198
- writeFileSync(
199
- join(dir, "package.json"),
200
- JSON.stringify({
201
- paperlint: { [PAPERS_DIR_FIELD]: "papers" },
202
- "research-paper-pipeline": { [PAPERS_DIR_FIELD]: "elsewhere" },
203
- }),
204
- );
205
- const both = runDoctor(dir, { cliPapers: "papers" });
206
- check(
207
- "both keys with different contents — a failure",
208
- both.code === 2 && /✗ package\.json has both/.test(both.out),
209
- );
210
- rmSync(dir, { recursive: true, force: true });
211
- }
212
-
213
172
  // ── III. TWO DECLARATIONS HAVE DRIFTED APART ────────────────────────────────────────────────
214
173
  {
215
- const dir = consumer({ papersDir: "writing/drafts", pkgKey: "papers" });
174
+ const dir = consumer({ papersDir: "writing/drafts", declared: "papers" });
216
175
  mkdirSync(join(dir, "papers"), { recursive: true });
217
176
  const r = runDoctor(dir, { cliPapers: "writing/drafts" });
218
177
  check("both declarations exist, but differ — a FAILURE", r.code === 2);
@@ -223,12 +182,10 @@ const runDoctor = (
223
182
  // This is load-bearing: a copy of the logic would drift silently and print a confident wrong
224
183
  // answer.
225
184
  {
226
- const dir = consumer({ papersDir: "docs/papers", pkgKey: "docs/papers/" });
185
+ const dir = consumer({ papersDir: "docs/papers", declared: "docs/papers/" });
227
186
  const r = runDoctor(dir, { cliPapers: "docs/papers" });
228
187
  const fromHook = papersRoot(
229
- JSON.stringify({
230
- paperlint: { [PAPERS_DIR_FIELD]: "docs/papers/" },
231
- }),
188
+ JSON.stringify({ [PAPERS_DIR_FIELD]: "docs/papers/" }),
232
189
  );
233
190
  check("the hook itself trims the trailing slash", fromHook === "docs/papers");
234
191
  check(
@@ -244,7 +201,7 @@ const runDoctor = (
244
201
 
245
202
  // ── V. THE HOOK'S REFUSAL IS PASSED ALONG, NOT TURNED INTO A DIRECTORY ─────────────────────
246
203
  {
247
- const dir = consumer({ papersDir: "papers", pkgKey: "" });
204
+ const dir = consumer({ papersDir: "papers", declared: "" });
248
205
  const r = runDoctor(dir, { cliPapers: "papers" });
249
206
  check(
250
207
  "an empty string in the declaration — the hook refuses, and doctor NAMES it",
@@ -255,7 +212,7 @@ const runDoctor = (
255
212
 
256
213
  // ── VI. EXTERNAL PROGRAMS ARE A FACT, NOT A VERDICT ─────────────────────────────────────────
257
214
  {
258
- const dir = consumer({ papersDir: "papers", pkgKey: "papers" });
215
+ const dir = consumer({ papersDir: "papers", declared: "papers" });
259
216
  const none = runDoctor(dir, { cliPapers: "papers", have: () => 1 });
260
217
  check(
261
218
  "🔴 a missing tex install does NOT fail the run — a gate on advice would mute the whole thing",
@@ -330,7 +287,7 @@ const runDoctor = (
330
287
  // nothing anywhere said so. The link state itself is `link-skills.harness.mjs`'s subject; here
331
288
  // only doctor's REPORTING of it is judged, so the state is handed in rather than built on disk.
332
289
  {
333
- const dir = consumer({ papersDir: "papers", pkgKey: "papers" });
290
+ const dir = consumer({ papersDir: "papers", declared: "papers" });
334
291
  const state = {
335
292
  ok: true,
336
293
  home: join(dir, ".claude", "skills"),
@@ -46,8 +46,8 @@ process.exit(
46
46
  edits: [
47
47
  [
48
48
  SRC,
49
- " const hookRoot = rawPkg ? papersRoot(rawPkg) : null;",
50
- ' const hookRoot = rawPkg ? (JSON.parse(rawPkg)?.["research-paper-pipeline"]?.papersDir ?? "papers") : null;',
49
+ ' const hookRoot = papersRoot(raw ?? "{}");',
50
+ ' const hookRoot = raw === null ? "papers" : (() => { try { return JSON.parse(raw)?.papersDir ?? "papers"; } catch { return "papers"; } })();',
51
51
  ],
52
52
  ],
53
53
  },
@@ -115,18 +115,18 @@ process.exit(
115
115
  ],
116
116
  },
117
117
  {
118
- name: "a missing declaration is reported as all-clear",
118
+ name: "an unparsable paperlint.json is reported as all-clear",
119
119
  harness: HARNESS,
120
- expect: "the missing declaration is NAMED, not skipped",
120
+ expect: "an unparsable paperlint.json — a FAILURE",
121
121
  disables:
122
- "the only trace of issue #33 left on an install that STILL works: `paperlint init` writes " +
123
- "one file, the hook reads another, and the directories matching up rests on the " +
124
- "default. A cheerful checkmark instead of a warning turns a coincidence into a confirmation",
122
+ "the one failure left in the settings file: while it does not parse, the edit guard " +
123
+ "refuses every Bash command. A doctor that counts it as fine sends the reader looking " +
124
+ "everywhere but the file",
125
125
  edits: [
126
126
  [
127
127
  SRC,
128
- ' ` ⚠ package.json has no "${CONFIG_KEY}": { "${PAPERS_DIR_FIELD}": … } — the hooks fall back to "${DEFAULT_PAPERS_ROOT}"`,',
129
- " ` ✓ package.json`,",
128
+ " ` the edit guard refuses every Bash command until it parses — fix it with Edit or Write`,\n ],\n bad: 1,",
129
+ " ` the edit guard refuses every Bash command until it parses — fix it with Edit or Write`,\n ],\n bad: 0,",
130
130
  ],
131
131
  ],
132
132
  },
package/src/doctor.ts CHANGED
@@ -29,19 +29,11 @@ import { spawnSync } from "node:child_process";
29
29
  // the hook itself rather than at a shared module.
30
30
  import {
31
31
  papersRoot,
32
- CONFIG_KEY,
33
32
  DEFAULT_PAPERS_ROOT,
34
33
  PAPERS_DIR_FIELD,
35
- OLD_PAPERS_DIR_FIELD,
34
+ CONFIG_FILE,
36
35
  } from "../hooks/paper-edit-guard.hook.mjs";
37
- import {
38
- LEGACY_CONFIG_KEY,
39
- LEGACY_KEY_MESSAGE,
40
- LEGACY_PAPER_SETTINGS_FILE,
41
- PAPER_SETTINGS_FILE,
42
- declaredSettings,
43
- } from "../lib/paper-config.mjs";
44
- import { PAPER_MARKERS, papersIn } from "./build.ts";
36
+ import { PAPER_MARKERS } from "./build.ts";
45
37
  import { linkSkills, SKILLS_HOME, type LinkReport } from "./link-skills.ts";
46
38
  import { doctorHooks } from "./hooks-settings.ts";
47
39
 
@@ -148,51 +140,44 @@ export function detectPapers(cwd: string, depth = 2): string[] {
148
140
  }
149
141
 
150
142
  /**
151
- * The declaration's verdict. The old field name and two differing keys are failures (every reader
152
- * refuses them); the old KEY is read and named; a missing declaration is a warning.
143
+ * The root `paperlint.json`'s verdict. Absent, or silent on `papersDir`, is fine: the default is
144
+ * `papers`, and the CLI and the hooks take it alike. Only a file that does not parse is a failure —
145
+ * the edit guard refuses every Bash command while it cannot read it.
153
146
  */
154
- function declarationVerdict(rawPkg: string): { lines: string[]; bad: number } {
155
- const out: string[] = [];
156
- const found = (() => {
157
- try {
158
- return declaredSettings(JSON.parse(rawPkg));
159
- } catch {
160
- return declaredSettings(undefined);
161
- }
162
- })();
163
- if (found.conflict !== null)
164
- return { lines: [` ✗ ${found.conflict}`], bad: 1 };
165
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #49: replace with a real type
166
- const settings = found.settings as Record<string, any> | undefined;
167
- const key = found.legacy ? LEGACY_CONFIG_KEY : CONFIG_KEY;
168
- const declared = settings?.[PAPERS_DIR_FIELD];
169
- if (found.legacy) out.push(` ⚠ ${LEGACY_KEY_MESSAGE}`);
170
- // The old field name is a failure, not a warning: every reader refuses it.
171
- if (settings && Object.hasOwn(settings, OLD_PAPERS_DIR_FIELD))
147
+ function declarationVerdict(raw: string | null): {
148
+ lines: string[];
149
+ bad: number;
150
+ } {
151
+ if (raw === null)
152
+ return {
153
+ lines: [
154
+ ` ✓ no ${CONFIG_FILE} — every setting has its default; papers are in "${DEFAULT_PAPERS_ROOT}"`,
155
+ ],
156
+ bad: 0,
157
+ };
158
+ let settings: unknown;
159
+ try {
160
+ settings = JSON.parse(raw);
161
+ } catch (e) {
172
162
  return {
173
163
  lines: [
174
- ...out,
175
- ` ✗ "${OLD_PAPERS_DIR_FIELD}" was renamed to "${PAPERS_DIR_FIELD}" in package.json → "${key}"`,
164
+ ` ✗ ${CONFIG_FILE} is not valid JSON: ${(e as Error).message}`,
165
+ ` the edit guard refuses every Bash command until it parses — fix it with Edit or Write`,
176
166
  ],
177
167
  bad: 1,
178
168
  };
179
- // 🔴 A MISSING DECLARATION IS A WARNING, NOT A REFUSAL, and that is a decision, not an
180
- // oversight. Without it the hook takes the `papers` default; if the papers do live there, the
181
- // install WORKS — just by coincidence, and it will break silently on the day the directory
182
- // moves. Failing on a working install is not allowed here: for an `error`-level check a false
183
- // positive costs more than a miss, because people do not fix it, they switch it off — together
184
- // with the binary findings below, which the command was written for. A real breakage (the roots
185
- // drifted apart, the directory does not exist) is caught where it is binary.
186
- if (declared === undefined)
187
- out.push(
188
- ` ⚠ package.json has no "${CONFIG_KEY}": { "${PAPERS_DIR_FIELD}": … } — the hooks fall back to "${DEFAULT_PAPERS_ROOT}"`,
189
- ` it works only while your papers happen to live there; declare it and it keeps working`,
190
- );
191
- else
192
- out.push(
193
- ` ✓ package.json → ${key}.${PAPERS_DIR_FIELD} = ${JSON.stringify(declared)}`,
194
- );
195
- return { lines: out, bad: 0 };
169
+ }
170
+ const declared = (settings as Record<string, unknown> | null)?.[
171
+ PAPERS_DIR_FIELD
172
+ ];
173
+ return {
174
+ lines: [
175
+ declared === undefined
176
+ ? ` ✓ ${CONFIG_FILE} names no ${PAPERS_DIR_FIELD} — the default "${DEFAULT_PAPERS_ROOT}"`
177
+ : ` ✓ ${CONFIG_FILE} → ${PAPERS_DIR_FIELD} = ${JSON.stringify(declared)}`,
178
+ ],
179
+ bad: 0,
180
+ };
196
181
  }
197
182
 
198
183
  /**
@@ -235,28 +220,6 @@ function papersVerdict(
235
220
  return { lines: out, bad };
236
221
  }
237
222
 
238
- /**
239
- * A pre-2.1.0 `venue.json` left in a paper: it is no longer read, so that paper's venue checks
240
- * silently would not run if nothing named it. `init` moves it.
241
- */
242
- function leftoverSettingsVerdict(
243
- root: string,
244
- papersDir: string | null,
245
- ): { lines: string[]; bad: number } {
246
- if (papersDir === null) return { lines: [], bad: 0 };
247
- const lines = papersIn(resolve(root, papersDir), [
248
- ...PAPER_MARKERS,
249
- LEGACY_PAPER_SETTINGS_FILE,
250
- ])
251
- .map((dir) => join(dir, LEGACY_PAPER_SETTINGS_FILE))
252
- .filter((f) => existsSync(f))
253
- .map(
254
- (f) =>
255
- ` ✗ ${relative(root, f)} is no longer read — paperlint 2.1.0 renamed it ${PAPER_SETTINGS_FILE}, so this paper's venue checks do not run. \`npx paperlint init\` moves it`,
256
- );
257
- return { lines, bad: lines.length };
258
- }
259
-
260
223
  export interface DoctorOptions {
261
224
  log?: typeof console.log;
262
225
  cwd?: string;
@@ -285,7 +248,7 @@ export function doctor({
285
248
  skillLinks = (r: string) => linkSkills(r, { write: false }),
286
249
  }: DoctorOptions = {}): number {
287
250
  const root = projectDir ?? cwd;
288
- const pkgPath = join(root, "package.json");
251
+ const configPath = join(root, CONFIG_FILE);
289
252
  const out: string[] = [
290
253
  "",
291
254
  "paperlint doctor — what is wired, and what only looks wired",
@@ -293,22 +256,15 @@ export function doctor({
293
256
  ];
294
257
  let bad = 0;
295
258
 
296
- out.push("declaration");
297
- const rawPkg = existsSync(pkgPath) ? readFileSync(pkgPath, "utf8") : "";
298
- if (!rawPkg) {
299
- out.push(` ✗ no package.json at ${root}`);
300
- out.push(
301
- ` the hooks read their papers directory from there and refuse without it`,
302
- );
303
- bad++;
304
- } else {
305
- const declaration = declarationVerdict(rawPkg);
306
- out.push(...declaration.lines);
307
- bad += declaration.bad;
308
- }
259
+ out.push("settings");
260
+ const raw = existsSync(configPath) ? readFileSync(configPath, "utf8") : null;
261
+ const declaration = declarationVerdict(raw);
262
+ out.push(...declaration.lines);
263
+ bad += declaration.bad;
309
264
 
310
265
  out.push("", "papers directory");
311
- const hookRoot = rawPkg ? papersRoot(rawPkg) : null;
266
+ // The hook is given what its provider gives it: the file, or `{}` when there is none.
267
+ const hookRoot = papersRoot(raw ?? "{}");
312
268
  const hookSays = typeof hookRoot === "string" ? hookRoot : null;
313
269
  out.push(
314
270
  ` the CLI will lint ${cliPapers ?? "(nothing — no declaration found)"}`,
@@ -317,9 +273,8 @@ export function doctor({
317
273
  ` the hooks will guard ${hookSays ?? "(nothing — the guard refuses and says why on first use)"}`,
318
274
  );
319
275
  const verdict = papersVerdict(root, cliPapers, hookSays);
320
- const leftover = leftoverSettingsVerdict(root, cliPapers ?? hookSays);
321
- out.push(...verdict.lines, ...leftover.lines);
322
- bad += verdict.bad + leftover.bad;
276
+ out.push(...verdict.lines);
277
+ bad += verdict.bad;
323
278
 
324
279
  // A skill that is not linked is ADVISORY, like a missing program: `paperlint lint`, the hooks and CI
325
280
  // work without it, and an entry of the same name that `init` refused to replace is the
@@ -272,13 +272,6 @@ test("declaredVenue: the label is the preset's, and an extends that resolves now
272
272
  assert.throws(() => declaredVenue(typo, PAPER), /shipped presets/);
273
273
  });
274
274
 
275
- test("declaredVenue: a venue.json alone is refused with the command that moves it, not read", () => {
276
- const files = memoryFiles({
277
- [`${PAPER}/venue.json`]: '{"venue":"agenticdev"}',
278
- });
279
- assert.throws(() => declaredVenue(files, PAPER), /npx paperlint init/);
280
- });
281
-
282
275
  test("declaredVenue: a paperlint.json with an unknown key is refused, naming the key", () => {
283
276
  const files = memoryFiles({
284
277
  [`${PAPER}/paperlint.json`]: '{"venu":"agenticdev"}',
package/src/facts-file.ts CHANGED
@@ -75,8 +75,8 @@ export interface VenueDecl {
75
75
 
76
76
  /**
77
77
  * What a paper's `paperlint.json` declares, or null when it has none. THROWS with a one-line reason
78
- * when the file does not parse, its `extends` does not resolve, or only the pre-2.1.0 `venue.json`
79
- * is there: the build reports that as its `facts` failure rather than building against no venue.
78
+ * when the file does not parse or its `extends` does not resolve: the build reports that as its
79
+ * `facts` failure rather than building against no venue.
80
80
  */
81
81
  export function declaredVenue(
82
82
  files: Files,
@@ -36,7 +36,6 @@ const {
36
36
  shippedWiring,
37
37
  wireHooks,
38
38
  wiredCounts,
39
- pluginEnabledHere,
40
39
  doctorHooks,
41
40
  readSettings,
42
41
  MANAGED_BY,
@@ -83,36 +82,26 @@ try {
83
82
  // ── which spelling runs which hook ─────────────────────────────────────────────────────
84
83
  const ours = `node "\${CLAUDE_PROJECT_DIR}/${MANAGED_BY}" hook paper-edit-guard`;
85
84
  const cases = [
86
- [ours, { name: "paper-edit-guard", ours: true, legacy: false }],
85
+ [ours, { name: "paper-edit-guard", ours: true }],
87
86
  [
88
87
  `node "$CLAUDE_PROJECT_DIR/${MANAGED_BY}" hook paper-status-gates`,
89
- { name: "paper-status-gates", ours: true, legacy: false },
90
- ],
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
- [
94
- `node "$CLAUDE_PROJECT_DIR/node_modules/research-paper-pipeline/bin/rpp.mjs" hook paper-edit-guard`,
95
- { name: "paper-edit-guard", ours: false, legacy: true },
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 },
88
+ { name: "paper-status-gates", ours: true },
100
89
  ],
101
90
  [
102
91
  `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"`,
103
- { name: "paper-edit-guard", ours: false, legacy: false },
92
+ { name: "paper-edit-guard", ours: false },
104
93
  ],
105
94
  [
106
95
  `npx paperlint hook paper-skills-nudge`,
107
- { name: "paper-skills-nudge", ours: false, legacy: false },
96
+ { name: "paper-skills-nudge", ours: false },
108
97
  ],
109
98
  [
110
99
  `npx paperlint hook paper-skills-nudge`,
111
- { name: "paper-skills-nudge", ours: false, legacy: false },
100
+ { name: "paper-skills-nudge", ours: false },
112
101
  ],
113
102
  [
114
103
  `node /abs/proj/node_modules/paperlint/bin/paperlint.mjs hook paper-edit-guard`,
115
- { name: "paper-edit-guard", ours: false, legacy: false },
104
+ { name: "paper-edit-guard", ours: false },
116
105
  ],
117
106
  [`node my-own-lint.mjs`, null],
118
107
  [`node node_modules/paperlint/bin/paperlint.mjs lint`, null],
@@ -158,46 +147,6 @@ try {
158
147
  );
159
148
  }
160
149
 
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
- ]) {
168
- const legacyWired = JSON.parse(
169
- JSON.stringify(merge({}, wiring.compiled, MANAGED_BY)).replaceAll(
170
- MANAGED_BY,
171
- stale,
172
- ),
173
- );
174
- legacyWired.hooks.PostToolUse[0].hooks.push({
175
- type: "command",
176
- command: "node my-own-lint.mjs",
177
- });
178
- const dir = project(`legacy-${release}`, legacyWired);
179
- const before = doctorHooks(dir, wiring).join("\n");
180
- check(
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`) &&
183
- /npx paperlint init` replaces them/.test(before),
184
- );
185
- const r = wireHooks(dir, merge, wiring);
186
- const s = JSON.parse(text(dir));
187
- const counts = [...wiredCounts(s, wiring.names).values()];
188
- check(
189
- `🔴 init replaces what ${release} wrote: every hook wired once, none left at the old path`,
190
- r.status === "written" &&
191
- r.replaced === wiring.names.length &&
192
- counts.every((c) => c.ours === 1 && c.legacy === 0 && c.other === 0) &&
193
- !text(dir).includes(stale),
194
- );
195
- check(
196
- `and the user's own command in the same matcher survives the ${release} migration`,
197
- text(dir).includes("node my-own-lint.mjs"),
198
- );
199
- }
200
-
201
150
  // ── the user's own hooks and keys survive ──────────────────────────────────────────────
202
151
  {
203
152
  const dir = project("user", {
@@ -250,7 +199,7 @@ try {
250
199
  {
251
200
  type: "command",
252
201
  command:
253
- 'node "$CLAUDE_PROJECT_DIR/node_modules/vigiles/dist/cli.js" hook-runtime run-program "$CLAUDE_PROJECT_DIR/node_modules/research-paper-pipeline/hooks/paper-edit-guard.hook.mjs"',
202
+ '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"',
254
203
  },
255
204
  ],
256
205
  },
@@ -296,25 +245,6 @@ try {
296
245
  );
297
246
  }
298
247
 
299
- // ── the plugin, enabled by the project ─────────────────────────────────────────────────
300
- check(
301
- "the project's enabledPlugins entry for this package is found",
302
- pluginEnabledHere({
303
- enabledPlugins: {
304
- "research-paper-pipeline@research-paper-pipeline": true,
305
- "other@x": true,
306
- },
307
- }).join() === "research-paper-pipeline@research-paper-pipeline",
308
- );
309
- check(
310
- "a disabled entry does not count",
311
- pluginEnabledHere({
312
- enabledPlugins: {
313
- "research-paper-pipeline@research-paper-pipeline": false,
314
- },
315
- }).length === 0,
316
- );
317
-
318
248
  // ── doctor's section ───────────────────────────────────────────────────────────────────
319
249
  const doc = (settings) => {
320
250
  const dir = project(`doc-${String(n)}`, settings);
@@ -361,21 +291,6 @@ try {
361
291
  handPartial,
362
292
  ) && /writes nothing then/.test(handPartial),
363
293
  );
364
- const pluginText = doc({
365
- ...wiredOnce,
366
- enabledPlugins: { "research-paper-pipeline@research-paper-pipeline": true },
367
- });
368
- check(
369
- "the project also enabling the plugin → told to uninstall it, with the command",
370
- /ALSO enables the plugin/.test(pluginText) &&
371
- /\/plugin uninstall research-paper-pipeline@research-paper-pipeline/.test(
372
- pluginText,
373
- ),
374
- );
375
- check(
376
- "🔴 and it says it cannot see a USER-scope plugin, rather than implying there is none",
377
- /USER scope is not visible from here/.test(doc({})),
378
- );
379
294
  check(
380
295
  "an unparsable file is named in doctor, not crashed on",
381
296
  /does not parse/.test(
@@ -24,12 +24,12 @@ process.exit(
24
24
  harness: HARNESS,
25
25
  // The spelling truth table is the first owner: `npx paperlint hook …` must read as NOT ours,
26
26
  // and that row is where it goes red.
27
- expect: '→ {"name":"paper-edit-guard","ours":false,"legacy":true}',
27
+ expect: '→ {"name":"paper-skills-nudge","ours":false}',
28
28
  disables:
29
29
  "the duplicate guard for every bin spelling but ours. A hook wired by hand as " +
30
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: [[SRC, " ours: t === MANAGED_BY,", " ours: true,"]],
32
+ edits: [[SRC, "ours: t === MANAGED_BY,", "ours: true,"]],
33
33
  },
34
34
  {
35
35
  name: "another spelling no longer stops the write",
@@ -94,21 +94,6 @@ process.exit(
94
94
  "same thing again — a loop with no exit",
95
95
  edits: [[SRC, " const remedy = handWired", " const remedy = false"]],
96
96
  },
97
- {
98
- name: "a disabled plugin counts as enabled",
99
- harness: HARNESS,
100
- expect: "a disabled entry does not count",
101
- disables:
102
- "the plugin warning's precision. A project that already switched the plugin off would " +
103
- "be told to uninstall it",
104
- edits: [
105
- [
106
- SRC,
107
- ' ([id, on]) => on === true && id.split("@")[0] === LEGACY_PACKAGE_NAME,',
108
- ' ([id]) => id.split("@")[0] === LEGACY_PACKAGE_NAME,',
109
- ],
110
- ],
111
- },
112
97
  ],
113
98
  }),
114
99
  );