paperlint 2.0.1 → 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 (226) hide show
  1. package/CLAUDE.md +6 -1
  2. package/CONTRIBUTING.md +21 -0
  3. package/README.md +231 -180
  4. package/action.yml +4 -6
  5. package/dist/adapters/references/index.d.ts +13 -0
  6. package/dist/adapters/references/index.d.ts.map +1 -0
  7. package/dist/adapters/references/index.js +55 -0
  8. package/dist/adapters/references/index.js.map +1 -0
  9. package/dist/adapters/references/reach.io.d.ts +3 -0
  10. package/dist/adapters/references/reach.io.d.ts.map +1 -0
  11. package/dist/adapters/references/reach.io.js +14 -0
  12. package/dist/adapters/references/reach.io.js.map +1 -0
  13. package/dist/build-engine.d.ts.map +1 -1
  14. package/dist/build-engine.js +5 -2
  15. package/dist/build-engine.js.map +1 -1
  16. package/dist/build.d.ts +16 -1
  17. package/dist/build.d.ts.map +1 -1
  18. package/dist/build.js +40 -5
  19. package/dist/build.js.map +1 -1
  20. package/dist/cli.d.ts +37 -20
  21. package/dist/cli.d.ts.map +1 -1
  22. package/dist/cli.js +377 -251
  23. package/dist/cli.js.map +1 -1
  24. package/dist/doctor.d.ts.map +1 -1
  25. package/dist/doctor.js +37 -52
  26. package/dist/doctor.js.map +1 -1
  27. package/dist/facts-file.d.ts +33 -9
  28. package/dist/facts-file.d.ts.map +1 -1
  29. package/dist/facts-file.js +117 -17
  30. package/dist/facts-file.js.map +1 -1
  31. package/dist/hooks-settings.d.ts +0 -21
  32. package/dist/hooks-settings.d.ts.map +1 -1
  33. package/dist/hooks-settings.js +17 -135
  34. package/dist/hooks-settings.js.map +1 -1
  35. package/dist/init.d.ts +40 -19
  36. package/dist/init.d.ts.map +1 -1
  37. package/dist/init.js +144 -109
  38. package/dist/init.js.map +1 -1
  39. package/dist/link-skills.d.ts +1 -1
  40. package/dist/link-skills.d.ts.map +1 -1
  41. package/dist/link-skills.js +11 -29
  42. package/dist/link-skills.js.map +1 -1
  43. package/dist/new-paper.d.ts +2 -2
  44. package/dist/new-paper.d.ts.map +1 -1
  45. package/dist/new-paper.js +6 -2
  46. package/dist/new-paper.js.map +1 -1
  47. package/dist/paper-files.d.ts +63 -0
  48. package/dist/paper-files.d.ts.map +1 -0
  49. package/dist/paper-files.js +69 -0
  50. package/dist/paper-files.js.map +1 -0
  51. package/dist/paper-settings.d.ts +40 -0
  52. package/dist/paper-settings.d.ts.map +1 -0
  53. package/dist/paper-settings.js +144 -0
  54. package/dist/paper-settings.js.map +1 -0
  55. package/dist/ports/check-references.d.ts +31 -0
  56. package/dist/ports/check-references.d.ts.map +1 -0
  57. package/dist/ports/check-references.js +7 -0
  58. package/dist/ports/check-references.js.map +1 -0
  59. package/dist/presets.d.ts +84 -0
  60. package/dist/presets.d.ts.map +1 -0
  61. package/dist/presets.js +184 -0
  62. package/dist/presets.js.map +1 -0
  63. package/dist/reference-rules.d.ts +47 -0
  64. package/dist/reference-rules.d.ts.map +1 -0
  65. package/dist/reference-rules.js +156 -0
  66. package/dist/reference-rules.js.map +1 -0
  67. package/dist/references.d.ts +36 -0
  68. package/dist/references.d.ts.map +1 -0
  69. package/dist/references.js +91 -0
  70. package/dist/references.js.map +1 -0
  71. package/dist/rules-config.d.ts +11 -2
  72. package/dist/rules-config.d.ts.map +1 -1
  73. package/dist/rules-config.js +25 -11
  74. package/dist/rules-config.js.map +1 -1
  75. package/dist/structure.d.ts.map +1 -1
  76. package/dist/structure.js +3 -2
  77. package/dist/structure.js.map +1 -1
  78. package/dist/tex-requirements.d.ts +63 -7
  79. package/dist/tex-requirements.d.ts.map +1 -1
  80. package/dist/tex-requirements.js +91 -24
  81. package/dist/tex-requirements.js.map +1 -1
  82. package/dist/types.d.ts +20 -18
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/venue-rules.d.ts +109 -0
  85. package/dist/venue-rules.d.ts.map +1 -0
  86. package/dist/venue-rules.js +389 -0
  87. package/dist/venue-rules.js.map +1 -0
  88. package/docs/configuration.md +152 -77
  89. package/docs/e2e.md +1 -1
  90. package/docs/install.md +25 -40
  91. package/docs/optional-rules.md +26 -24
  92. package/docs/rules.md +181 -15
  93. package/eslint-rules/bib-reachable-entry.mjs +75 -0
  94. package/eslint-rules/latex-language.mjs +29 -1
  95. package/eslint-rules/paper-stages.harness.mjs +1 -117
  96. package/eslint-rules/paper-stages.mjs +0 -120
  97. package/eslint-rules/paper-stages.mutations.mjs +0 -76
  98. package/eslint-rules/paper-typography.mjs +267 -192
  99. package/eslint-rules/paper-typography.test.mjs +297 -0
  100. package/eslint-rules/papers.harness.mjs +8 -12
  101. package/eslint-rules/papers.mjs +17 -27
  102. package/eslint-rules/pdf-last-page-balance.mjs +23 -5
  103. package/eslint-rules/review-frontmatter.mjs +118 -0
  104. package/eslint-rules/review-frontmatter.schema.json +36 -0
  105. package/eslint-rules/review-frontmatter.test.mjs +118 -0
  106. package/eslint-rules/sibling-frontmatter.mjs +19 -0
  107. package/eslint-rules/sibling-frontmatter.schema.json +14 -0
  108. package/eslint.config.mjs +0 -15
  109. package/fixtures/build-e2e/acmart/paperlint.json +1 -0
  110. package/fixtures/real-markdown-paper/baseline.json +6 -7
  111. package/hooks/hooks.harness.mjs +39 -98
  112. package/hooks/hooks.mutations.mjs +6 -6
  113. package/hooks/paper-edit-guard.hook.d.mts +2 -3
  114. package/hooks/paper-edit-guard.hook.mjs +31 -42
  115. package/hooks/paper-skills-nudge.hook.mjs +21 -19
  116. package/hooks/paper-status-gates.hook.mjs +21 -19
  117. package/hooks/paper-status-gates.sh +8 -13
  118. package/lib/paper-config.d.mts +15 -18
  119. package/lib/paper-config.harness.mjs +18 -35
  120. package/lib/paper-config.mjs +90 -81
  121. package/lib/paper-config.mutations.mjs +2 -2
  122. package/lib/skill-checks.mjs +1 -1
  123. package/lib/skill-trigger-cases.harness.mjs +9 -8
  124. package/lib/skill-trigger-cases.mjs +7 -15
  125. package/package.json +1 -1
  126. package/scripts/eslint-report-guard.mjs +1 -1
  127. package/scripts/harness-api.frozen.json +0 -3
  128. package/scripts/mutation-batteries.frozen.json +4 -13
  129. package/skills/analyze-sibling-paper/SKILL.md +16 -12
  130. package/skills/analyze-sibling-paper/SKILL.md.spec.ts +15 -11
  131. package/skills/cold-read-diff/SKILL.md +20 -1
  132. package/skills/cold-read-diff/SKILL.md.spec.ts +19 -0
  133. package/skills/find-venue/SKILL.md +5 -1
  134. package/skills/find-venue/SKILL.md.spec.ts +4 -0
  135. package/skills/grade-paper-writing/SKILL.md +20 -1
  136. package/skills/grade-paper-writing/SKILL.md.spec.ts +19 -0
  137. package/skills/paper-pipeline/PIPELINE-MAP.md +14 -4
  138. package/skills/paper-pipeline/SKILL.md +4 -3
  139. package/skills/paper-pipeline/SKILL.md.spec.ts +3 -2
  140. package/skills/paper-pipeline/scripts/consumer.d.mts +0 -1
  141. package/skills/paper-pipeline/scripts/consumer.harness.mjs +8 -12
  142. package/skills/paper-pipeline/scripts/consumer.mjs +19 -33
  143. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +6 -4
  144. package/skills/paper-pipeline/scripts/run-mechanical.mjs +2 -11
  145. package/skills/plan-paper-timeline/SKILL.md +2 -2
  146. package/skills/plan-paper-timeline/SKILL.md.spec.ts +1 -1
  147. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +1 -1
  148. package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +2 -2
  149. package/skills/render-paper/check-render.sh +2 -3
  150. package/skills/render-paper/extract-pdf-facts.harness.mjs +5 -5
  151. package/skills/render-paper/extract-pdf-facts.mjs +4 -4
  152. package/skills/render-paper/render-paper.harness.mjs +2 -2
  153. package/skills/study-accepted-papers/SKILL.md +2 -1
  154. package/skills/study-accepted-papers/SKILL.md.spec.ts +1 -0
  155. package/skills/submit-paper/SKILL.md +9 -1
  156. package/skills/submit-paper/SKILL.md.spec.ts +8 -0
  157. package/skills/submit-paper/references/venues/acm-sigconf.jsonc +86 -0
  158. package/skills/submit-paper/references/venues/agenticdev.jsonc +34 -102
  159. package/skills/submit-paper/references/venues/aisec.jsonc +21 -82
  160. package/skills/submit-paper/references/venues/realm.jsonc +45 -43
  161. package/skills/submit-paper/references/venues/tex-base.jsonc +1 -1
  162. package/skills/submit-paper/references/venues/venue-profile.schema.json +119 -29
  163. package/skills/verify-citations/SKILL.md +10 -1
  164. package/skills/verify-citations/SKILL.md.spec.ts +9 -0
  165. package/skills/verify-citations/scripts/bib-authors.mjs +48 -26
  166. package/src/adapters/references/index.ts +102 -0
  167. package/src/adapters/references/reach.io.ts +12 -0
  168. package/src/build-engine.ts +10 -2
  169. package/src/build.harness.mjs +31 -22
  170. package/src/build.ts +65 -8
  171. package/src/cli.harness.mjs +151 -294
  172. package/src/cli.mutations.mjs +35 -44
  173. package/src/cli.ts +457 -288
  174. package/src/doctor.harness.mjs +41 -84
  175. package/src/doctor.mutations.mjs +9 -9
  176. package/src/doctor.ts +41 -61
  177. package/src/facts-file.test.ts +47 -7
  178. package/src/facts-file.ts +145 -24
  179. package/src/hooks-settings.harness.mjs +7 -92
  180. package/src/hooks-settings.mutations.mjs +2 -17
  181. package/src/hooks-settings.ts +18 -162
  182. package/src/init.test.ts +206 -0
  183. package/src/init.ts +193 -159
  184. package/src/link-skills.harness.mjs +1 -38
  185. package/src/link-skills.mutations.mjs +9 -5
  186. package/src/link-skills.ts +10 -34
  187. package/src/lint-fix.test.ts +120 -0
  188. package/src/new-paper.harness.mjs +9 -6
  189. package/src/new-paper.test.ts +133 -0
  190. package/src/new-paper.ts +7 -2
  191. package/src/paper-files.test.ts +290 -0
  192. package/src/paper-files.ts +144 -0
  193. package/src/paper-settings-commands.test.ts +274 -0
  194. package/src/paper-settings.test.ts +249 -0
  195. package/src/paper-settings.ts +208 -0
  196. package/src/ports/check-references.ts +28 -0
  197. package/src/presets.test.ts +236 -0
  198. package/src/presets.ts +297 -0
  199. package/src/reference-rules.ts +229 -0
  200. package/src/references.test.ts +209 -0
  201. package/src/references.ts +138 -0
  202. package/src/rules-config.harness.mjs +24 -8
  203. package/src/rules-config.ts +30 -13
  204. package/src/structure.harness.mjs +6 -4
  205. package/src/structure.ts +3 -2
  206. package/src/tex-requirements.harness.mjs +11 -17
  207. package/src/tex-requirements.ts +179 -29
  208. package/src/types.ts +30 -18
  209. package/src/venue-rules.test.ts +554 -0
  210. package/src/venue-rules.ts +571 -0
  211. package/templates/paper/paperlint.json +4 -0
  212. package/eslint-rules/doc-fields.harness.mjs +0 -336
  213. package/eslint-rules/doc-fields.mjs +0 -186
  214. package/eslint-rules/doc-fields.mutations.mjs +0 -96
  215. package/eslint-rules/paper-typography.harness.mjs +0 -291
  216. package/eslint-rules/paper-typography.mutations.mjs +0 -131
  217. package/eslint-rules/review-findings-cause.harness.mjs +0 -228
  218. package/eslint-rules/review-findings-cause.mjs +0 -135
  219. package/eslint-rules/review-findings-cause.mutations.mjs +0 -72
  220. package/fixtures/build-e2e/acmart/venue.json +0 -1
  221. package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +0 -16
  222. package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +0 -17
  223. package/fixtures/review-findings-cause/clean.md +0 -17
  224. package/fixtures/review-findings-cause/defect.md +0 -14
  225. package/fixtures/review-findings-cause/old-debt.md +0 -14
  226. 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,16 +29,10 @@ 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
