paperlint 3.1.2 → 3.2.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 (374) hide show
  1. package/.github/workflows/ci.yml +12 -7
  2. package/CLAUDE.md +3 -3
  3. package/CONTRIBUTING.md +34 -5
  4. package/dist/adapters/banal/index.d.ts.map +1 -1
  5. package/dist/adapters/banal/index.js +9 -5
  6. package/dist/adapters/banal/index.js.map +1 -1
  7. package/dist/adapters/banal/invocation.d.ts +1 -1
  8. package/dist/adapters/node/files.io.d.ts.map +1 -1
  9. package/dist/adapters/node/files.io.js +2 -1
  10. package/dist/adapters/node/files.io.js.map +1 -1
  11. package/dist/adapters/node/process.io.d.ts +0 -5
  12. package/dist/adapters/node/process.io.d.ts.map +1 -1
  13. package/dist/adapters/node/process.io.js +4 -3
  14. package/dist/adapters/node/process.io.js.map +1 -1
  15. package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
  16. package/dist/adapters/pdfjs/fill.js +13 -7
  17. package/dist/adapters/pdfjs/fill.js.map +1 -1
  18. package/dist/adapters/references/index.d.ts.map +1 -1
  19. package/dist/adapters/references/index.js +4 -5
  20. package/dist/adapters/references/index.js.map +1 -1
  21. package/dist/adapters/references/pool.d.ts.map +1 -1
  22. package/dist/adapters/references/pool.js +4 -5
  23. package/dist/adapters/references/pool.js.map +1 -1
  24. package/dist/adapters/references/reach.io.d.ts.map +1 -1
  25. package/dist/adapters/references/reach.io.js +2 -1
  26. package/dist/adapters/references/reach.io.js.map +1 -1
  27. package/dist/build-engine.js +2 -2
  28. package/dist/build-engine.js.map +1 -1
  29. package/dist/build.d.ts.map +1 -1
  30. package/dist/build.js +14 -11
  31. package/dist/build.js.map +1 -1
  32. package/dist/cli.d.ts.map +1 -1
  33. package/dist/cli.js +31 -34
  34. package/dist/cli.js.map +1 -1
  35. package/dist/doctor.d.ts.map +1 -1
  36. package/dist/doctor.js +3 -2
  37. package/dist/doctor.js.map +1 -1
  38. package/dist/domain/lookup-cache.d.ts.map +1 -1
  39. package/dist/domain/lookup-cache.js +10 -8
  40. package/dist/domain/lookup-cache.js.map +1 -1
  41. package/dist/domain/text.d.ts +12 -0
  42. package/dist/domain/text.d.ts.map +1 -1
  43. package/dist/domain/text.js +17 -0
  44. package/dist/domain/text.js.map +1 -1
  45. package/dist/engine.d.ts.map +1 -1
  46. package/dist/engine.js +4 -3
  47. package/dist/engine.js.map +1 -1
  48. package/dist/eslint-rules/bib-reachable-entry.d.ts +22 -0
  49. package/dist/eslint-rules/bib-reachable-entry.d.ts.map +1 -0
  50. package/dist/eslint-rules/bib-reachable-entry.js +75 -0
  51. package/dist/eslint-rules/bib-reachable-entry.js.map +1 -0
  52. package/dist/eslint-rules/install-path-literals.d.ts +80 -0
  53. package/dist/eslint-rules/install-path-literals.d.ts.map +1 -0
  54. package/dist/eslint-rules/install-path-literals.js +105 -0
  55. package/dist/eslint-rules/install-path-literals.js.map +1 -0
  56. package/dist/eslint-rules/latex-language.d.ts +136 -0
  57. package/dist/eslint-rules/latex-language.d.ts.map +1 -0
  58. package/dist/eslint-rules/latex-language.js +639 -0
  59. package/dist/eslint-rules/latex-language.js.map +1 -0
  60. package/dist/eslint-rules/paper-research-question.d.ts +31 -0
  61. package/dist/eslint-rules/paper-research-question.d.ts.map +1 -0
  62. package/dist/eslint-rules/paper-research-question.js +188 -0
  63. package/dist/eslint-rules/paper-research-question.js.map +1 -0
  64. package/dist/eslint-rules/paper-stages.d.ts +83 -0
  65. package/dist/eslint-rules/paper-stages.d.ts.map +1 -0
  66. package/dist/eslint-rules/paper-stages.js +358 -0
  67. package/dist/eslint-rules/paper-stages.js.map +1 -0
  68. package/dist/eslint-rules/paper-typography.d.ts +74 -0
  69. package/dist/eslint-rules/paper-typography.d.ts.map +1 -0
  70. package/dist/eslint-rules/paper-typography.js +404 -0
  71. package/dist/eslint-rules/paper-typography.js.map +1 -0
  72. package/dist/eslint-rules/papers.d.ts +42 -0
  73. package/dist/eslint-rules/papers.d.ts.map +1 -0
  74. package/dist/eslint-rules/papers.js +145 -0
  75. package/dist/eslint-rules/papers.js.map +1 -0
  76. package/dist/eslint-rules/pdf-last-page-balance.d.ts +72 -0
  77. package/dist/eslint-rules/pdf-last-page-balance.d.ts.map +1 -0
  78. package/dist/eslint-rules/pdf-last-page-balance.js +237 -0
  79. package/dist/eslint-rules/pdf-last-page-balance.js.map +1 -0
  80. package/dist/eslint-rules/review-frontmatter.d.ts +57 -0
  81. package/dist/eslint-rules/review-frontmatter.d.ts.map +1 -0
  82. package/dist/eslint-rules/review-frontmatter.js +126 -0
  83. package/dist/eslint-rules/review-frontmatter.js.map +1 -0
  84. package/dist/eslint-rules/review-frontmatter.schema.json +36 -0
  85. package/dist/eslint-rules/rule-context.d.ts +55 -0
  86. package/dist/eslint-rules/rule-context.d.ts.map +1 -0
  87. package/dist/eslint-rules/rule-context.js +9 -0
  88. package/dist/eslint-rules/rule-context.js.map +1 -0
  89. package/dist/eslint-rules/sibling-frontmatter.d.ts +25 -0
  90. package/dist/eslint-rules/sibling-frontmatter.d.ts.map +1 -0
  91. package/dist/eslint-rules/sibling-frontmatter.js +16 -0
  92. package/dist/eslint-rules/sibling-frontmatter.js.map +1 -0
  93. package/dist/eslint-rules/sibling-frontmatter.schema.json +14 -0
  94. package/dist/eslint-rules/temp-root-realpath.d.ts +71 -0
  95. package/dist/eslint-rules/temp-root-realpath.d.ts.map +1 -0
  96. package/dist/eslint-rules/temp-root-realpath.js +139 -0
  97. package/dist/eslint-rules/temp-root-realpath.js.map +1 -0
  98. package/dist/eslint-rules/tex-build.d.ts +35 -0
  99. package/dist/eslint-rules/tex-build.d.ts.map +1 -0
  100. package/dist/eslint-rules/tex-build.js +313 -0
  101. package/dist/eslint-rules/tex-build.js.map +1 -0
  102. package/dist/facts-file.d.ts.map +1 -1
  103. package/dist/facts-file.js +2 -1
  104. package/dist/facts-file.js.map +1 -1
  105. package/dist/hooks-settings.d.ts +1 -1
  106. package/dist/hooks-settings.d.ts.map +1 -1
  107. package/dist/hooks-settings.js +14 -9
  108. package/dist/hooks-settings.js.map +1 -1
  109. package/dist/init.d.ts +5 -5
  110. package/dist/init.d.ts.map +1 -1
  111. package/dist/init.js +15 -7
  112. package/dist/init.js.map +1 -1
  113. package/dist/latex-loop.d.ts +2 -2
  114. package/dist/latex-loop.d.ts.map +1 -1
  115. package/dist/latex-loop.js +2 -2
  116. package/dist/latex-loop.js.map +1 -1
  117. package/dist/lib/agent-cli-version.d.ts +65 -0
  118. package/dist/lib/agent-cli-version.d.ts.map +1 -0
  119. package/dist/lib/agent-cli-version.js +77 -0
  120. package/dist/lib/agent-cli-version.js.map +1 -0
  121. package/dist/lib/check.d.ts +19 -0
  122. package/dist/lib/check.d.ts.map +1 -0
  123. package/dist/lib/check.js +52 -0
  124. package/dist/lib/check.js.map +1 -0
  125. package/dist/lib/markdown.d.ts +180 -0
  126. package/dist/lib/markdown.d.ts.map +1 -0
  127. package/dist/lib/markdown.js +302 -0
  128. package/dist/lib/markdown.js.map +1 -0
  129. package/dist/lib/paper-config.d.ts +84 -0
  130. package/dist/lib/paper-config.d.ts.map +1 -0
  131. package/dist/lib/paper-config.js +142 -0
  132. package/dist/lib/paper-config.js.map +1 -0
  133. package/dist/lib/skill-eval-fixture.d.ts +29 -0
  134. package/dist/lib/skill-eval-fixture.d.ts.map +1 -0
  135. package/dist/lib/skill-eval-fixture.js +62 -0
  136. package/dist/lib/skill-eval-fixture.js.map +1 -0
  137. package/dist/link-skills.d.ts.map +1 -1
  138. package/dist/link-skills.js +5 -4
  139. package/dist/link-skills.js.map +1 -1
  140. package/dist/new-paper.d.ts.map +1 -1
  141. package/dist/new-paper.js +2 -2
  142. package/dist/new-paper.js.map +1 -1
  143. package/dist/paper-settings.d.ts.map +1 -1
  144. package/dist/paper-settings.js +5 -4
  145. package/dist/paper-settings.js.map +1 -1
  146. package/dist/pdf-facts.d.ts.map +1 -1
  147. package/dist/pdf-facts.js +2 -1
  148. package/dist/pdf-facts.js.map +1 -1
  149. package/dist/pdf-geometry.d.ts.map +1 -1
  150. package/dist/pdf-geometry.js +1 -1
  151. package/dist/pdf-geometry.js.map +1 -1
  152. package/dist/presets.d.ts.map +1 -1
  153. package/dist/presets.js +3 -2
  154. package/dist/presets.js.map +1 -1
  155. package/dist/references.d.ts.map +1 -1
  156. package/dist/references.js +11 -6
  157. package/dist/references.js.map +1 -1
  158. package/dist/rules-config.d.ts.map +1 -1
  159. package/dist/rules-config.js +6 -3
  160. package/dist/rules-config.js.map +1 -1
  161. package/dist/structure.js +1 -1
  162. package/dist/structure.js.map +1 -1
  163. package/dist/tex-requirements.d.ts.map +1 -1
  164. package/dist/tex-requirements.js +8 -3
  165. package/dist/tex-requirements.js.map +1 -1
  166. package/dist/toolchain.d.ts.map +1 -1
  167. package/dist/toolchain.js +21 -16
  168. package/dist/toolchain.js.map +1 -1
  169. package/dist/venue-rules.js +1 -1
  170. package/dist/venue-rules.js.map +1 -1
  171. package/docs/e2e.md +7 -7
  172. package/docs/install.md +1 -1
  173. package/docs/prior-art/README.md +1 -1
  174. package/docs/prior-art/package-location.md +5 -5
  175. package/docs/prior-art/repro/README.md +1 -1
  176. package/docs/prior-art/test-tooling.md +2 -2
  177. package/docs/testing.md +11 -8
  178. package/docs/toolchain.md +1 -1
  179. package/eslint-rules/{bib-reachable-entry.mjs → bib-reachable-entry.ts} +4 -3
  180. package/eslint-rules/install-path-literals.harness.mjs +1 -1
  181. package/eslint-rules/{install-path-literals.mjs → install-path-literals.ts} +16 -8
  182. package/eslint-rules/latex-language.harness.mjs +2 -2
  183. package/eslint-rules/latex-language.test.mjs +7 -1
  184. package/eslint-rules/{latex-language.mjs → latex-language.ts} +297 -109
  185. package/eslint-rules/paper-research-question.harness.mjs +3 -3
  186. package/eslint-rules/{paper-research-question.mjs → paper-research-question.ts} +34 -15
  187. package/eslint-rules/paper-stages.harness.mjs +2 -2
  188. package/eslint-rules/paper-stages.test.mjs +8 -2
  189. package/eslint-rules/{paper-stages.mjs → paper-stages.ts} +74 -29
  190. package/eslint-rules/paper-typography.test.mjs +3 -3
  191. package/eslint-rules/{paper-typography.mjs → paper-typography.ts} +132 -54
  192. package/eslint-rules/papers.harness.mjs +2 -2
  193. package/eslint-rules/{papers.mjs → papers.ts} +7 -4
  194. package/eslint-rules/pdf-last-page-balance.harness.mjs +3 -3
  195. package/eslint-rules/{pdf-last-page-balance.mjs → pdf-last-page-balance.ts} +88 -31
  196. package/eslint-rules/review-frontmatter.test.mjs +2 -2
  197. package/eslint-rules/{review-frontmatter.mjs → review-frontmatter.ts} +37 -15
  198. package/eslint-rules/rule-context.ts +62 -0
  199. package/eslint-rules/{sibling-frontmatter.mjs → sibling-frontmatter.ts} +1 -1
  200. package/eslint-rules/temp-root-realpath.harness.mjs +1 -1
  201. package/eslint-rules/{temp-root-realpath.mjs → temp-root-realpath.ts} +19 -6
  202. package/eslint-rules/tex-build.harness.mjs +2 -2
  203. package/eslint-rules/{tex-build.mjs → tex-build.ts} +4 -3
  204. package/eslint.config.mjs +139 -30
  205. package/fixtures/pdf-facts/README.md +2 -2
  206. package/fixtures/real-markdown-paper/{baseline.mjs → baseline.ts} +31 -10
  207. package/hooks/{hooks.harness.mjs → hooks.harness.ts} +51 -34
  208. package/hooks/paper-edit-guard.hook.mjs +1 -1
  209. package/hooks/{paper-edit-guard.test.mjs → paper-edit-guard.test.ts} +3 -7
  210. package/hooks/paper-skills-nudge.hook.mjs +1 -1
  211. package/hooks/{paper-skills-nudge.test.mjs → paper-skills-nudge.test.ts} +2 -6
  212. package/hooks/{paper-status-gates.test.mjs → paper-status-gates.test.ts} +2 -6
  213. package/lib/agent-cli-version.harness.mjs +1 -1
  214. package/lib/{agent-cli-version.mjs → agent-cli-version.ts} +32 -18
  215. package/lib/check.test.ts +14 -9
  216. package/lib/{check.mjs → check.ts} +22 -9
  217. package/lib/markdown.test.mjs +7 -7
  218. package/lib/{markdown.mjs → markdown.ts} +68 -29
  219. package/lib/paper-config.harness.mjs +8 -8
  220. package/lib/{paper-config.mjs → paper-config.ts} +19 -14
  221. package/lib/skill-checks.mjs +1 -1
  222. package/lib/skill-checks.test.mjs +1 -1
  223. package/lib/skill-corpus.mjs +1 -1
  224. package/lib/skill-eval-kit.mjs +1 -1
  225. package/package.json +42 -10
  226. package/scripts/{check.harness.mjs → check.harness.ts} +111 -10
  227. package/scripts/{check.test.mjs → check.test.ts} +13 -10
  228. package/scripts/{check.mjs → check.ts} +116 -42
  229. package/scripts/coverage-config.test.ts +80 -37
  230. package/scripts/eslint-report-guard.test.mjs +1 -1
  231. package/scripts/harness-api.frozen.json +14 -15
  232. package/scripts/harness-api.test.ts +8 -5
  233. package/scripts/layer-legacy-frozen.test.ts +17 -13
  234. package/scripts/{layer-legacy-frozen.mjs → layer-legacy-frozen.ts} +128 -39
  235. package/scripts/release-config.test.ts +12 -4
  236. package/scripts/rules-see-files.test.mjs +1 -1
  237. package/scripts/test.test.ts +78 -0
  238. package/scripts/test.ts +87 -0
  239. package/skills/grade-paper-writing/prose-lint.mjs +1 -1
  240. package/skills/grade-paper-writing/{prose-lint.test.mjs → prose-lint.test.ts} +2 -2
  241. package/skills/paper-pipeline/description-language.eval.mjs +1 -1
  242. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +1 -1
  243. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +1 -1
  244. package/skills/paper-pipeline/pipeline-firing.eval.mjs +1 -1
  245. package/skills/paper-pipeline/pipeline-language.eval.mjs +1 -1
  246. package/skills/paper-pipeline/scripts/README.md +1 -1
  247. package/skills/paper-pipeline/scripts/announce.test.mjs +1 -1
  248. package/skills/paper-pipeline/scripts/artifact-coverage.mjs +1 -1
  249. package/skills/paper-pipeline/scripts/{artifact-coverage.test.mjs → artifact-coverage.test.ts} +2 -2
  250. package/skills/paper-pipeline/scripts/consumer.mjs +1 -5
  251. package/skills/paper-pipeline/scripts/consumer.test.mjs +1 -1
  252. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +1 -1
  253. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +1 -4
  254. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +1 -1
  255. package/skills/paper-pipeline/scripts/fixtures/{scorecard.mjs → scorecard.ts} +36 -7
  256. package/skills/paper-pipeline/scripts/{generated-code.test.mjs → generated-code.test.ts} +3 -3
  257. package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +1 -1
  258. package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +2 -2
  259. package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -2
  260. package/skills/paper-pipeline/scripts/pipeline-check.mjs +1 -4
  261. package/skills/paper-pipeline/scripts/population-map.mjs +1 -4
  262. package/skills/paper-pipeline/scripts/population-map.test.mjs +1 -1
  263. package/skills/paper-pipeline/scripts/{round-diff.harness.mjs → round-diff.harness.ts} +45 -18
  264. package/skills/paper-pipeline/scripts/round-diff.mjs +1 -1
  265. package/skills/paper-pipeline/scripts/round-diff.test.mjs +1 -1
  266. package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +1 -1
  267. package/skills/paper-pipeline/scripts/status.test.mjs +1 -1
  268. package/skills/plan-paper-timeline/fixtures/{fake-google-calendar.mjs → fake-google-calendar.ts} +101 -70
  269. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +3 -3
  270. package/skills/render-paper/{extract-pdf-facts.harness.mjs → extract-pdf-facts.harness.ts} +34 -9
  271. package/skills/render-paper/render-paper.harness.mjs +2 -2
  272. package/skills/tighten-paper/structure.mjs +1 -1
  273. package/skills/tighten-paper/{structure.test.mjs → structure.test.ts} +4 -3
  274. package/skills/verify-citations/scripts/{bib-authors.cli.test.mjs → bib-authors.cli.test.ts} +14 -4
  275. package/skills/verify-citations/scripts/bib-authors.d.mts +63 -0
  276. package/skills/verify-citations/scripts/bib-authors.mjs +1 -4
  277. package/skills/verify-citations/scripts/{verify-cites.cli.test.mjs → verify-cites.cli.test.ts} +32 -9
  278. package/skills/verify-citations/scripts/verify-cites.d.mts +58 -0
  279. package/skills/verify-citations/scripts/verify-cites.mjs +11 -1
  280. package/skills/verify-citations/scripts/verify-cites.net.test.mjs +35 -0
  281. package/skills/verify-citations/scripts/verify-cites.test.mjs +14 -0
  282. package/src/CLAUDE.md +1 -1
  283. package/src/adapters/banal/index.test.ts +3 -6
  284. package/src/adapters/banal/index.ts +7 -4
  285. package/src/adapters/banal/invocation.test.ts +3 -1
  286. package/src/adapters/banal/invocation.ts +1 -1
  287. package/src/adapters/banal/output.test.ts +1 -1
  288. package/src/adapters/banal/run.test.ts +3 -3
  289. package/src/adapters/banal/xml.test.ts +1 -1
  290. package/src/adapters/curl/download.test.ts +4 -2
  291. package/src/adapters/node/files.io.ts +2 -2
  292. package/src/adapters/node/files.test.ts +3 -1
  293. package/src/adapters/node/process.io.ts +4 -3
  294. package/src/adapters/node/process.test.ts +1 -1
  295. package/src/adapters/node/workspace.test.ts +3 -1
  296. package/src/adapters/pdfjs/fill.test.ts +2 -2
  297. package/src/adapters/pdfjs/fill.ts +14 -7
  298. package/src/adapters/references/index.test.ts +31 -38
  299. package/src/adapters/references/index.ts +26 -58
  300. package/src/adapters/references/pool.ts +3 -5
  301. package/src/adapters/references/reach.io.ts +2 -1
  302. package/src/build-engine.harness.mjs +1 -1
  303. package/src/build-engine.test.ts +2 -2
  304. package/src/build-engine.ts +2 -2
  305. package/src/build.harness.mjs +2 -2
  306. package/src/build.test.ts +15 -14
  307. package/src/build.ts +28 -19
  308. package/src/cli.harness.mjs +6 -6
  309. package/src/cli.test.ts +13 -6
  310. package/src/cli.ts +39 -37
  311. package/src/doctor.harness.mjs +3 -3
  312. package/src/doctor.ts +3 -2
  313. package/src/domain/lookup-cache.ts +21 -11
  314. package/src/domain/result.test.ts +2 -2
  315. package/src/domain/text.test.ts +32 -0
  316. package/src/domain/text.ts +23 -0
  317. package/src/engine.harness.mjs +2 -2
  318. package/src/engine.ts +4 -3
  319. package/src/facts-file.test.ts +9 -7
  320. package/src/facts-file.ts +2 -1
  321. package/src/{hooks-settings.harness.mjs → hooks-settings.harness.ts} +39 -20
  322. package/src/hooks-settings.ts +20 -9
  323. package/src/init.test.ts +12 -9
  324. package/src/init.ts +22 -12
  325. package/src/{latex-log.harness.mjs → latex-log.harness.ts} +14 -14
  326. package/src/{latex-loop.harness.mjs → latex-loop.harness.ts} +64 -36
  327. package/src/latex-loop.ts +3 -3
  328. package/src/{link-skills.harness.mjs → link-skills.harness.ts} +19 -19
  329. package/src/link-skills.test.ts +3 -3
  330. package/src/link-skills.ts +5 -4
  331. package/src/lint-fix.test.ts +4 -6
  332. package/src/linted-set.test.ts +2 -2
  333. package/src/new-paper.harness.mjs +2 -2
  334. package/src/new-paper.test.ts +8 -10
  335. package/src/new-paper.ts +2 -2
  336. package/src/paper-files.test.ts +3 -5
  337. package/src/paper-settings-commands.test.ts +5 -4
  338. package/src/paper-settings.test.ts +5 -5
  339. package/src/paper-settings.ts +8 -9
  340. package/src/{pdf-facts.harness.mjs → pdf-facts.harness.ts} +9 -8
  341. package/src/pdf-facts.test.ts +10 -11
  342. package/src/pdf-facts.ts +2 -1
  343. package/src/{pdf-geometry.harness.mjs → pdf-geometry.harness.ts} +19 -12
  344. package/src/pdf-geometry.ts +3 -1
  345. package/src/presets.ts +3 -2
  346. package/src/references.test.ts +51 -43
  347. package/src/references.ts +14 -7
  348. package/src/{rules-config.harness.mjs → rules-config.harness.ts} +7 -13
  349. package/src/rules-config.ts +7 -4
  350. package/src/rules-off.test.ts +6 -4
  351. package/src/{structure.harness.mjs → structure.harness.ts} +26 -15
  352. package/src/structure.ts +1 -1
  353. package/src/{tex-requirements.harness.mjs → tex-requirements.harness.ts} +50 -22
  354. package/src/tex-requirements.ts +8 -5
  355. package/src/toolchain.harness.mjs +2 -2
  356. package/src/toolchain.test.ts +1 -1
  357. package/src/toolchain.ts +31 -17
  358. package/src/venue-rules.test.ts +62 -64
  359. package/src/venue-rules.ts +1 -1
  360. package/tsconfig.json +6 -3
  361. package/tsconfig.pkg.json +10 -0
  362. package/lib/check.d.mts +0 -17
  363. package/lib/paper-config.d.mts +0 -31
  364. package/scripts/layer-legacy-frozen.d.mts +0 -36
  365. package/scripts/rules-are-content-only.harness.mjs +0 -105
  366. package/scripts/rules-are-content-only.mjs +0 -148
  367. package/scripts/rules-are-content-only.test.mjs +0 -61
  368. package/scripts/test.mjs +0 -44
  369. package/scripts/test.test.mjs +0 -52
  370. /package/bin/{paperlint.test.mjs → paperlint.test.ts} +0 -0
  371. /package/lib/{skill-eval-fixture.mjs → skill-eval-fixture.ts} +0 -0
  372. /package/skills/grade-paper-writing/__snapshots__/{prose-lint.test.mjs.snap → prose-lint.test.ts.snap} +0 -0
  373. /package/skills/osf-artifact-upload/{osf-artifact-upload.harness.mjs → osf-artifact-upload.harness.ts} +0 -0
  374. /package/skills/tighten-paper/__snapshots__/{structure.test.mjs.snap → structure.test.ts.snap} +0 -0
