paperlint 2.1.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/CLAUDE.md +6 -1
  2. package/README.md +243 -235
  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-engine.d.ts.map +1 -1
  13. package/dist/build-engine.js +6 -2
  14. package/dist/build-engine.js.map +1 -1
  15. package/dist/build.d.ts +15 -0
  16. package/dist/build.d.ts.map +1 -1
  17. package/dist/build.js +60 -7
  18. package/dist/build.js.map +1 -1
  19. package/dist/cli.d.ts +94 -30
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +500 -288
  22. package/dist/cli.js.map +1 -1
  23. package/dist/doctor.d.ts.map +1 -1
  24. package/dist/doctor.js +40 -72
  25. package/dist/doctor.js.map +1 -1
  26. package/dist/engine.d.ts +9 -0
  27. package/dist/engine.d.ts.map +1 -1
  28. package/dist/engine.js +27 -0
  29. package/dist/engine.js.map +1 -1
  30. package/dist/facts-file.d.ts +2 -2
  31. package/dist/facts-file.js +2 -2
  32. package/dist/hooks-settings.d.ts +0 -21
  33. package/dist/hooks-settings.d.ts.map +1 -1
  34. package/dist/hooks-settings.js +17 -135
  35. package/dist/hooks-settings.js.map +1 -1
  36. package/dist/init.d.ts +39 -29
  37. package/dist/init.d.ts.map +1 -1
  38. package/dist/init.js +144 -173
  39. package/dist/init.js.map +1 -1
  40. package/dist/link-skills.d.ts +1 -1
  41. package/dist/link-skills.d.ts.map +1 -1
  42. package/dist/link-skills.js +11 -29
  43. package/dist/link-skills.js.map +1 -1
  44. package/dist/new-paper.d.ts +8 -1
  45. package/dist/new-paper.d.ts.map +1 -1
  46. package/dist/new-paper.js +49 -25
  47. package/dist/new-paper.js.map +1 -1
  48. package/dist/paper-files.d.ts +85 -0
  49. package/dist/paper-files.d.ts.map +1 -0
  50. package/dist/paper-files.js +96 -0
  51. package/dist/paper-files.js.map +1 -0
  52. package/dist/paper-settings.d.ts +13 -41
  53. package/dist/paper-settings.d.ts.map +1 -1
  54. package/dist/paper-settings.js +71 -71
  55. package/dist/paper-settings.js.map +1 -1
  56. package/dist/ports/check-references.d.ts +31 -0
  57. package/dist/ports/check-references.d.ts.map +1 -0
  58. package/dist/ports/check-references.js +7 -0
  59. package/dist/ports/check-references.js.map +1 -0
  60. package/dist/presets.d.ts +1 -1
  61. package/dist/presets.d.ts.map +1 -1
  62. package/dist/presets.js +4 -6
  63. package/dist/presets.js.map +1 -1
  64. package/dist/reference-rules.d.ts +47 -0
  65. package/dist/reference-rules.d.ts.map +1 -0
  66. package/dist/reference-rules.js +156 -0
  67. package/dist/reference-rules.js.map +1 -0
  68. package/dist/references.d.ts +36 -0
  69. package/dist/references.d.ts.map +1 -0
  70. package/dist/references.js +91 -0
  71. package/dist/references.js.map +1 -0
  72. package/dist/rules-config.d.ts +5 -3
  73. package/dist/rules-config.d.ts.map +1 -1
  74. package/dist/rules-config.js +15 -6
  75. package/dist/rules-config.js.map +1 -1
  76. package/dist/structure.js +3 -3
  77. package/dist/structure.js.map +1 -1
  78. package/dist/tex-requirements.js +2 -2
  79. package/dist/tex-requirements.js.map +1 -1
  80. package/dist/toolchain.d.ts +5 -1
  81. package/dist/toolchain.d.ts.map +1 -1
  82. package/dist/toolchain.js +22 -5
  83. package/dist/toolchain.js.map +1 -1
  84. package/dist/types.d.ts +26 -18
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/venue-rules.d.ts.map +1 -1
  87. package/dist/venue-rules.js +12 -10
  88. package/dist/venue-rules.js.map +1 -1
  89. package/docs/configuration.md +163 -104
  90. package/docs/e2e.md +1 -1
  91. package/docs/install.md +25 -40
  92. package/docs/optional-rules.md +5 -4
  93. package/docs/rules.md +65 -24
  94. package/docs/skills.md +80 -0
  95. package/eslint-rules/bib-reachable-entry.mjs +75 -0
  96. package/eslint-rules/latex-language.mjs +29 -1
  97. package/eslint-rules/paper-stages.harness.mjs +1 -117
  98. package/eslint-rules/paper-stages.mjs +0 -120
  99. package/eslint-rules/paper-stages.mutations.mjs +0 -76
  100. package/eslint-rules/paper-typography.mjs +306 -191
  101. package/eslint-rules/paper-typography.test.mjs +337 -0
  102. package/eslint-rules/papers.harness.mjs +8 -12
  103. package/eslint-rules/papers.mjs +17 -31
  104. package/eslint-rules/pdf-last-page-balance.mjs +4 -7
  105. package/eslint-rules/review-frontmatter.mjs +118 -0
  106. package/eslint-rules/review-frontmatter.schema.json +36 -0
  107. package/eslint-rules/review-frontmatter.test.mjs +118 -0
  108. package/eslint-rules/sibling-frontmatter.mjs +19 -0
  109. package/eslint-rules/sibling-frontmatter.schema.json +14 -0
  110. package/eslint.config.mjs +0 -15
  111. package/fixtures/real-markdown-paper/baseline.json +6 -7
  112. package/fixtures/toolchain-mirror/catalog.txt +1 -0
  113. package/fixtures/toolchain-mirror/install-tl +4 -1
  114. package/fixtures/toolchain-mirror/stub-fmtutil-sys +4 -0
  115. package/fixtures/toolchain-mirror/stub-tlmgr +15 -0
  116. package/hooks/hooks.harness.mjs +39 -98
  117. package/hooks/hooks.mutations.mjs +6 -6
  118. package/hooks/paper-edit-guard.hook.d.mts +2 -3
  119. package/hooks/paper-edit-guard.hook.mjs +31 -42
  120. package/hooks/paper-skills-nudge.hook.mjs +21 -19
  121. package/hooks/paper-status-gates.hook.mjs +21 -19
  122. package/hooks/paper-status-gates.sh +8 -13
  123. package/lib/paper-config.d.mts +15 -25
  124. package/lib/paper-config.harness.mjs +18 -35
  125. package/lib/paper-config.mjs +83 -105
  126. package/lib/paper-config.mutations.mjs +2 -2
  127. package/lib/skill-checks.mjs +1 -1
  128. package/lib/skill-trigger-cases.harness.mjs +9 -8
  129. package/lib/skill-trigger-cases.mjs +7 -15
  130. package/package.json +1 -1
  131. package/scripts/eslint-report-guard.mjs +1 -1
  132. package/scripts/harness-api.frozen.json +0 -3
  133. package/scripts/mutation-batteries.frozen.json +5 -14
  134. package/skills/analyze-sibling-paper/SKILL.md +16 -12
  135. package/skills/analyze-sibling-paper/SKILL.md.spec.ts +15 -11
  136. package/skills/cold-read-diff/SKILL.md +20 -1
  137. package/skills/cold-read-diff/SKILL.md.spec.ts +19 -0
  138. package/skills/grade-paper-writing/SKILL.md +20 -1
  139. package/skills/grade-paper-writing/SKILL.md.spec.ts +19 -0
  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/render-paper.harness.mjs +2 -2
  151. package/skills/verify-citations/SKILL.md +10 -1
  152. package/skills/verify-citations/SKILL.md.spec.ts +9 -0
  153. package/skills/verify-citations/scripts/bib-authors.mjs +48 -26
  154. package/src/adapters/references/index.ts +102 -0
  155. package/src/adapters/references/reach.io.ts +12 -0
  156. package/src/build-engine.ts +6 -1
  157. package/src/build.harness.mjs +54 -6
  158. package/src/build.ts +89 -11
  159. package/src/cli.harness.mjs +149 -294
  160. package/src/cli.mutations.mjs +35 -44
  161. package/src/cli.ts +637 -328
  162. package/src/doctor.harness.mjs +41 -84
  163. package/src/doctor.mutations.mjs +9 -9
  164. package/src/doctor.ts +44 -89
  165. package/src/engine.harness.mjs +50 -0
  166. package/src/engine.ts +35 -0
  167. package/src/facts-file.test.ts +0 -7
  168. package/src/facts-file.ts +2 -2
  169. package/src/hooks-settings.harness.mjs +7 -92
  170. package/src/hooks-settings.mutations.mjs +2 -17
  171. package/src/hooks-settings.ts +18 -162
  172. package/src/init.test.ts +206 -0
  173. package/src/init.ts +187 -231
  174. package/src/link-skills.harness.mjs +1 -38
  175. package/src/link-skills.mutations.mjs +9 -5
  176. package/src/link-skills.ts +10 -34
  177. package/src/lint-fix.test.ts +120 -0
  178. package/src/linted-set.test.ts +118 -0
  179. package/src/new-paper.harness.mjs +2 -0
  180. package/src/new-paper.mutations.mjs +0 -14
  181. package/src/new-paper.test.ts +173 -14
  182. package/src/new-paper.ts +82 -30
  183. package/src/paper-files.test.ts +395 -0
  184. package/src/paper-files.ts +200 -0
  185. package/src/paper-settings-commands.test.ts +121 -122
  186. package/src/paper-settings.test.ts +93 -64
  187. package/src/paper-settings.ts +99 -97
  188. package/src/parse-args.test.ts +48 -0
  189. package/src/ports/check-references.ts +28 -0
  190. package/src/presets.ts +5 -15
  191. package/src/reference-rules.ts +229 -0
  192. package/src/references.test.ts +209 -0
  193. package/src/references.ts +138 -0
  194. package/src/rules-config.harness.mjs +24 -8
  195. package/src/rules-config.ts +19 -7
  196. package/src/rules-off.test.ts +111 -0
  197. package/src/structure.harness.mjs +4 -2
  198. package/src/structure.ts +3 -3
  199. package/src/tex-requirements.ts +2 -2
  200. package/src/toolchain.harness.mjs +41 -6
  201. package/src/toolchain.ts +29 -5
  202. package/src/types.ts +36 -18
  203. package/src/venue-rules.test.ts +23 -11
  204. package/src/venue-rules.ts +12 -14
  205. package/templates/paper/paperlint.json +1 -1
  206. package/eslint-rules/doc-fields.harness.mjs +0 -336
  207. package/eslint-rules/doc-fields.mjs +0 -186
  208. package/eslint-rules/doc-fields.mutations.mjs +0 -96
  209. package/eslint-rules/paper-typography.harness.mjs +0 -291
  210. package/eslint-rules/paper-typography.mutations.mjs +0 -131
  211. package/eslint-rules/review-findings-cause.harness.mjs +0 -228
  212. package/eslint-rules/review-findings-cause.mjs +0 -135
  213. package/eslint-rules/review-findings-cause.mutations.mjs +0 -72
  214. package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +0 -16
  215. package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +0 -17
  216. package/fixtures/review-findings-cause/clean.md +0 -17
  217. package/fixtures/review-findings-cause/defect.md +0 -14
  218. package/fixtures/review-findings-cause/old-debt.md +0 -14
  219. package/fixtures/review-findings-cause/quiet-in-fence.md +0 -16