- declaredSettings,
41
- } from "../lib/paper-config.mjs";
42
36
  import { PAPER_MARKERS } from "./build.ts";
43
37
  import { linkSkills, SKILLS_HOME, type LinkReport } from "./link-skills.ts";
44
38
  import { doctorHooks } from "./hooks-settings.ts";
@@ -146,51 +140,44 @@ export function detectPapers(cwd: string, depth = 2): string[] {
146
140
  }
147
141
 
148
142
  /**
149
- * The declaration's verdict. The old field name and two differing keys are failures (every reader
150
- * 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.
151
146
  */
152
- function declarationVerdict(rawPkg: string): { lines: string[]; bad: number } {
153
- const out: string[] = [];
154
- const found = (() => {
155
- try {
156
- return declaredSettings(JSON.parse(rawPkg));
157
- } catch {
158
- return declaredSettings(undefined);
159
- }
160
- })();
161
- if (found.conflict !== null)
162
- return { lines: [` ✗ ${found.conflict}`], bad: 1 };
163
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- #49: replace with a real type
164
- const settings = found.settings as Record<string, any> | undefined;
165
- const key = found.legacy ? LEGACY_CONFIG_KEY : CONFIG_KEY;
166
- const declared = settings?.[PAPERS_DIR_FIELD];
167
- if (found.legacy) out.push(` ⚠ ${LEGACY_KEY_MESSAGE}`);
168
- // The old field name is a failure, not a warning: every reader refuses it.
169
- 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) {
170
162
  return {
171
163
  lines: [
172
- ...out,
173
- ` ✗ "${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`,
174
166
  ],
175
167
  bad: 1,
176
168
  };
177
- // 🔴 A MISSING DECLARATION IS A WARNING, NOT A REFUSAL, and that is a decision, not an
178
- // oversight. Without it the hook takes the `papers` default; if the papers do live there, the
179
- // install WORKS — just by coincidence, and it will break silently on the day the directory
180
- // moves. Failing on a working install is not allowed here: for an `error`-level check a false
181
- // positive costs more than a miss, because people do not fix it, they switch it off — together
182
- // with the binary findings below, which the command was written for. A real breakage (the roots
183
- // drifted apart, the directory does not exist) is caught where it is binary.
184
- if (declared === undefined)
185
- out.push(
186
- ` ⚠ package.json has no "${CONFIG_KEY}": { "${PAPERS_DIR_FIELD}": … } — the hooks fall back to "${DEFAULT_PAPERS_ROOT}"`,
187
- ` it works only while your papers happen to live there; declare it and it keeps working`,
188
- );
189
- else
190
- out.push(
191
- ` ✓ package.json → ${key}.${PAPERS_DIR_FIELD} = ${JSON.stringify(declared)}`,
192
- );
193
- 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
+ };
194
181
  }