@@ -13,11 +13,7 @@ import { describe, expect, it } from "vitest";
13
13
  import { memoryFiles } from "./adapters/memory/index.ts";
14
14
  import { sha256Hex } from "./domain/sha256.ts";
15
15
  import { packageVenuesDir } from "../skills/paper-pipeline/scripts/consumer.mjs";
16
- import {
17
- VENUE_RULE_LEVELS,
18
- venueRules,
19
- type VenueRuleModule,
20
- } from "./venue-rules.ts";
16
+ import { VENUE_RULE_LEVELS, venueRules } from "./venue-rules.ts";
21
17
  import { buildConfig, OPTIONAL_RULES, SHIPPED_RULES } from "./cli.ts";
22
18
 
23
19
  const VENUES = packageVenuesDir();
@@ -34,9 +30,18 @@ const shipped = Object.fromEntries(
34
30
  );
35
31
 
36
32
  type Json = Record<string, unknown>;
33
+ interface Font {
34
+ name: string;
35
+ type: string;
36
+ embedded: boolean;
37
+ program: string;
38
+ }
39
+ /** The facts file as these tests edit it: free-form, except the fonts they reach into. */
40
+ type Facts = Json & { fonts: Font[] };
41
+ type Patch = (f: Facts) => void;
37
42
 