@@ -0,0 +1,206 @@
1
+ /**
2
+ * `paperlint init` over a project that ALREADY declares its papers directory: the declaration is
3
+ * the answer, and init reports it — not a directory it discovered elsewhere and then did not use.
4
+ *
5
+ * Seen migrating a consumer project: init printed "papers directory ✓ eslint-rules/fixtures — 4
6
+ * candidates", then kept the declared papersDir. The line described a decision that was not made.
7
+ */
8
+ import {
9
+ existsSync,
10
+ mkdirSync,
11
+ mkdtempSync,
12
+ readFileSync,
13
+ realpathSync,
14
+ rmSync,
15
+ writeFileSync,
16
+ } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join } from "node:path";
19
+ import { afterEach, describe, expect, it } from "vitest";
20
+ import { choosePapers, init } from "./init.ts";
21
+
22
+ const dirs: string[] = [];
23
+ afterEach(() => {
24
+ for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
25
+ });
26
+
27
+ /** Declares `papersDir: "papers"`, and holds two OTHER directories that look like papers roots. */
28
+ function project(): string {
29
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-init-")));
30
+ dirs.push(root);
31
+ const files: Record<string, string> = {
32
+ "package.json": JSON.stringify({ name: "c", private: true }),
33
+ "paperlint.json": JSON.stringify({ papersDir: "papers" }),
34
+ "papers/a/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
35
+ "eslint-rules/fixtures/x/PIPELINE-STATUS.md": "---\nstages: []\n---\n",
36
+ "eslint-rules/fixtures/y/paper.md": "# y\n",
37
+ "drafts/z/paper.md": "# z\n",
38
+ };
39
+ for (const [p, text] of Object.entries(files)) {
40
+ mkdirSync(join(root, p, ".."), { recursive: true });
41
+ writeFileSync(join(root, p), text);
42
+ }
43
+ return root;
44
+ }
45
+
46
+ describe("paperlint init — a declared papers directory", () => {
47
+ it("🔴 is the one reported, and no discovered candidate is named", async () => {
48
+ const root = project();
49
+ const out: string[] = [];
50
+ const code = await init(root, {
51
+ cwd: root,
52
+ log: (s: string) => out.push(s),
53
+ err: (s: string) => out.push(s),
54
+ interactive: false,
55
+ hooks: false,
56
+ run: (() => ({ status: 0 })) as never,
57
+ link: () => ({ ok: false, error: "not linked in this test" }),
58
+ });
59
+ const text = out.join("\n");
60
+ const section = text.slice(
61
+ text.indexOf("papers directory"),
62
+ text.indexOf("settings"),
63
+ );
64
+ expect(code).toBe(0);
65
+ expect(section).toMatch(/✓ papers — declared in paperlint\.json/);
66
+ expect(section).not.toMatch(/candidates|eslint-rules|drafts/);
67
+ const cfg = JSON.parse(readFileSync(join(root, "paperlint.json"), "utf8"));
68
+ expect(cfg).toEqual({ papersDir: "papers" });
69
+ });
70
+
71
+ it("is not asked about, even with a human at the terminal", async () => {
72
+ const root = project();
73
+ const asked: string[] = [];
74
+ await init(root, {
75
+ cwd: root,
76
+ log: () => {},
77
+ err: () => {},
78
+ interactive: true,
79
+ ask: async (q) => (asked.push(q), "n"),
80
+ hooks: false,
81
+ run: (() => ({ status: 0 })) as never,
82
+ link: () => ({ ok: false, error: "not linked in this test" }),
83
+ });
84
+ expect(asked.filter((q) => /papers roots/.test(q))).toEqual([]);
85
+ });
86
+
87
+ it("without a declaration, the candidates are still measured and offered", async () => {
88
+ const choice = await choosePapers(project(), { interactive: false });
89
+ expect(choice.how).toBe("not-asked");
90
+ expect(choice.candidates.length).toBeGreaterThan(1);
91
+ });
92
+ });
93
+
94
+ describe("paperlint init — writes paperlint.json only when something differs from the default", () => {
95
+ const quiet = {
96
+ log: () => {},
97
+ err: () => {},
98
+ interactive: false,
99
+ hooks: false,
100
+ run: (() => ({ status: 0 })) as never,
101
+ link: () => ({ ok: false as const, error: "not linked in this test" }),
102
+ };
103
+ const fresh = (files: Record<string, string>): string => {
104
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-init-")));
105
+ dirs.push(root);
106
+ for (const [p, text] of Object.entries(files)) {
107
+ mkdirSync(join(root, p, ".."), { recursive: true });
108
+ writeFileSync(join(root, p), text);
109
+ }
110
+ return root;
111
+ };
112
+
113
+ it("🔴 papers/ is where the papers are: no paperlint.json is written", async () => {
114
+ const root = fresh({ "papers/a/paper.tex": "x" });
115
+ await init(root, { cwd: root, ...quiet });
116
+ expect(existsSync(join(root, "paperlint.json"))).toBe(false);
117
+ });
118
+
119
+ it("nothing looks like papers: the default, and still no file", async () => {
120
+ const root = fresh({ "README.md": "x" });
121
+ await init(root, { cwd: root, ...quiet });
122
+ expect(existsSync(join(root, "paperlint.json"))).toBe(false);
123
+ });
124
+
125
+ it("papers elsewhere: { papersDir } is written, and other keys already there are kept", async () => {
126
+ const root = fresh({
127
+ "docs/drafts/a/paper.tex": "x",
128
+ "paperlint.json": '{ "kind": "short" }\n',
129
+ });
130
+ await init(root, { cwd: root, ...quiet });
131
+ expect(
132
+ JSON.parse(readFileSync(join(root, "paperlint.json"), "utf8")),
133
+ ).toEqual({ kind: "short", papersDir: "docs/drafts" });
134
+ });
135
+ });
136
+
137
+ /** Runs init in a fresh directory with the given TeX Live port; what it printed and asked. */
138
+ const texRun = async (
139
+ tex: { installed: () => boolean; install?: () => number },
140
+ { interactive, answer = "" }: { interactive: boolean; answer?: string },
141
+ ) => {
142
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "paperlint-tex-")));
143
+ dirs.push(root);
144
+ const out: string[] = [];
145
+ const asked: string[] = [];
146
+ await init(root, {
147
+ cwd: root,
148
+ log: (s: string) => out.push(s),
149
+ err: () => {},
150
+ interactive,
151
+ ask: async (q) => (asked.push(q), /TeX Live/.test(q) ? answer : ""),
152
+ hooks: false,
153
+ run: (() => ({ status: 0 })) as never,
154
+ link: () => ({ ok: false as const, error: "not linked in this test" }),
155
+ tex,
156
+ });
157
+ return { text: out.join("\n"), asked };
158
+ };
159
+
160
+ describe("paperlint init — TeX Live is offered with its cost, never installed unasked", () => {
161
+ it("🔴 no terminal: nothing is asked or installed, and the command is the next step", async () => {
162
+ let installs = 0;
163
+ const r = await texRun(
164
+ { installed: () => false, install: () => (installs++, 0) },
165
+ { interactive: false },
166
+ );
167
+ expect(installs).toBe(0);
168
+ expect(r.asked.filter((q) => /TeX Live/.test(q))).toEqual([]);
169
+ expect(r.text).toMatch(
170
+ /npx paperlint toolchain {3}# ~270 MB, ~3 min, once/,
171
+ );
172
+ expect(r.text).toMatch(/next: .*\n.*npx paperlint toolchain/);
173
+ });
174
+
175
+ it("a human is asked, with the size in the question; yes installs once", async () => {
176
+ let installs = 0;
177
+ const r = await texRun(
178
+ { installed: () => false, install: () => (installs++, 0) },
179
+ { interactive: true, answer: "y" },
180
+ );
181
+ expect(r.asked.find((q) => /TeX Live/.test(q))).toMatch(
182
+ /~270 MB, ~3 min, once\) \[y\/N\]/,
183
+ );
184
+ expect(installs).toBe(1);
185
+ expect(r.text).not.toMatch(/next: .*\n.*npx paperlint toolchain/);
186
+ });
187
+
188
+ it("Enter is no — nothing installed", async () => {
189
+ let installs = 0;
190
+ const r = await texRun(
191
+ { installed: () => false, install: () => (installs++, 0) },
192
+ { interactive: true },
193
+ );
194
+ expect(installs).toBe(0);
195
+ expect(r.text).toMatch(/declined — nothing installed/);
196
+ });
197
+
198
+ it("already installed: not asked about", async () => {
199
+ const r = await texRun(
200
+ { installed: () => true, install: () => 0 },
201
+ { interactive: true, answer: "y" },
202
+ );
203
+ expect(r.asked.filter((q) => /TeX Live/.test(q))).toEqual([]);
204
+ expect(r.text).toMatch(/✓ paperlint's TeX Live is installed/);
205
+ });
206
+ });