195
182
 
196
183
  /**
@@ -261,7 +248,7 @@ export function doctor({
261
248
  skillLinks = (r: string) => linkSkills(r, { write: false }),
262
249
  }: DoctorOptions = {}): number {
263
250
  const root = projectDir ?? cwd;
264
- const pkgPath = join(root, "package.json");
251
+ const configPath = join(root, CONFIG_FILE);
265
252
  const out: string[] = [
266
253
  "",
267
254
  "paperlint doctor — what is wired, and what only looks wired",
@@ -269,22 +256,15 @@ export function doctor({
269
256
  ];
270
257
  let bad = 0;
271
258
 
272
- out.push("declaration");
273
- const rawPkg = existsSync(pkgPath) ? readFileSync(pkgPath, "utf8") : "";
274
- if (!rawPkg) {
275
- out.push(` ✗ no package.json at ${root}`);
276
- out.push(
277
- ` the hooks read their papers directory from there and refuse without it`,
278
- );
279
- bad++;
280
- } else {
281
- const declaration = declarationVerdict(rawPkg);
282
- out.push(...declaration.lines);
283
- bad += declaration.bad;
284
- }
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;
285
264
 
286
265
  out.push("", "papers directory");
287
- 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 ?? "{}");
288
268
  const hookSays = typeof hookRoot === "string" ? hookRoot : null;
289
269
  out.push(
290
270
  ` the CLI will lint ${cliPapers ?? "(nothing — no declaration found)"}`,
@@ -8,6 +8,9 @@
8
8
  * real banal is `test/e2e/banal.mjs`.
9
9
  */