38
43
  /** The facts of a short agenticdev paper that meets every number in its profile. */
39
- function goodFacts(): Json {
44
+ function goodFacts(): Facts {
40
45
  return {
41
46
  schema: 2,
42
47
  pdf: "paper.pdf",
@@ -118,10 +123,7 @@ function lint(
118
123
  const files = memoryFiles(paperFiles(p));
119
124
  const rules = venueRules({ files, venuesDir: VENUES });
120
125
  const out: Finding[] = [];
121
- for (const [name, rule] of Object.entries(rules) as [
122
- string,
123
- VenueRuleModule,
124
- ][]) {
126
+ for (const [name, rule] of Object.entries(rules)) {
125
127
  const visitor = rule.create({
126
128
  filename,
127
129
  ...(cwd === undefined ? {} : { cwd }),
@@ -147,7 +149,13 @@ function lint(
147
149
  const DECL = { extends: "paperlint:agenticdev", kind: "short" };
148
150
  const ids = (fs: readonly Finding[]) =>
149
151
  fs.map((f) => `${f.rule}:${f.messageId}`).sort();
150
- const withFacts = (patch: (f: Json) => void): Json => {
152
+ /** A patch that changes one field set of the `i`-th font. */
153
+ const font =
154
+ (i: number, change: Partial<Font>): Patch =>
155
+ (f) => {
156
+ f.fonts = f.fonts.map((x, j) => (j === i ? { ...x, ...change } : x));
157
+ };
158
+ const withFacts = (patch: Patch): Facts => {
151
159
  const f = goodFacts();
152
160
  patch(f);
153
161
  return f;
@@ -260,7 +268,7 @@ describe("pdf/profile — the kind", () => {
260
268
  // the fonts rule still runs with an unresolved kind
261
269
  const fonts = lint({
262
270
  venue,
263
- facts: withFacts((f) => ((f.fonts as Json[])[0]!.embedded = false)),
271
+ facts: withFacts(font(0, { embedded: false })),
264
272
  });
265
273
  expect(ids(fonts)).toEqual([
266
274
  `pdf/fonts:notEmbedded`,
@@ -338,7 +346,7 @@ describe("pdf/measured — a paper that names a venue and was not measured says
338
346
  "pages_by_type",
339
347
  ])
340
348
  f[k] = null;
341
- (f.fonts as Json[])[0]!.program = "Type3";
349
+ font(0, { program: "Type3" })(f);
342
350
  });
343
351
  expect(ids(lint({ venue: DECL, facts: g }))).toEqual([
344
352
  "pdf/fonts:type3",
@@ -348,7 +356,7 @@ describe("pdf/measured — a paper that names a venue and was not measured says
348
356
  });
349
357
 
350
358
  describe("pdf/fresh — facts about another build are not judged", () => {
351
- it.each([
359
+ it.each<[string, Paper, string]>([
352
360
  [
353
361
  "a changed PDF",
354
362
  { pdf: new TextEncoder().encode("another build") },
@@ -364,55 +372,48 @@ describe("pdf/fresh — facts about another build are not judged", () => {
364
372
  ],
365
373
  [
366
374
  "a font entry of the wrong shape",
367
- { facts: withFacts((f) => (f.fonts = [{ name: 3 }])) },
375
+ { facts: { ...goodFacts(), fonts: [{ name: 3 }] } },
368
376
  "factsBroken",
369
377
  ],
370
- ] as [string, Paper, string][])(
371
- "%s: one finding, from pdf/fresh only",
372
- (_, p, messageId) => {
373
- // Everything else planted wrong too — none of it may be reported over stale facts.
374
- const facts =
375
- p.facts ?? withFacts((f) => ((f.body_pages = 99), (f.columns = 1)));
376
- expect(ids(lint({ venue: DECL, ...p, facts }))).toEqual([
377
- `pdf/fresh:${messageId}`,
378
- ]);
379
- },
380
- );
378
+ ])("%s: one finding, from pdf/fresh only", (_, p, messageId) => {
379
+ // Everything else planted wrong too — none of it may be reported over stale facts.
380
+ const facts =
381
+ p.facts ?? withFacts((f) => ((f.body_pages = 99), (f.columns = 1)));
382
+ expect(ids(lint({ venue: DECL, ...p, facts }))).toEqual([
383
+ `pdf/fresh:${messageId}`,
384
+ ]);
385
+ });
381
386
  });
382
387
 
383
388
  describe("pdf/fonts", () => {
384
- it.each([
385
- [
386
- "a Type 3 font",
387
- (f: Json) => ((f.fonts as Json[])[0]!.program = "Type3"),
388
- ["pdf/fonts:type3"],
389
- ],
389
+ it.each<[string, (f: Facts) => void, string[]]>([
390
+ ["a Type 3 font", font(0, { program: "Type3" }), ["pdf/fonts:type3"]],
390
391
  [
391
392
  "a font not embedded",
392
- (f: Json) => ((f.fonts as Json[])[1]!.embedded = false),
393
+ font(1, { embedded: false }),
393
394
  ["pdf/fonts:notEmbedded"],
394
395
  ],
395
396
  [
396
397
  "the text family missing — acmart fell back to Computer Modern",
397
- (f: Json) => ((f.fonts as Json[])[0]!.name = "CMR10"),
398
+ font(0, { name: "CMR10" }),
398
399
  ["pdf/fonts:noFamily"],
399
400
  ],
400
401
  [
401
402
  "both families missing: one finding per family",
402
- (f: Json) =>
403
+ (f: Facts) =>
403
404
  (f.fonts = [
404
405
  { name: "CMR10", type: "Type 1", embedded: true, program: "Type1" },
405
406
  ]),
406
407
  ["pdf/fonts:noFamily", "pdf/fonts:noFamily"],
407
408
  ],
408
- ] as [string, (f: Json) => void, string[]][])("%s", (_, patch, want) => {
409
+ ])("%s", (_, patch, want) => {
409
410
  expect(ids(lint({ venue: DECL, facts: withFacts(patch) }))).toEqual(want);
410
411
  });
411
412
 
412
413
  it("names the family and the fonts the PDF does have", () => {
413
414
  const [f] = lint({
414
415
  venue: DECL,
415
- facts: withFacts((x) => ((x.fonts as Json[])[0]!.name = "CMR10")),
416
+ facts: withFacts(font(0, { name: "CMR10" })),
416
417
  });
417
418
  expect(f?.message).toMatch(/LinLibertine/);
418
419
  expect(f?.message).toMatch(/CMR10/);
@@ -420,28 +421,28 @@ describe("pdf/fonts", () => {
420
421
  });
421
422
 
422
423
  describe("pdf/geometry", () => {
423
- it.each([
424
+ it.each<[string, (f: Facts) => void, string[]]>([
424
425
  [
425
426
  "A4 instead of letter",
426
- (f: Json) => ((f.page_w_in = 8.27), (f.page_h_in = 11.69)),
427
+ (f: Facts) => ((f.page_w_in = 8.27), (f.page_h_in = 11.69)),
427
428
  ["pdf/geometry:dim", "pdf/geometry:dim"],
428
429
  ],
429
430
  [
430
431
  "one column instead of two",
431
- (f: Json) => (f.columns = 1),
432
+ (f: Facts) => (f.columns = 1),
432
433
  ["pdf/geometry:columns"],
433
434
  ],
434
435
  [
435
436
  "a page size banal could not read",
436
- (f: Json) => (f.page_w_in = null),
437
+ (f: Facts) => (f.page_w_in = null),
437
438
  ["pdf/geometry:dimMissing"],
438
439
  ],
439
440
  [
440
441
  "0.03 in off is inside the default tolerance",
441
- (f: Json) => (f.page_w_in = 8.53),
442
+ (f: Facts) => (f.page_w_in = 8.53),
442
443
  [],
443
444
  ],
444
- ] as [string, (f: Json) => void, string[]][])("%s", (_, patch, want) => {
445
+ ])("%s", (_, patch, want) => {
445
446
  expect(ids(lint({ venue: DECL, facts: withFacts(patch) }))).toEqual(want);
446
447
  });
447
448
 
@@ -455,46 +456,43 @@ describe("pdf/geometry", () => {
455
456
  });
456
457
 
457
458
  describe("pdf/limits", () => {
458
- it.each([
459
+ it.each<[string, (f: Facts) => void, string[], RegExp | null]>([
459
460
  [
460
461
  "one body page over the short-paper limit",
461
- (f: Json) => (f.body_pages = 6),
462
+ (f: Facts) => (f.body_pages = 6),
462
463
  ["pdf/limits:pages"],
463
464
  /body pages: 6, over the limit 5 for agenticdev\/short/,
464
465
  ],
465
466
  [
466
467
  "one reference page over",
467
- (f: Json) => (f.ref_pages = 3),
468
+ (f: Facts) => (f.ref_pages = 3),
468
469
  ["pdf/limits:pages"],
469
470
  /reference pages: 3, over the limit 2/,
470
471
  ],
471
- ["exactly at the limit", (f: Json) => (f.body_pages = 5), [], null],
472
+ ["exactly at the limit", (f: Facts) => (f.body_pages = 5), [], null],
472
473
  [
473
474
  "a reference font below the range",
474
- (f: Json) => (f.ref_pt = 6),
475
+ (f: Facts) => (f.ref_pt = 6),
475
476
  ["pdf/limits:refPt"],
476
477
  /6 pt/,
477
478
  ],
478
479
  [
479
480
  "6.8 pt is inside the range once the measuring drift (body_pt_tol) is allowed",
480
- (f: Json) => (f.ref_pt = 6.8),
481
+ (f: Facts) => (f.ref_pt = 6.8),
481
482
  [],
482
483
  null,
483
484
  ],
484
485
  [
485
486
  "no reference font measured (no bibliography) is not a finding",
486
- (f: Json) => (f.ref_pt = null),
487
+ (f: Facts) => (f.ref_pt = null),
487
488
  [],
488
489
  null,
489
490
  ],
490
- ] as [string, (f: Json) => void, string[], RegExp | null][])(
491
- "%s",
492
- (_, patch, want, text) => {
493
- const fs = lint({ venue: DECL, facts: withFacts(patch) });
494
- expect(ids(fs)).toEqual(want);
495
- if (text) expect(fs[0]?.message).toMatch(text);
496
- },
497
- );
491
+ ])("%s", (_, patch, want, text) => {
492
+ const fs = lint({ venue: DECL, facts: withFacts(patch) });
493
+ expect(ids(fs)).toEqual(want);
494
+ if (text) expect(fs[0]?.message).toMatch(text);
495
+ });
498
496
 
499
497
  it("a longer kind of the same venue passes the same page count", () => {
500
498
  const facts = withFacts((f) => (f.body_pages = 9));
@@ -506,19 +504,19 @@ describe("pdf/limits", () => {
506
504
  });
507
505
 
508
506
  describe("pdf/body-size", () => {
509
- it.each([
507
+ it.each<[string, (f: Facts) => void, string[]]>([
510
508
  [
511
509
  "10.2 pt against 9 ± 0.5",
512
- (f: Json) => (f.body_pt = 10.2),
510
+ (f: Facts) => (f.body_pt = 10.2),
513
511
  ["pdf/body-size:body"],
514
512
  ],
515
- ["9.4 pt is inside the tolerance", (f: Json) => (f.body_pt = 9.4), []],
513
+ ["9.4 pt is inside the tolerance", (f: Facts) => (f.body_pt = 9.4), []],
516
514
  [
517
515
  "no body size measured",
518
- (f: Json) => (f.body_pt = null),
516
+ (f: Facts) => (f.body_pt = null),
519
517
  ["pdf/body-size:bodyMissing"],
520
518
  ],
521
- ] as [string, (f: Json) => void, string[]][])("%s", (_, patch, want) => {
519
+ ])("%s", (_, patch, want) => {
522
520
  expect(ids(lint({ venue: DECL, facts: withFacts(patch) }))).toEqual(want);
523
521
  });
524
522
  });
@@ -567,7 +565,7 @@ describe("a project's own preset, and the facts' other spellings", () => {
567
565
  format: { columns: 2, body_pt: 9, ref_pt_min: 7, ref_pt_max: 8 },
568
566
  }),
569
567
  };
570
- const house = (patch: (f: Json) => void = () => {}) =>
568
+ const house = (patch: (f: Facts) => void = () => {}) =>
571
569
  lint({
572
570
  venue: { extends: "./house.jsonc" },
573
571
  facts: withFacts(patch),
@@ -48,7 +48,7 @@ import type { FlatGeometry } from "./domain/geometry.ts";
48
48
  import type { AbsolutePath } from "./domain/paths.ts";
49
49
  import { sha256Hex } from "./domain/sha256.ts";
50
50
  import type { Files } from "./ports/files.ts";
51
- import { CONFIG_FILE } from "../lib/paper-config.mjs";
51
+ import { CONFIG_FILE } from "#lib/paper-config";
52
52
 
53
53
  // ── the verdict's vocabulary ─────────────────────────────────────────────────────────
54
54
 
package/tsconfig.json CHANGED
@@ -11,13 +11,16 @@
11
11
  "sourceMap": true,
12
12
  "strict": true,
13
13
  "noUncheckedIndexedAccess": true,
14
- "exactOptionalPropertyTypes": false,
14
+ "exactOptionalPropertyTypes": true,
15
15
  "skipLibCheck": true,
16
16
  "verbatimModuleSyntax": false,
17
17
  "types": ["node"],
18
18
  "allowImportingTsExtensions": true,
19
- "rewriteRelativeImportExtensions": true
19
+ "rewriteRelativeImportExtensions": true,
20
+ "noImplicitOverride": true,
21
+ "noImplicitReturns": true,
22
+ "noFallthroughCasesInSwitch": true
20
23
  },
21
24
  "include": ["src/**/*.ts"],
22
- "exclude": ["src/**/*.test.ts"]
25
+ "exclude": ["src/**/*.test.ts", "src/**/*.harness.ts"]
23
26
  }
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": ".",
5
+ "outDir": "dist",
6
+ "resolveJsonModule": true
7
+ },
8
+ "include": ["lib/*.ts", "eslint-rules/*.ts", "eslint-rules/*.json"],
9
+ "exclude": ["**/*.test.ts", "**/*.harness.ts", "**/*.d.ts"]
10
+ }
package/lib/check.d.mts DELETED
@@ -1,17 +0,0 @@
1
- // Types for lib/check.mjs — the shared harness assertion helper (see the module).
2
-
3
- /** A failure detail: text, any value (rendered with `util.inspect`), or a thunk producing one. */
4
- export type Detail = unknown;
5
-
6
- export declare function renderDetail(detail: Detail): string;
7
- export declare function failureMessage(label: string, detail: Detail): string;
8
-
9
- export interface Check {
10
- (label: string, cond: unknown, detail?: Detail): void;
11
- /** How many times this checker has been called. */
12
- readonly count: number;
13
- }
14
-
15
- export declare function createChecker(options?: {
16
- log?: (line: string) => void;
17
- }): Check;
@@ -1,31 +0,0 @@
1
- // Types for lib/paper-config.mjs, which `src/*.ts` imports. Without this file TypeScript sees
2
- // `any` and each import needs a `@ts-expect-error`, which covers only the next line and stops
3
- // working as soon as a formatter wraps the import across several lines.
4
- //
5
- // The field names are typed as `string`, not as their values, so that the value is written
6
- // once, in paper-config.mjs.
7
-
8
- /** The tool's name — its settings file is named after it. */
9
- export declare const CONFIG_KEY: string;
10
- /** `paperlint.json`: the settings file, at the project root and in each paper. */
11
- export declare const CONFIG_FILE: string;
12
- /** The files that make a directory a paper. */
13
- export declare const PAPER_MARKERS: readonly string[];
14
-
15
- /** The project root, walking up from `startDir` (see the module). */
16
- export declare function findProjectRoot(
17
- startDir: string,
18
- isFile?: (path: string) => boolean,
19
- ): string;
20
-
21
- /** The root `paperlint.json` of `root` — undefined when absent, unparsable or not an object. */
22
- export declare function settingsOf(
23
- root: string,
24
- ): Readonly<Record<string, unknown>> | undefined;
25
- export declare const DEFAULT_PAPERS_ROOT: string;
26
- export declare const PAPERS_DIR_FIELD: string;
27
-
28
- /** Every key a `paperlint.json` may hold, at either level, mapped to who reads it. */
29
- export declare const SETTINGS_KEYS: Readonly<Record<string, string>>;
30
- /** The keys refused in a paper's `paperlint.json`. */
31
- export declare const ROOT_ONLY_KEYS: readonly string[];
@@ -1,36 +0,0 @@
1
- /** Types for `layer-legacy-frozen.mjs`, for its TypeScript test. */
2
- export interface Suppressed {
3
- readonly filePath: string;
4
- readonly suppressedMessages?: readonly {
5
- readonly ruleId: string | null;
6
- readonly line: number;
7
- readonly suppressions?: readonly { readonly justification?: string }[];
8
- }[];
9
- }
10
- export type Counts = Record<string, Record<string, number>>;
11
- export declare const FROZEN_FILE: string;
12
- export declare const LAYER_RULES: readonly string[];
13
- export declare const NO_LEGACY_UNDER: readonly string[];
14
- export declare const LEGACY: RegExp;
15
- export declare function tally(results: readonly Suppressed[]): {
16
- counts: Counts;
17
- unexplained: string[];
18
- };
19
- export declare function judge(o: {
20
- counts: Counts;
21
- unexplained: readonly string[];
22
- frozen: Counts;
23
- }): string[];
24
- export declare function checkFrozen(
25
- root?: string,
26
- options?: { lint?: (cwd: string) => Promise<unknown[]> },
27
- ): Promise<{
28
- problems: string[];
29
- frozen: Counts;
30
- counts: Counts;
31
- }>;
32
- export declare function main(options?: {
33
- check?: () => Promise<{ problems: string[]; frozen: Counts; counts: Counts }>;
34
- log?: (line: string) => void;
35
- err?: (line: string) => void;
36
- }): Promise<number>;
@@ -1,105 +0,0 @@
1
- /**
2
- * Colocated test for `scripts/rules-are-content-only.mjs` — a rule may not ask git.
3
- * Run: `npx vigiles test scripts/rules-are-content-only.harness.mjs`
4
- *
5
- * 🔴 BOTH HALVES. Like its sibling, this guard's success state is silence, so silence alone is
6
- * no evidence it works:
7
- * I. QUIET on this repository's real `eslint-rules/`, and the count is asserted non-zero —
8
- * a guard that scanned nothing is also quiet, and that is the reading to rule out first.
9
- * II. FIRES on each form the defect takes: static `import`, `require()`, dynamic `import()`,
10
- * and the `node:` prefix — the prefix matters because a set membership test that lists
11
- * only the bare specifier passes a file that writes the modern spelling.
12
- * III. QUIET on sources that merely TALK about the banned module — a comment and a string
13
- * literal. This is the half a grep cannot have, and the reason the check parses.
14
- */
15
- import { dirname, join } from "node:path";
16
- import { fileURLToPath } from "node:url";
17
- import { createChecker } from "../lib/check.mjs";
18
-
19
- const HERE = dirname(fileURLToPath(import.meta.url));
20
- const { rulesAreContentOnly, processImports } = await import(
21
- join(HERE, "rules-are-content-only.mjs")
22
- );
23
-
24
- const check = createChecker();
25
-
26
- // ── I. quiet on the real corpus, and it actually looked ───────────────────────
27
- {
28
- const { checked, findings } = rulesAreContentOnly({ cwd: join(HERE, "..") });
29
- check(
30
- `scanned something (${checked.length} rule sources)`,
31
- checked.length > 0,
32
- );
33
- check(
34
- `quiet on the real corpus (${findings.length} findings)`,
35
- findings.length === 0,
36
- );
37
- }
38
-
39
- // ── II. fires on every form the defect has taken ────────────────────────────
40
- for (const [label, src] of [
41
- [
42
- "execFileSync with a git argv",
43
- `import { execFileSync } from "node:child_process";
44
- execFileSync("git", ["cat-file", "-e", sha]);`,
45
- ],
46
- [
47
- "a namespaced call",
48
- `import cp from "node:child_process";
49
- cp.spawnSync("git", ["rev-parse", ref]);`,
50
- ],
51
- [
52
- "execSync with a git shell line",
53
- `import { execSync } from "child_process";
54
- execSync("git log --oneline -1");`,
55
- ],
56
- ["exec, async form", `exec("git rev-list --all", cb);`],
57
- ]) {
58
- check(`FIRES on ${label}`, processImports(src).length === 1);
59
- }
60
-
61
- // ── III. the FALSE POSITIVE that forced the narrowing, asserted so it cannot return ──
62
- // 🔴 `paper-texcount.mjs` in the consumer shells out to `texcount` to count words in a .tex.
63
- // The first version of this check keyed on importing `child_process` at all, so it claimed that
64
- // rule on its first run — a false positive on an `error` gate, which is worse than a miss
65
- // because the gate that cannot be cleared gets switched off rather than fixed.
66
- for (const [label, src] of [
67
- [
68
- "a rule spawning texcount",
69
- `import { execFileSync } from "node:child_process";
70
- execFileSync("texcount", ["-brief", file]);`,
71
- ],
72
- [
73
- "a rule spawning pdflatex",
74
- `import { spawnSync } from "node:child_process";
75
- spawnSync("pdflatex", [tex]);`,
76
- ],
77
- [
78
- "importing child_process without running git",
79
- `import { execFileSync } from "node:child_process";
80
- export default {};`,
81
- ],
82
- ]) {
83
- check(`quiet on ${label}`, processImports(src).length === 0);
84
- }
85
-
86
- // ── IV. quiet on sources that only MENTION it — what a grep cannot do ───────
87
- for (const [label, src] of [
88
- [
89
- "a comment naming git",
90
- `// never reach for git cat-file here\nexport default {};`,
91
- ],
92
- ["a string literal", `export const why = "git cat-file is banned in rules";`],
93
- [
94
- "a reported message",
95
- `export default { meta: { messages: { m: "run git cat-file? no" } } };`,
96
- ],
97
- [
98
- "a program merely NAMED git-something",
99
- `execFileSync("git-lfs-helper", []);`,
100
- ],
101
- ]) {
102
- check(`quiet on ${label}`, processImports(src).length === 0);
103
- }
104
-
105
- console.log(`rules-are-content-only.harness: ${check.count} assertions passed`);
@@ -1,148 +0,0 @@
1
- /**
2
- * A RULE MAY NOT ASK GIT.
3
- *
4
- * 🔴 The defect it exists for, measured 2026-09-16. `paper/source` bound a paper's stage to a
5
- * commit sha and verified it with `git cat-file -e`. Of four shas recorded in the consumer's
6
- * front matter, ONE still resolved ninety minutes later: a squash-merge had orphaned the branch
7
- * commits and `gc` collected them. A recorded sha is a pointer to a pointer, and both hops are
8
- * removed by ROUTINE maintenance — squash, rebase, gc, force-push, filter-repo. Nobody is at
9
- * fault, so nobody is on the hook to fix it; the rule simply turns red and gets switched off.
10
- *
11
- * ── THE STRONGER LEG, AND THE REASON THIS IS A FLOOR CHECK ──────────────────
12
- * `actions/checkout` defaults to `fetch-depth: 1` — exactly one commit. Measured in the
13
- * consumer the same day: six checkouts, not one override. So a git-reading rule does not
14
- * resolve even a LIVE sha under CI, which is the one place it has to hold. Locally it stays
15
- * green, so the failure is one-directional and in the flattering direction.
16
- *
17
- * ── WHAT IT CHECKS, AND WHY NOT A GREP ──────────────────────────────────────
18
- * A `child_process` spawner called with `git` as the program, read from the AST. Parsed, not
19
- * grepped, so the words `git cat-file` in this very comment are not a finding — a text guard
20
- * would have to exempt itself, which is the failure mode that makes text guards unmaintainable.
21
- *
22
- * ⚠️ THE BOUND, stated so the check is not mistaken for more than it is. Two things pass: a
23
- * rule that opens `.git/` with `fs` (indistinguishable at this level from reading a data file),
24
- * and a rule that assembles the program name at runtime. Both accepted deliberately — see the
25
- * note on `SPAWNERS` for the false positive that forced the predicate this narrow.
26
- *
27
- * Run: `node scripts/rules-are-content-only.mjs` (exit 1 and a named rule on a finding)
28
- * Tested by: `scripts/rules-are-content-only.harness.mjs` — quiet on the real corpus, firing on
29
- * a planted rule.
30
- */
31
- import { existsSync, readdirSync, readFileSync } from "node:fs";
32
- import { join } from "node:path";
33
- import { parse } from "espree";
34
- import { isMain } from "../skills/paper-pipeline/scripts/consumer.mjs";
35
-
36
- /**
37
- * 🔴 THE PREDICATE IS "RUNS GIT", NOT "SPAWNS A PROCESS", and the narrowing was forced by a
38
- * FALSE POSITIVE on the consumer's corpus the hour this check shipped: `paper-texcount.mjs`
39
- * shells out to `texcount` to count words in a .tex, which is a measuring tool, not history.
40
- * A false positive on an `error` gate is worse than a miss — the gate that cannot be cleared
41
- * gets switched off, and this one would have been switched off on its first run.
42
- *
43
- * ⚠️ THE COST OF NARROWING, stated rather than hidden: a rule that builds the program name at
44
- * runtime (`const g = "gi" + "t"`) passes. That is accepted. The defect has taken exactly one
45
- * form — a literal `git` handed to a `child_process` function — and a check that also caught
46
- * the contrived form would have to claim every spawn, which is the false positive above.
47
- */
48
- const SPAWNERS = new Set([
49
- "exec",
50
- "execSync",
51
- "execFile",
52
- "execFileSync",
53
- "spawn",
54
- "spawnSync",
55
- "fork",
56
- ]);
57
-
58
- /** `"git"` as the program, or `"git …"` as the head of a shell line handed to `exec`. */
59
- const isGit = (v) =>
60
- typeof v === "string" && (v === "git" || v.startsWith("git "));
61
-
62
- /** Rule sources only: a harness plants defects on purpose and asserts about them. */
63
- const isRuleSource = (f) => f.endsWith(".mjs") && !f.endsWith(".harness.mjs");
64
-
65
- /** @param {string} src @returns {string[]} the git invocations this source makes */
66
- export function processImports(src) {
67
- const ast = parse(src, {
68
- ecmaVersion: "latest",
69
- sourceType: "module",
70
- range: false,
71
- });
72
- const found = [];
73
- const walk = (n) => {
74
- if (n === null || typeof n !== "object") return;
75
- if (Array.isArray(n)) return n.forEach(walk);
76
- // Both spellings of the callee: `execFileSync(…)` after a named import, and
77
- // `cp.execFileSync(…)` after a namespace import. Listed, not described — the node types a
78
- // check walks are the only honest statement of its coverage.
79
- if (n.type === "CallExpression") {
80
- const name =
81
- n.callee?.type === "MemberExpression"
82
- ? n.callee.property?.name
83
- : n.callee?.name;
84
- if (SPAWNERS.has(String(name)) && isGit(n.arguments?.[0]?.value))
85
- found.push(String(n.arguments[0].value));
86
- }
87
- for (const k of Object.keys(n)) if (k !== "parent") walk(n[k]);
88
- };
89
- walk(ast);
90
- return found;
91
- }
92
-
93
- /** @param {{cwd: string}} options */
94
- export function rulesAreContentOnly({ cwd }) {
95
- const dir = join(cwd, "eslint-rules");
96
- const checked = [];
97
- const findings = [];
98
- // A repository with no `eslint-rules/` has no rule sources: `checked` stays empty and the gate
99
- // says so by name. `readdirSync` would throw ENOENT instead — a stack trace, not the refusal.
100
- const names = existsSync(dir) ? readdirSync(dir) : [];
101
- for (const f of names.filter(isRuleSource)) {
102
- const hits = processImports(readFileSync(join(dir, f), "utf8"));
103
- // 🔴 The counter increments WITH the verdict, not before it: a `checked` that keeps
104
- // counting while the verdict stops being reached is the failure recorded in the consumer's
105
- // rules as "a counter that counts what it ignores".
106
- checked.push(f);
107
- for (const h of hits) findings.push({ file: f, specifier: h });
108
- }
109
- return { checked, findings };
110
- }
111
-
112
- /**
113
- * The gate over `cwd`'s `eslint-rules/`: exit 1 on no rule sources (a silent zero would read as
114
- * clean) or on any rule asking git; 0 otherwise.
115
- *
116
- * 🔴 `cwd` is `process.cwd()`, NOT the package root. A consumer installs this package and runs the
117
- * script from its own repository, where the rules that matter are ITS `eslint-rules/` — the
118
- * package's own are already checked by the package's own gate. Anchored to the package root the
119
- * consumer's gate would re-check the same eight files and report a confident zero about rules it
120
- * never opened: a counter that counts what it ignores.
121
- */
122
- export function main({
123
- cwd = process.cwd(),
124
- log = console.log,
125
- err = console.error,
126
- } = {}) {
127
- const { checked, findings } = rulesAreContentOnly({ cwd });
128
- if (checked.length === 0) {
129
- err(
130
- `${join(cwd, "eslint-rules")}: no rule sources. Either this is not a repository with ` +
131
- `ESLint rules, or the directory moved — a silent zero here would read as a clean run.`,
132
- );
133
- return 1;
134
- }
135
- for (const { file, specifier } of findings) {
136
- err(
137
- `${file}: imports \`${specifier}\`. A rule may not ask git: history is rewritten by ` +
138
- `routine maintenance and CI clones one commit deep, so the fact does not survive where ` +
139
- `the rule has to hold. Put the fact on disk and check it by bytes.`,
140
- );
141
- }
142
- log(
143
- `rules-are-content-only: ${checked.length} rule sources under ${cwd}, ${findings.length} findings`,
144
- );
145
- return findings.length === 0 ? 0 : 1;
146
- }
147
-
148
- if (isMain(import.meta.url)) process.exit(main());