10
10
  import assert from "node:assert/strict";
11
+ import { readdirSync, readFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
11
14
  import { test } from "vitest";
12
15
  import { memoryFiles } from "./adapters/memory/index.ts";
13
16
  import {
@@ -147,12 +150,23 @@ const good: PdfReader = async () => ({
147
150
  ok: true,
148
151
  facts: { pages: 2, fonts: FONTS, last: lastPage(60, 60), layout: [] },
149
152
  });
150
- /** Options over in-memory files: the PDF and venue.json on "disk", a measurer answering `geometry`. */
153
+ /** The shipped presets at their real paths: `extends` resolves through the same Files port. */
154
+ const SHIPPED_PRESETS = Object.fromEntries(
155
+ readdirSync(packageVenuesDir())
156
+ .filter((f) => /\.jsonc?$/.test(f))
157
+ .map((f) => [
158
+ join(packageVenuesDir(), f),
159
+ readFileSync(join(packageVenuesDir(), f)),
160
+ ]),
161
+ );
162
+
163
+ /** Options over in-memory files: the PDF and paperlint.json on "disk", a measurer answering `geometry`. */
151
164
  function setup(geometry: Geometry = NONE) {
152
165
  const files = memoryFiles({
153
166
  [PDF]: PDF_BYTES,
154
- [`${PAPER}/venue.json`]: JSON.stringify({
155
- venue: "agenticdev",
167
+ ...SHIPPED_PRESETS,
168
+ [`${PAPER}/paperlint.json`]: JSON.stringify({
169
+ extends: "paperlint:agenticdev",
156
170
  kind: "short",
157
171
  }),
158
172
  });
@@ -170,7 +184,7 @@ function setup(geometry: Geometry = NONE) {
170
184
  return { files, measured, o };
171
185
  }
172
186
 
173
- test("measure: venue and kind from venue.json, pdf relative to the paper, the file's sha256", async () => {
187
+ test("measure: venue and kind from paperlint.json, pdf relative to the paper, the file's sha256", async () => {
174
188
  const { o } = setup();
175
189
  const m = await measurePaper(PAPER, PDF, o);
176
190
  assert.ok(m.ok);
@@ -233,8 +247,34 @@ test("writeFactsFile: <paper>/_build/paper.facts.json holds exactly the document
233
247
  assert.deepEqual(JSON.parse(text), m.value.facts);
234
248
  });
235
249
 
236
- test("declaredVenue: none without venue.json, none when it names no venue", () => {
250
+ test("declaredVenue: none without paperlint.json; no label when it extends no preset", () => {
237
251
  assert.equal(declaredVenue(memoryFiles(), PAPER), null);
238
- const files = memoryFiles({ [`${PAPER}/venue.json`]: '{"kind":"x"}' });
239
- assert.equal(declaredVenue(files, PAPER), null);
252
+ const files = memoryFiles({
253
+ [`${PAPER}/paperlint.json`]: '{"kind":"x","pdf":"b/p.pdf"}',
254
+ });
255
+ assert.deepEqual(declaredVenue(files, PAPER), {
256
+ label: null,
257
+ kind: "x",
258
+ pdf: "b/p.pdf",
259
+ });
260
+ });
261
+
262
+ test("declaredVenue: the label is the preset's, and an extends that resolves nowhere is refused", () => {
263
+ const ok = memoryFiles({
264
+ ...SHIPPED_PRESETS,
265
+ [`${PAPER}/paperlint.json`]: '{"extends":"paperlint:aisec"}',
266
+ });
267
+ assert.equal(declaredVenue(ok, PAPER)?.label, "aisec");
268
+ const typo = memoryFiles({
269
+ ...SHIPPED_PRESETS,
270
+ [`${PAPER}/paperlint.json`]: '{"extends":"paperlint:aisek"}',
271
+ });
272
+ assert.throws(() => declaredVenue(typo, PAPER), /shipped presets/);
273
+ });
274
+
275
+ test("declaredVenue: a paperlint.json with an unknown key is refused, naming the key", () => {
276
+ const files = memoryFiles({
277
+ [`${PAPER}/paperlint.json`]: '{"venu":"agenticdev"}',
278
+ });
279
+ assert.throws(() => declaredVenue(files, PAPER), /unknown key "venu"/);
240
280
  });