paperlint 2.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 (762) hide show
  1. package/.github/dependabot.yml +72 -0
  2. package/.github/workflows/ci.yml +297 -0
  3. package/.github/workflows/dependabot-automerge.yml +70 -0
  4. package/.github/workflows/pr-title.yml +59 -0
  5. package/.github/workflows/release.yml +54 -0
  6. package/CLAUDE.md +598 -0
  7. package/CONTRIBUTING.md +159 -0
  8. package/LICENSE +21 -0
  9. package/README.md +240 -0
  10. package/action.harness.mjs +287 -0
  11. package/action.mutations.mjs +162 -0
  12. package/action.yml +138 -0
  13. package/bin/rpp.mjs +43 -0
  14. package/dist/action-ref.d.ts +12 -0
  15. package/dist/action-ref.d.ts.map +1 -0
  16. package/dist/action-ref.js +16 -0
  17. package/dist/action-ref.js.map +1 -0
  18. package/dist/adapters/banal/failure.d.ts +73 -0
  19. package/dist/adapters/banal/failure.d.ts.map +1 -0
  20. package/dist/adapters/banal/failure.js +58 -0
  21. package/dist/adapters/banal/failure.js.map +1 -0
  22. package/dist/adapters/banal/index.d.ts +17 -0
  23. package/dist/adapters/banal/index.d.ts.map +1 -0
  24. package/dist/adapters/banal/index.js +56 -0
  25. package/dist/adapters/banal/index.js.map +1 -0
  26. package/dist/adapters/banal/install.d.ts +26 -0
  27. package/dist/adapters/banal/install.d.ts.map +1 -0
  28. package/dist/adapters/banal/install.js +15 -0
  29. package/dist/adapters/banal/install.js.map +1 -0
  30. package/dist/adapters/banal/invocation.d.ts +48 -0
  31. package/dist/adapters/banal/invocation.d.ts.map +1 -0
  32. package/dist/adapters/banal/invocation.js +43 -0
  33. package/dist/adapters/banal/invocation.js.map +1 -0
  34. package/dist/adapters/banal/locate.d.ts +50 -0
  35. package/dist/adapters/banal/locate.d.ts.map +1 -0
  36. package/dist/adapters/banal/locate.js +34 -0
  37. package/dist/adapters/banal/locate.js.map +1 -0
  38. package/dist/adapters/banal/output.d.ts +27 -0
  39. package/dist/adapters/banal/output.d.ts.map +1 -0
  40. package/dist/adapters/banal/output.js +112 -0
  41. package/dist/adapters/banal/output.js.map +1 -0
  42. package/dist/adapters/banal/pin.d.ts +19 -0
  43. package/dist/adapters/banal/pin.d.ts.map +1 -0
  44. package/dist/adapters/banal/pin.js +15 -0
  45. package/dist/adapters/banal/pin.js.map +1 -0
  46. package/dist/adapters/banal/probe.d.ts +12 -0
  47. package/dist/adapters/banal/probe.d.ts.map +1 -0
  48. package/dist/adapters/banal/probe.js +27 -0
  49. package/dist/adapters/banal/probe.js.map +1 -0
  50. package/dist/adapters/banal/run.d.ts +89 -0
  51. package/dist/adapters/banal/run.d.ts.map +1 -0
  52. package/dist/adapters/banal/run.js +104 -0
  53. package/dist/adapters/banal/run.js.map +1 -0
  54. package/dist/adapters/banal/settings.d.ts +18 -0
  55. package/dist/adapters/banal/settings.d.ts.map +1 -0
  56. package/dist/adapters/banal/settings.js +29 -0
  57. package/dist/adapters/banal/settings.js.map +1 -0
  58. package/dist/adapters/banal/xml.d.ts +48 -0
  59. package/dist/adapters/banal/xml.d.ts.map +1 -0
  60. package/dist/adapters/banal/xml.js +67 -0
  61. package/dist/adapters/banal/xml.js.map +1 -0
  62. package/dist/adapters/curl/download.io.d.ts +14 -0
  63. package/dist/adapters/curl/download.io.d.ts.map +1 -0
  64. package/dist/adapters/curl/download.io.js +69 -0
  65. package/dist/adapters/curl/download.io.js.map +1 -0
  66. package/dist/adapters/curl/index.d.ts +6 -0
  67. package/dist/adapters/curl/index.d.ts.map +1 -0
  68. package/dist/adapters/curl/index.js +6 -0
  69. package/dist/adapters/curl/index.js.map +1 -0
  70. package/dist/adapters/memory/index.d.ts +43 -0
  71. package/dist/adapters/memory/index.d.ts.map +1 -0
  72. package/dist/adapters/memory/index.js +79 -0
  73. package/dist/adapters/memory/index.js.map +1 -0
  74. package/dist/adapters/node/files.io.d.ts +3 -0
  75. package/dist/adapters/node/files.io.d.ts.map +1 -0
  76. package/dist/adapters/node/files.io.js +31 -0
  77. package/dist/adapters/node/files.io.js.map +1 -0
  78. package/dist/adapters/node/host.io.d.ts +3 -0
  79. package/dist/adapters/node/host.io.d.ts.map +1 -0
  80. package/dist/adapters/node/host.io.js +14 -0
  81. package/dist/adapters/node/host.io.js.map +1 -0
  82. package/dist/adapters/node/index.d.ts +25 -0
  83. package/dist/adapters/node/index.d.ts.map +1 -0
  84. package/dist/adapters/node/index.js +14 -0
  85. package/dist/adapters/node/index.js.map +1 -0
  86. package/dist/adapters/node/process.io.d.ts +14 -0
  87. package/dist/adapters/node/process.io.d.ts.map +1 -0
  88. package/dist/adapters/node/process.io.js +41 -0
  89. package/dist/adapters/node/process.io.js.map +1 -0
  90. package/dist/adapters/node/workspace.io.d.ts +4 -0
  91. package/dist/adapters/node/workspace.io.d.ts.map +1 -0
  92. package/dist/adapters/node/workspace.io.js +33 -0
  93. package/dist/adapters/node/workspace.io.js.map +1 -0
  94. package/dist/adapters/pdfjs/fill.d.ts +42 -0
  95. package/dist/adapters/pdfjs/fill.d.ts.map +1 -0
  96. package/dist/adapters/pdfjs/fill.js +91 -0
  97. package/dist/adapters/pdfjs/fill.js.map +1 -0
  98. package/dist/build-engine.d.ts +48 -0
  99. package/dist/build-engine.d.ts.map +1 -0
  100. package/dist/build-engine.js +148 -0
  101. package/dist/build-engine.js.map +1 -0
  102. package/dist/build.d.ts +163 -0
  103. package/dist/build.d.ts.map +1 -0
  104. package/dist/build.js +575 -0
  105. package/dist/build.js.map +1 -0
  106. package/dist/cli.d.ts +151 -0
  107. package/dist/cli.d.ts.map +1 -0
  108. package/dist/cli.js +951 -0
  109. package/dist/cli.js.map +1 -0
  110. package/dist/doctor.d.ts +42 -0
  111. package/dist/doctor.d.ts.map +1 -0
  112. package/dist/doctor.js +280 -0
  113. package/dist/doctor.js.map +1 -0
  114. package/dist/domain/geometry.d.ts +71 -0
  115. package/dist/domain/geometry.d.ts.map +1 -0
  116. package/dist/domain/geometry.js +35 -0
  117. package/dist/domain/geometry.js.map +1 -0
  118. package/dist/domain/host.d.ts +16 -0
  119. package/dist/domain/host.d.ts.map +1 -0
  120. package/dist/domain/host.js +8 -0
  121. package/dist/domain/host.js.map +1 -0
  122. package/dist/domain/page-layout.d.ts +34 -0
  123. package/dist/domain/page-layout.d.ts.map +1 -0
  124. package/dist/domain/page-layout.js +8 -0
  125. package/dist/domain/page-layout.js.map +1 -0
  126. package/dist/domain/paths.d.ts +5 -0
  127. package/dist/domain/paths.d.ts.map +1 -0
  128. package/dist/domain/paths.js +2 -0
  129. package/dist/domain/paths.js.map +1 -0
  130. package/dist/domain/result.d.ts +23 -0
  131. package/dist/domain/result.d.ts.map +1 -0
  132. package/dist/domain/result.js +10 -0
  133. package/dist/domain/result.js.map +1 -0
  134. package/dist/domain/sha256.d.ts +7 -0
  135. package/dist/domain/sha256.d.ts.map +1 -0
  136. package/dist/domain/sha256.js +14 -0
  137. package/dist/domain/sha256.js.map +1 -0
  138. package/dist/domain/text.d.ts +6 -0
  139. package/dist/domain/text.d.ts.map +1 -0
  140. package/dist/domain/text.js +7 -0
  141. package/dist/domain/text.js.map +1 -0
  142. package/dist/engine.d.ts +93 -0
  143. package/dist/engine.d.ts.map +1 -0
  144. package/dist/engine.js +119 -0
  145. package/dist/engine.js.map +1 -0
  146. package/dist/exit-code.d.ts +22 -0
  147. package/dist/exit-code.d.ts.map +1 -0
  148. package/dist/exit-code.js +10 -0
  149. package/dist/exit-code.js.map +1 -0
  150. package/dist/facts-file.d.ts +96 -0
  151. package/dist/facts-file.d.ts.map +1 -0
  152. package/dist/facts-file.js +134 -0
  153. package/dist/facts-file.js.map +1 -0
  154. package/dist/hooks-settings.d.ts +141 -0
  155. package/dist/hooks-settings.d.ts.map +1 -0
  156. package/dist/hooks-settings.js +306 -0
  157. package/dist/hooks-settings.js.map +1 -0
  158. package/dist/init.d.ts +201 -0
  159. package/dist/init.d.ts.map +1 -0
  160. package/dist/init.js +579 -0
  161. package/dist/init.js.map +1 -0
  162. package/dist/latex-log.d.ts +80 -0
  163. package/dist/latex-log.d.ts.map +1 -0
  164. package/dist/latex-log.js +187 -0
  165. package/dist/latex-log.js.map +1 -0
  166. package/dist/latex-loop.d.ts +129 -0
  167. package/dist/latex-loop.d.ts.map +1 -0
  168. package/dist/latex-loop.js +113 -0
  169. package/dist/latex-loop.js.map +1 -0
  170. package/dist/link-skills.d.ts +51 -0
  171. package/dist/link-skills.d.ts.map +1 -0
  172. package/dist/link-skills.js +199 -0
  173. package/dist/link-skills.js.map +1 -0
  174. package/dist/new-paper.d.ts +48 -0
  175. package/dist/new-paper.d.ts.map +1 -0
  176. package/dist/new-paper.js +110 -0
  177. package/dist/new-paper.js.map +1 -0
  178. package/dist/pdf-facts.d.ts +44 -0
  179. package/dist/pdf-facts.d.ts.map +1 -0
  180. package/dist/pdf-facts.js +239 -0
  181. package/dist/pdf-facts.js.map +1 -0
  182. package/dist/pdf-geometry.d.ts +170 -0
  183. package/dist/pdf-geometry.d.ts.map +1 -0
  184. package/dist/pdf-geometry.js +158 -0
  185. package/dist/pdf-geometry.js.map +1 -0
  186. package/dist/ports/download.d.ts +9 -0
  187. package/dist/ports/download.d.ts.map +1 -0
  188. package/dist/ports/download.js +2 -0
  189. package/dist/ports/download.js.map +1 -0
  190. package/dist/ports/files.d.ts +11 -0
  191. package/dist/ports/files.d.ts.map +1 -0
  192. package/dist/ports/files.js +2 -0
  193. package/dist/ports/files.js.map +1 -0
  194. package/dist/ports/measure-geometry.d.ts +8 -0
  195. package/dist/ports/measure-geometry.d.ts.map +1 -0
  196. package/dist/ports/measure-geometry.js +2 -0
  197. package/dist/ports/measure-geometry.js.map +1 -0
  198. package/dist/ports/process.d.ts +45 -0
  199. package/dist/ports/process.d.ts.map +1 -0
  200. package/dist/ports/process.js +2 -0
  201. package/dist/ports/process.js.map +1 -0
  202. package/dist/ports/tool-installer.d.ts +29 -0
  203. package/dist/ports/tool-installer.d.ts.map +1 -0
  204. package/dist/ports/tool-installer.js +2 -0
  205. package/dist/ports/tool-installer.js.map +1 -0
  206. package/dist/ports/workspace.d.ts +18 -0
  207. package/dist/ports/workspace.d.ts.map +1 -0
  208. package/dist/ports/workspace.js +2 -0
  209. package/dist/ports/workspace.js.map +1 -0
  210. package/dist/rules-config.d.ts +34 -0
  211. package/dist/rules-config.d.ts.map +1 -0
  212. package/dist/rules-config.js +132 -0
  213. package/dist/rules-config.js.map +1 -0
  214. package/dist/structure.d.ts +34 -0
  215. package/dist/structure.d.ts.map +1 -0
  216. package/dist/structure.js +149 -0
  217. package/dist/structure.js.map +1 -0
  218. package/dist/tex-requirements.d.ts +43 -0
  219. package/dist/tex-requirements.d.ts.map +1 -0
  220. package/dist/tex-requirements.js +127 -0
  221. package/dist/tex-requirements.js.map +1 -0
  222. package/dist/toolchain.d.ts +159 -0
  223. package/dist/toolchain.d.ts.map +1 -0
  224. package/dist/toolchain.js +542 -0
  225. package/dist/toolchain.js.map +1 -0
  226. package/dist/types.d.ts +110 -0
  227. package/dist/types.d.ts.map +1 -0
  228. package/dist/types.js +2 -0
  229. package/dist/types.js.map +1 -0
  230. package/docs/configuration.md +235 -0
  231. package/docs/e2e.md +152 -0
  232. package/docs/incidents.md +59 -0
  233. package/docs/install.md +170 -0
  234. package/docs/optional-rules.md +107 -0
  235. package/docs/package-shape-options.md +262 -0
  236. package/docs/prior-art/README.md +76 -0
  237. package/docs/prior-art/blocking-vs-advisory.md +83 -0
  238. package/docs/prior-art/content-delivery.md +124 -0
  239. package/docs/prior-art/multi-mode-tools.md +106 -0
  240. package/docs/prior-art/nondeterministic-checks.md +99 -0
  241. package/docs/prior-art/package-location.md +422 -0
  242. package/docs/prior-art/paper-folder-scaffolding.md +538 -0
  243. package/docs/prior-art/readme-structure.md +69 -0
  244. package/docs/prior-art/repro/README.md +92 -0
  245. package/docs/prior-art/repro/claim1-allowedtools.mjs +66 -0
  246. package/docs/prior-art/repro/claim1-at2.mjs +40 -0
  247. package/docs/prior-art/repro/claim1-crosschannel.mjs +54 -0
  248. package/docs/prior-art/repro/claim1-frontmatter.mjs +76 -0
  249. package/docs/prior-art/repro/claim1-hook-payload-reporter.mjs +10 -0
  250. package/docs/prior-art/repro/claim1-plugin-frontmatter.mjs +27 -0
  251. package/docs/prior-art/repro/claim1-plugin-skill.mjs +52 -0
  252. package/docs/prior-art/repro/claim1-project-skill.mjs +81 -0
  253. package/docs/prior-art/repro/claim2-marketplace-flat-asclaimed.json +1 -0
  254. package/docs/prior-art/repro/claim2-marketplace-negative-control.json +1 -0
  255. package/docs/prior-art/repro/claim2-marketplace-nested-exact.json +9 -0
  256. package/docs/prior-art/repro/claim2-marketplace-nested-noversion.json +9 -0
  257. package/docs/prior-art/repro/claim2-marketplace-nested-range.json +1 -0
  258. package/docs/prior-art/repro/claim3-imports.mjs +50 -0
  259. package/docs/prior-art/repro/claim4-find-package-json.mjs +8 -0
  260. package/docs/prior-art/repro/claim4-package-dir.mjs +39 -0
  261. package/docs/prior-art/repro/claim4-parent-arg.mjs +17 -0
  262. package/docs/prior-art/repro/claim4-resolve-apis.mjs +21 -0
  263. package/docs/prior-art/repro/claim4-setup-consumers.mjs +45 -0
  264. package/docs/prior-art/repro/claim4-yarn-pnp.mjs +70 -0
  265. package/docs/prior-art/repro/claim5-bin-launch.mjs +39 -0
  266. package/docs/prior-art/repro/claim5-exports-mutation.mjs +57 -0
  267. package/docs/prior-art/repro/claim5-resolved-location-and-bin.mjs +33 -0
  268. package/docs/prior-art/repro/claim6-candidate-ambiguity.mjs +17 -0
  269. package/docs/prior-art/repro/claim6-doc-path-candidates.mjs +27 -0
  270. package/docs/prior-art/test-tooling.md +131 -0
  271. package/docs/rules.md +58 -0
  272. package/docs/texlive-install-decision.md +230 -0
  273. package/docs/toolchain.md +152 -0
  274. package/eslint-rules/doc-fields.harness.mjs +336 -0
  275. package/eslint-rules/doc-fields.mjs +186 -0
  276. package/eslint-rules/doc-fields.mutations.mjs +96 -0
  277. package/eslint-rules/install-path-literals.harness.mjs +121 -0
  278. package/eslint-rules/install-path-literals.mjs +108 -0
  279. package/eslint-rules/install-path-literals.mutations.mjs +62 -0
  280. package/eslint-rules/latex-language.harness.mjs +599 -0
  281. package/eslint-rules/latex-language.mjs +591 -0
  282. package/eslint-rules/latex-language.mutations.mjs +196 -0
  283. package/eslint-rules/paper-research-question.harness.mjs +146 -0
  284. package/eslint-rules/paper-research-question.mjs +180 -0
  285. package/eslint-rules/paper-research-question.mutations.mjs +127 -0
  286. package/eslint-rules/paper-stages.harness.mjs +356 -0
  287. package/eslint-rules/paper-stages.mjs +455 -0
  288. package/eslint-rules/paper-stages.mutations.mjs +157 -0
  289. package/eslint-rules/paper-typography.harness.mjs +291 -0
  290. package/eslint-rules/paper-typography.mjs +313 -0
  291. package/eslint-rules/paper-typography.mutations.mjs +131 -0
  292. package/eslint-rules/papers.harness.mjs +259 -0
  293. package/eslint-rules/papers.mjs +166 -0
  294. package/eslint-rules/papers.mutations.mjs +186 -0
  295. package/eslint-rules/pdf-last-page-balance.harness.mjs +206 -0
  296. package/eslint-rules/pdf-last-page-balance.mjs +208 -0
  297. package/eslint-rules/review-findings-cause.harness.mjs +228 -0
  298. package/eslint-rules/review-findings-cause.mjs +135 -0
  299. package/eslint-rules/review-findings-cause.mutations.mjs +72 -0
  300. package/eslint-rules/temp-root-realpath.harness.mjs +176 -0
  301. package/eslint-rules/temp-root-realpath.mjs +129 -0
  302. package/eslint-rules/temp-root-realpath.mutations.mjs +99 -0
  303. package/eslint-rules/tex-build.harness.mjs +753 -0
  304. package/eslint-rules/tex-build.mjs +322 -0
  305. package/eslint-rules/tex-build.mutations.mjs +258 -0
  306. package/eslint.config.mjs +521 -0
  307. package/fixtures/build-e2e/acmart/PIPELINE-STATUS.md +3 -0
  308. package/fixtures/build-e2e/acmart/paper.tex +11 -0
  309. package/fixtures/build-e2e/acmart/venue.json +1 -0
  310. package/fixtures/build-e2e/broken/PIPELINE-STATUS.md +3 -0
  311. package/fixtures/build-e2e/broken/paper.tex +7 -0
  312. package/fixtures/build-e2e/cite/PIPELINE-STATUS.md +3 -0
  313. package/fixtures/build-e2e/cite/build.sh +5 -0
  314. package/fixtures/build-e2e/cite/paper.tex +10 -0
  315. package/fixtures/build-e2e/cite/refs.bib +9 -0
  316. package/fixtures/build-e2e/empty/PIPELINE-STATUS.md +3 -0
  317. package/fixtures/build-e2e/empty/paper.tex +6 -0
  318. package/fixtures/build-e2e/fallback/PIPELINE-STATUS.md +3 -0
  319. package/fixtures/build-e2e/fallback/paper.tex +11 -0
  320. package/fixtures/build-e2e/guards/PIPELINE-STATUS.md +3 -0
  321. package/fixtures/build-e2e/guards/paper.tex +10 -0
  322. package/fixtures/build-e2e/no-source/PIPELINE-STATUS.md +3 -0
  323. package/fixtures/build-e2e/unbalanced/PIPELINE-STATUS.md +3 -0
  324. package/fixtures/build-e2e/unbalanced/paper.tex +28 -0
  325. package/fixtures/build-e2e/unbalanced/refs.bib +269 -0
  326. package/fixtures/install-path-literals/clean.fixture.mjs +3 -0
  327. package/fixtures/install-path-literals/clean.md +15 -0
  328. package/fixtures/install-path-literals/defect.fixture.mjs +3 -0
  329. package/fixtures/install-path-literals/defect.md +14 -0
  330. package/fixtures/latex-language/clean.tex +50 -0
  331. package/fixtures/latex-language/defect.tex +52 -0
  332. package/fixtures/paper-research-question/comment-only/PIPELINE-STATUS.md +9 -0
  333. package/fixtures/paper-research-question/comment-only/paper.tex +7 -0
  334. package/fixtures/paper-research-question/declared-not-in-paper/PIPELINE-STATUS.md +10 -0
  335. package/fixtures/paper-research-question/declared-not-in-paper/paper.tex +6 -0
  336. package/fixtures/paper-research-question/draft/PIPELINE-STATUS.md +6 -0
  337. package/fixtures/paper-research-question/draft/paper.tex +2 -0
  338. package/fixtures/paper-research-question/markdown-no-rq/PIPELINE-STATUS.md +9 -0
  339. package/fixtures/paper-research-question/markdown-no-rq/paper.md +4 -0
  340. package/fixtures/paper-research-question/shipped-no-rq/PIPELINE-STATUS.md +12 -0
  341. package/fixtures/paper-research-question/shipped-no-rq/paper.tex +3 -0
  342. package/fixtures/paper-research-question/shipped-with-rq/PIPELINE-STATUS.md +10 -0
  343. package/fixtures/paper-research-question/shipped-with-rq/paper.tex +2 -0
  344. package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +16 -0
  345. package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +17 -0
  346. package/fixtures/paper-stages/nofile/PIPELINE-STATUS.md +8 -0
  347. package/fixtures/paper-stages/noheader/PIPELINE-STATUS.md +1 -0
  348. package/fixtures/paper-stages/noheader/versions/2026-07-22-submitted.pdf +0 -0
  349. package/fixtures/paper-stages/nothing/PIPELINE-STATUS.md +3 -0
  350. package/fixtures/paper-stages/ok/PIPELINE-STATUS.md +9 -0
  351. package/fixtures/paper-stages/ok/versions/2026-07-22-submitted.pdf +0 -0
  352. package/fixtures/paper-stages/stale/PIPELINE-STATUS.md +1 -0
  353. package/fixtures/paper-stages/stale/versions/2026-07-22-submitted.STALE-WRONG-FILE.pdf +0 -0
  354. package/fixtures/paper-stages/twice/PIPELINE-STATUS.md +14 -0
  355. package/fixtures/paper-stages/twice/versions/2026-08-06-submitted.pdf +0 -0
  356. package/fixtures/paper-stages/twice/versions/2026-10-24-submitted.pdf +0 -0
  357. package/fixtures/paper-stages/undeclared/PIPELINE-STATUS.md +8 -0
  358. package/fixtures/paper-stages/undeclared/versions/2026-07-22-submitted.pdf +0 -0
  359. package/fixtures/paper-stages/undeclared/versions/2026-08-29-camera-ready.pdf +0 -0
  360. package/fixtures/paper-stages/wrongsize/PIPELINE-STATUS.md +8 -0
  361. package/fixtures/paper-stages/wrongsize/versions/2026-07-22-submitted.pdf +0 -0
  362. package/fixtures/paper-typography/clean-paper/paper.tex +29 -0
  363. package/fixtures/paper-typography/messy-paper/paper.tex +27 -0
  364. package/fixtures/pdf-facts/README.md +22 -0
  365. package/fixtures/pdf-facts/corrupt-font.pdf +0 -0
  366. package/fixtures/pdf-facts/encrypted.pdf +0 -0
  367. package/fixtures/pdf-facts/hidden-text.pdf +0 -0
  368. package/fixtures/pdf-facts/hidden-text.tex +28 -0
  369. package/fixtures/pdf-facts/t3-all.pdf +0 -0
  370. package/fixtures/pdf-facts/t3-all.tex +8 -0
  371. package/fixtures/pdf-facts/t3-mixed.pdf +0 -0
  372. package/fixtures/pdf-facts/t3-mixed.tex +9 -0
  373. package/fixtures/pdf-facts/ttf.pdf +2240 -1
  374. package/fixtures/pdf-facts/ttf.tex +6 -0
  375. package/fixtures/real-markdown-paper/baseline.json +24 -0
  376. package/fixtures/real-markdown-paper/baseline.mjs +48 -0
  377. package/fixtures/render-paper/build-clean.sh +25 -0
  378. package/fixtures/render-paper/build-defect.sh +15 -0
  379. package/fixtures/review-findings-cause/clean.md +17 -0
  380. package/fixtures/review-findings-cause/defect.md +14 -0
  381. package/fixtures/review-findings-cause/old-debt.md +14 -0
  382. package/fixtures/review-findings-cause/quiet-in-fence.md +16 -0
  383. package/fixtures/tex-build/clean.tex +21 -0
  384. package/fixtures/tex-build/defect.tex +24 -0
  385. package/fixtures/tex-build/frontmatter-clean.tex +25 -0
  386. package/fixtures/tex-build/frontmatter-defect.tex +23 -0
  387. package/fixtures/toolchain-mirror/catalog.txt +5 -0
  388. package/fixtures/toolchain-mirror/install-tl +27 -0
  389. package/fixtures/toolchain-mirror/release-texlive.txt +3 -0
  390. package/fixtures/toolchain-mirror/release-year +1 -0
  391. package/fixtures/toolchain-mirror/stub-kpsewhich +8 -0
  392. package/fixtures/toolchain-mirror/stub-pdflatex +3 -0
  393. package/fixtures/toolchain-mirror/stub-tlmgr +44 -0
  394. package/hooks/hooks.harness.mjs +713 -0
  395. package/hooks/hooks.mutations.mjs +337 -0
  396. package/hooks/paper-edit-guard.hook.d.mts +13 -0
  397. package/hooks/paper-edit-guard.hook.mjs +457 -0
  398. package/hooks/paper-skills-nudge.hook.mjs +136 -0
  399. package/hooks/paper-status-gates.hook.mjs +156 -0
  400. package/hooks/paper-status-gates.sh +91 -0
  401. package/lib/agent-cli-version.harness.mjs +165 -0
  402. package/lib/agent-cli-version.mjs +106 -0
  403. package/lib/agent-cli-version.mutations.mjs +109 -0
  404. package/lib/markdown.mjs +386 -0
  405. package/lib/mutation-driver.harness.mjs +227 -0
  406. package/lib/mutation-driver.mjs +397 -0
  407. package/lib/mutation-driver.mutations.mjs +68 -0
  408. package/lib/paper-config.d.mts +34 -0
  409. package/lib/paper-config.harness.mjs +286 -0
  410. package/lib/paper-config.mjs +142 -0
  411. package/lib/paper-config.mutations.mjs +143 -0
  412. package/lib/skill-checks.mjs +701 -0
  413. package/lib/skill-corpus.mjs +403 -0
  414. package/lib/skill-eval-fixture.mjs +63 -0
  415. package/lib/skill-eval-kit.mjs +257 -0
  416. package/lib/skill-trigger-cases.harness.mjs +170 -0
  417. package/lib/skill-trigger-cases.mjs +446 -0
  418. package/lib/skill-trigger-cases.mutations.mjs +65 -0
  419. package/lib/trigger-ledger.mjs +215 -0
  420. package/package.json +97 -0
  421. package/plugin/.claude-plugin/plugin.json +8 -0
  422. package/plugin/hooks/hooks.json +30 -0
  423. package/scripts/check.harness.mjs +177 -0
  424. package/scripts/check.mjs +239 -0
  425. package/scripts/check.mutations.mjs +110 -0
  426. package/scripts/eslint-report-guard.mjs +82 -0
  427. package/scripts/exclusive.mjs +138 -0
  428. package/scripts/harness-api.frozen.json +76 -0
  429. package/scripts/harness-api.test.ts +175 -0
  430. package/scripts/layer-legacy-frozen.d.mts +28 -0
  431. package/scripts/layer-legacy-frozen.mjs +152 -0
  432. package/scripts/layer-legacy-frozen.test.ts +115 -0
  433. package/scripts/layer-legacy.frozen.json +50 -0
  434. package/scripts/mutation-batteries-frozen.harness.mjs +204 -0
  435. package/scripts/mutation-batteries-frozen.mjs +238 -0
  436. package/scripts/mutation-batteries.frozen.json +117 -0
  437. package/scripts/release-config.test.ts +90 -0
  438. package/scripts/rules-are-content-only.harness.mjs +113 -0
  439. package/scripts/rules-are-content-only.mjs +138 -0
  440. package/scripts/rules-are-content-only.mutations.mjs +81 -0
  441. package/scripts/rules-see-files.harness.mjs +115 -0
  442. package/scripts/rules-see-files.mjs +99 -0
  443. package/scripts/rules-see-files.mutations.mjs +131 -0
  444. package/scripts/run-mutations.mjs +100 -0
  445. package/scripts/semantic-release-plugins.d.ts +16 -0
  446. package/skills/README.md +15 -0
  447. package/skills/analyze-sibling-paper/SKILL.md +170 -0
  448. package/skills/analyze-sibling-paper/SKILL.md.spec.ts +186 -0
  449. package/skills/analyze-sibling-paper/analyze-sibling-paper.eval.mjs +19 -0
  450. package/skills/analyze-sibling-paper/analyze-sibling-paper.harness.mjs +23 -0
  451. package/skills/argument-arc/SKILL.md +177 -0
  452. package/skills/argument-arc/SKILL.md.spec.ts +192 -0
  453. package/skills/argument-arc/argument-arc.eval.mjs +19 -0
  454. package/skills/argument-arc/argument-arc.harness.mjs +23 -0
  455. package/skills/build-benchmark/SKILL.md +213 -0
  456. package/skills/build-benchmark/SKILL.md.spec.ts +220 -0
  457. package/skills/build-benchmark/build-benchmark.eval.mjs +19 -0
  458. package/skills/build-benchmark/build-benchmark.harness.mjs +23 -0
  459. package/skills/build-benchmark/references/adversarial-cold-repro.md +68 -0
  460. package/skills/camera-ready/SKILL.md +148 -0
  461. package/skills/camera-ready/SKILL.md.spec.ts +164 -0
  462. package/skills/camera-ready/camera-ready.eval.mjs +19 -0
  463. package/skills/camera-ready/camera-ready.harness.mjs +23 -0
  464. package/skills/cold-read-diff/SKILL.md +160 -0
  465. package/skills/cold-read-diff/SKILL.md.spec.ts +166 -0
  466. package/skills/cold-read-diff/cold-read-diff.eval.mjs +19 -0
  467. package/skills/cold-read-diff/cold-read-diff.harness.mjs +23 -0
  468. package/skills/draft-paper/SKILL.md +152 -0
  469. package/skills/draft-paper/SKILL.md.spec.ts +169 -0
  470. package/skills/draft-paper/draft-paper.eval.mjs +19 -0
  471. package/skills/draft-paper/draft-paper.harness.mjs +23 -0
  472. package/skills/extend-paper/SKILL.md +99 -0
  473. package/skills/extend-paper/SKILL.md.spec.ts +116 -0
  474. package/skills/extend-paper/extend-paper.eval.mjs +19 -0
  475. package/skills/extend-paper/extend-paper.harness.mjs +23 -0
  476. package/skills/find-venue/SKILL.md +128 -0
  477. package/skills/find-venue/SKILL.md.spec.ts +145 -0
  478. package/skills/find-venue/find-venue.eval.mjs +19 -0
  479. package/skills/find-venue/find-venue.harness.mjs +23 -0
  480. package/skills/grade-paper-writing/SKILL.md +436 -0
  481. package/skills/grade-paper-writing/SKILL.md.spec.ts +453 -0
  482. package/skills/grade-paper-writing/fixtures/control_gopen.txt +1 -0
  483. package/skills/grade-paper-writing/fixtures/control_human_paper.txt +1 -0
  484. package/skills/grade-paper-writing/fixtures/rewrite.txt +1 -0
  485. package/skills/grade-paper-writing/fixtures/specimen.txt +1 -0
  486. package/skills/grade-paper-writing/fixtures/structure-checks.md +22 -0
  487. package/skills/grade-paper-writing/grade-paper-writing.eval.mjs +19 -0
  488. package/skills/grade-paper-writing/grade-paper-writing.harness.mjs +23 -0
  489. package/skills/grade-paper-writing/prose-lint.mjs +713 -0
  490. package/skills/harden-paper/SKILL.md +318 -0
  491. package/skills/harden-paper/SKILL.md.spec.ts +336 -0
  492. package/skills/harden-paper/check-numbers.sh +33 -0
  493. package/skills/harden-paper/check-release-claims.sh +35 -0
  494. package/skills/harden-paper/fixtures/uncited-assertions-sample.md +43 -0
  495. package/skills/harden-paper/fixtures/uncited-assertions-sample.tex +77 -0
  496. package/skills/harden-paper/harden-paper.eval.mjs +19 -0
  497. package/skills/harden-paper/harden-paper.harness.mjs +23 -0
  498. package/skills/map-prior-work/SKILL.md +211 -0
  499. package/skills/map-prior-work/SKILL.md.spec.ts +227 -0
  500. package/skills/map-prior-work/map-prior-work.eval.mjs +19 -0
  501. package/skills/map-prior-work/map-prior-work.harness.mjs +23 -0
  502. package/skills/osf-artifact-upload/SKILL.md +52 -0
  503. package/skills/osf-artifact-upload/SKILL.md.spec.ts +59 -0
  504. package/skills/osf-artifact-upload/osf-artifact-upload.eval.mjs +22 -0
  505. package/skills/osf-artifact-upload/osf-artifact-upload.harness.mjs +103 -0
  506. package/skills/paper-adversarial-review/SKILL.md +126 -0
  507. package/skills/paper-adversarial-review/SKILL.md.spec.ts +142 -0
  508. package/skills/paper-adversarial-review/paper-adversarial-review.eval.mjs +19 -0
  509. package/skills/paper-adversarial-review/paper-adversarial-review.harness.mjs +23 -0
  510. package/skills/paper-pipeline/PIPELINE-MAP.md +371 -0
  511. package/skills/paper-pipeline/SKILL.md +499 -0
  512. package/skills/paper-pipeline/SKILL.md.spec.ts +517 -0
  513. package/skills/paper-pipeline/description-language.eval.mjs +347 -0
  514. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +891 -0
  515. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +1254 -0
  516. package/skills/paper-pipeline/paper-pipeline.eval.mjs +22 -0
  517. package/skills/paper-pipeline/paper-pipeline.harness.mjs +143 -0
  518. package/skills/paper-pipeline/pipeline-firing.baseline.json +270 -0
  519. package/skills/paper-pipeline/pipeline-firing.eval.mjs +664 -0
  520. package/skills/paper-pipeline/pipeline-language.eval.mjs +672 -0
  521. package/skills/paper-pipeline/references/acceptance-gate.md +329 -0
  522. package/skills/paper-pipeline/references/acl-venue-rules.md +142 -0
  523. package/skills/paper-pipeline/references/anonymization.md +68 -0
  524. package/skills/paper-pipeline/references/artifact-checklist.md +93 -0
  525. package/skills/paper-pipeline/references/body-vs-appendix.md +97 -0
  526. package/skills/paper-pipeline/references/credit-criteria.md +69 -0
  527. package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/README.md +35 -0
  528. package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/run_retext.mjs +24 -0
  529. package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/sentences.txt +11 -0
  530. package/skills/paper-pipeline/references/occupancy-2026-08-06-probe/test_sentences.py +25 -0
  531. package/skills/paper-pipeline/references/occupancy-2026-08-06-prose-checkers.md +538 -0
  532. package/skills/paper-pipeline/references/occupancy-2026-08-06-reproducible-tooling.md +431 -0
  533. package/skills/paper-pipeline/references/occupancy-2026-08-06-staleness-and-orchestration.md +592 -0
  534. package/skills/paper-pipeline/references/pipeline-status-template.md +162 -0
  535. package/skills/paper-pipeline/references/review-ratchet.md +36 -0
  536. package/skills/paper-pipeline/references/sweep-2026-08-09-ideal-pipeline.md +585 -0
  537. package/skills/paper-pipeline/references/writing-craft.md +448 -0
  538. package/skills/paper-pipeline/repro/2026-08-07-description-language-control.log +63 -0
  539. package/skills/paper-pipeline/repro/2026-08-07-fork-check.log +52 -0
  540. package/skills/paper-pipeline/repro/2026-08-07-fork-check2.log +33 -0
  541. package/skills/paper-pipeline/repro/2026-08-07-language-eval-pilot.log +33 -0
  542. package/skills/paper-pipeline/repro/2026-08-07-language-eval-raw.log +166 -0
  543. package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-oracle.json +338 -0
  544. package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-oracle.log +118 -0
  545. package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary-raw.log +245 -0
  546. package/skills/paper-pipeline/repro/2026-08-08-framing-vs-vocabulary.json +776 -0
  547. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-A6-oracle.log +53 -0
  548. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-A6-raw.log +89 -0
  549. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-oracle.json +450 -0
  550. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-oracle.log +136 -0
  551. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-raw.log +242 -0
  552. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation-setupdiff.log +59 -0
  553. package/skills/paper-pipeline/repro/2026-08-08-grade-paper-writing-ablation.json +1032 -0
  554. package/skills/paper-pipeline/repro/2026-08-08-parent-replication-gpw.json +139 -0
  555. package/skills/paper-pipeline/repro/2026-08-08-parent-replication-gpw.log +98 -0
  556. package/skills/paper-pipeline/repro/2026-08-08-parent-replication.mjs +92 -0
  557. package/skills/paper-pipeline/repro/README.md +129 -0
  558. package/skills/paper-pipeline/repro/analyze-language-eval.py +116 -0
  559. package/skills/paper-pipeline/scripts/README.md +344 -0
  560. package/skills/paper-pipeline/scripts/announce.mjs +67 -0
  561. package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +496 -0
  562. package/skills/paper-pipeline/scripts/artifact-coverage.mjs +397 -0
  563. package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +218 -0
  564. package/skills/paper-pipeline/scripts/check-provenance.mjs +184 -0
  565. package/skills/paper-pipeline/scripts/consumer.d.mts +32 -0
  566. package/skills/paper-pipeline/scripts/consumer.harness.mjs +562 -0
  567. package/skills/paper-pipeline/scripts/consumer.mjs +535 -0
  568. package/skills/paper-pipeline/scripts/consumer.mutations.mjs +190 -0
  569. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +457 -0
  570. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +656 -0
  571. package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +54 -0
  572. package/skills/paper-pipeline/scripts/fixtures/clean/PIPELINE-STATUS.md +51 -0
  573. package/skills/paper-pipeline/scripts/fixtures/dirty/PIPELINE-STATUS.md +52 -0
  574. package/skills/paper-pipeline/scripts/fixtures/dirty/paper.md +6 -0
  575. package/skills/paper-pipeline/scripts/fixtures/real-bib/refs.bib +153 -0
  576. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +466 -0
  577. package/skills/paper-pipeline/scripts/generated-code.mjs +338 -0
  578. package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +254 -0
  579. package/skills/paper-pipeline/scripts/ledger.mjs +623 -0
  580. package/skills/paper-pipeline/scripts/ledger.selftest.mjs +286 -0
  581. package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +389 -0
  582. package/skills/paper-pipeline/scripts/pipeline-check.mjs +737 -0
  583. package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +54 -0
  584. package/skills/paper-pipeline/scripts/pipeline-edges.mjs +169 -0
  585. package/skills/paper-pipeline/scripts/population-map.harness.mjs +178 -0
  586. package/skills/paper-pipeline/scripts/population-map.mjs +181 -0
  587. package/skills/paper-pipeline/scripts/population-map.mutations.mjs +65 -0
  588. package/skills/paper-pipeline/scripts/population-map.selftest.mjs +122 -0
  589. package/skills/paper-pipeline/scripts/provenance.harness.mjs +240 -0
  590. package/skills/paper-pipeline/scripts/provenance.mutations.mjs +59 -0
  591. package/skills/paper-pipeline/scripts/round-diff.harness.mjs +881 -0
  592. package/skills/paper-pipeline/scripts/round-diff.mjs +576 -0
  593. package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +276 -0
  594. package/skills/paper-pipeline/scripts/run-mechanical.mjs +633 -0
  595. package/skills/paper-pipeline/scripts/status.mjs +295 -0
  596. package/skills/paper-status/SKILL.md +183 -0
  597. package/skills/paper-status/SKILL.md.spec.ts +190 -0
  598. package/skills/paper-status/paper-status.eval.mjs +22 -0
  599. package/skills/paper-status/paper-status.harness.mjs +25 -0
  600. package/skills/pc-panel-review/SKILL.md +263 -0
  601. package/skills/pc-panel-review/SKILL.md.spec.ts +280 -0
  602. package/skills/pc-panel-review/pc-panel-review.eval.mjs +19 -0
  603. package/skills/pc-panel-review/pc-panel-review.harness.mjs +23 -0
  604. package/skills/plan-paper-timeline/SKILL.md +182 -0
  605. package/skills/plan-paper-timeline/SKILL.md.spec.ts +200 -0
  606. package/skills/plan-paper-timeline/fixtures/fake-google-calendar.mjs +239 -0
  607. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +431 -0
  608. package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +65 -0
  609. package/skills/plan-paper-timeline/plan-paper-timeline.eval.mjs +19 -0
  610. package/skills/plan-paper-timeline/plan-paper-timeline.harness.mjs +23 -0
  611. package/skills/render-paper/SKILL.md +159 -0
  612. package/skills/render-paper/SKILL.md.spec.ts +166 -0
  613. package/skills/render-paper/check-render.sh +419 -0
  614. package/skills/render-paper/checkers-requirements.txt +55 -0
  615. package/skills/render-paper/ensure-checkers.sh +69 -0
  616. package/skills/render-paper/extract-pdf-facts.harness.mjs +166 -0
  617. package/skills/render-paper/extract-pdf-facts.mjs +144 -0
  618. package/skills/render-paper/render-paper.eval.mjs +19 -0
  619. package/skills/render-paper/render-paper.harness.mjs +339 -0
  620. package/skills/research-ideate/SKILL.md +136 -0
  621. package/skills/research-ideate/SKILL.md.spec.ts +152 -0
  622. package/skills/research-ideate/research-ideate.eval.mjs +19 -0
  623. package/skills/research-ideate/research-ideate.harness.mjs +23 -0
  624. package/skills/skill-contract.mutations.mjs +179 -0
  625. package/skills/study-accepted-papers/SKILL.md +206 -0
  626. package/skills/study-accepted-papers/SKILL.md.spec.ts +223 -0
  627. package/skills/study-accepted-papers/study-accepted-papers.eval.mjs +19 -0
  628. package/skills/study-accepted-papers/study-accepted-papers.harness.mjs +23 -0
  629. package/skills/submit-paper/SKILL.md +182 -0
  630. package/skills/submit-paper/SKILL.md.spec.ts +199 -0
  631. package/skills/submit-paper/check-deanon.sh +149 -0
  632. package/skills/submit-paper/references/publishers/acm.md +92 -0
  633. package/skills/submit-paper/references/venues/agenticdev.jsonc +108 -0
  634. package/skills/submit-paper/references/venues/agenticdev.md +139 -0
  635. package/skills/submit-paper/references/venues/agenticdev.tex +19 -0
  636. package/skills/submit-paper/references/venues/aisec.jsonc +101 -0
  637. package/skills/submit-paper/references/venues/aisec.md +105 -0
  638. package/skills/submit-paper/references/venues/paper-guards.tex +41 -0
  639. package/skills/submit-paper/references/venues/realm.jsonc +81 -0
  640. package/skills/submit-paper/references/venues/realm.md +155 -0
  641. package/skills/submit-paper/references/venues/tex-base.jsonc +50 -0
  642. package/skills/submit-paper/references/venues/venue-profile.schema.json +74 -0
  643. package/skills/submit-paper/submit-paper.eval.mjs +19 -0
  644. package/skills/submit-paper/submit-paper.harness.mjs +23 -0
  645. package/skills/sweep-design-space/SKILL.md +269 -0
  646. package/skills/sweep-design-space/SKILL.md.spec.ts +285 -0
  647. package/skills/sweep-design-space/sweep-design-space.eval.mjs +19 -0
  648. package/skills/sweep-design-space/sweep-design-space.harness.mjs +23 -0
  649. package/skills/tighten-paper/SKILL.md +368 -0
  650. package/skills/tighten-paper/SKILL.md.spec.ts +384 -0
  651. package/skills/tighten-paper/structure.mjs +371 -0
  652. package/skills/tighten-paper/tighten-paper.eval.mjs +19 -0
  653. package/skills/tighten-paper/tighten-paper.harness.mjs +23 -0
  654. package/skills/verify-citations/SKILL.md +328 -0
  655. package/skills/verify-citations/SKILL.md.spec.ts +345 -0
  656. package/skills/verify-citations/scripts/bib-authors.mjs +479 -0
  657. package/skills/verify-citations/scripts/bib-authors.test.mjs +175 -0
  658. package/skills/verify-citations/scripts/verify-cites.mjs +1108 -0
  659. package/skills/verify-citations/scripts/verify-cites.test.mjs +735 -0
  660. package/skills/verify-citations/verify-citations.eval.mjs +19 -0
  661. package/skills/verify-citations/verify-citations.harness.mjs +23 -0
  662. package/src/CLAUDE.md +51 -0
  663. package/src/action-ref.test.ts +26 -0
  664. package/src/action-ref.ts +15 -0
  665. package/src/adapters/banal/failure.test.ts +63 -0
  666. package/src/adapters/banal/failure.ts +118 -0
  667. package/src/adapters/banal/index.test.ts +119 -0
  668. package/src/adapters/banal/index.ts +100 -0
  669. package/src/adapters/banal/install.test.ts +20 -0
  670. package/src/adapters/banal/install.ts +41 -0
  671. package/src/adapters/banal/invocation.test.ts +74 -0
  672. package/src/adapters/banal/invocation.ts +95 -0
  673. package/src/adapters/banal/locate.test.ts +52 -0
  674. package/src/adapters/banal/locate.ts +84 -0
  675. package/src/adapters/banal/output.test.ts +140 -0
  676. package/src/adapters/banal/output.ts +141 -0
  677. package/src/adapters/banal/pin.ts +30 -0
  678. package/src/adapters/banal/probe.ts +35 -0
  679. package/src/adapters/banal/run.test.ts +191 -0
  680. package/src/adapters/banal/run.ts +244 -0
  681. package/src/adapters/banal/settings.test.ts +31 -0
  682. package/src/adapters/banal/settings.ts +55 -0
  683. package/src/adapters/banal/xml.test.ts +111 -0
  684. package/src/adapters/banal/xml.ts +112 -0
  685. package/src/adapters/curl/download.io.ts +73 -0
  686. package/src/adapters/curl/download.test.ts +55 -0
  687. package/src/adapters/curl/index.ts +5 -0
  688. package/src/adapters/memory/index.ts +131 -0
  689. package/src/adapters/node/files.io.ts +39 -0
  690. package/src/adapters/node/files.test.ts +28 -0
  691. package/src/adapters/node/host.io.ts +15 -0
  692. package/src/adapters/node/index.ts +36 -0
  693. package/src/adapters/node/process.io.ts +49 -0
  694. package/src/adapters/node/process.test.ts +46 -0
  695. package/src/adapters/node/workspace.io.ts +40 -0
  696. package/src/adapters/node/workspace.test.ts +58 -0
  697. package/src/adapters/pdfjs/fill.test.ts +111 -0
  698. package/src/adapters/pdfjs/fill.ts +141 -0
  699. package/src/build-engine.harness.mjs +314 -0
  700. package/src/build-engine.ts +219 -0
  701. package/src/build.harness.mjs +631 -0
  702. package/src/build.mutations.mjs +195 -0
  703. package/src/build.ts +793 -0
  704. package/src/cli.harness.mjs +2007 -0
  705. package/src/cli.mutations.mjs +448 -0
  706. package/src/cli.ts +1189 -0
  707. package/src/doctor.harness.mjs +396 -0
  708. package/src/doctor.mutations.mjs +175 -0
  709. package/src/doctor.ts +356 -0
  710. package/src/domain/geometry.ts +108 -0
  711. package/src/domain/host.ts +23 -0
  712. package/src/domain/page-layout.ts +32 -0
  713. package/src/domain/paths.ts +5 -0
  714. package/src/domain/result.test.ts +26 -0
  715. package/src/domain/result.ts +29 -0
  716. package/src/domain/sha256.test.ts +12 -0
  717. package/src/domain/sha256.ts +21 -0
  718. package/src/domain/text.ts +11 -0
  719. package/src/engine.harness.mjs +252 -0
  720. package/src/engine.ts +176 -0
  721. package/src/exit-code.test.ts +21 -0
  722. package/src/exit-code.ts +38 -0
  723. package/src/facts-file.test.ts +240 -0
  724. package/src/facts-file.ts +241 -0
  725. package/src/hooks-settings.harness.mjs +386 -0
  726. package/src/hooks-settings.mutations.mjs +116 -0
  727. package/src/hooks-settings.ts +434 -0
  728. package/src/init.ts +900 -0
  729. package/src/latex-log.harness.mjs +226 -0
  730. package/src/latex-log.ts +234 -0
  731. package/src/latex-loop.harness.mjs +449 -0
  732. package/src/latex-loop.ts +211 -0
  733. package/src/link-skills.harness.mjs +273 -0
  734. package/src/link-skills.mutations.mjs +136 -0
  735. package/src/link-skills.ts +258 -0
  736. package/src/new-paper.harness.mjs +216 -0
  737. package/src/new-paper.mutations.mjs +79 -0
  738. package/src/new-paper.ts +158 -0
  739. package/src/pdf-facts.harness.mjs +188 -0
  740. package/src/pdf-facts.ts +327 -0
  741. package/src/pdf-geometry.harness.mjs +254 -0
  742. package/src/pdf-geometry.ts +300 -0
  743. package/src/ports/download.ts +10 -0
  744. package/src/ports/files.ts +11 -0
  745. package/src/ports/measure-geometry.ts +8 -0
  746. package/src/ports/process.ts +46 -0
  747. package/src/ports/tool-installer.ts +33 -0
  748. package/src/ports/workspace.ts +20 -0
  749. package/src/rules-config.harness.mjs +114 -0
  750. package/src/rules-config.ts +178 -0
  751. package/src/structure.harness.mjs +179 -0
  752. package/src/structure.mutations.mjs +83 -0
  753. package/src/structure.ts +166 -0
  754. package/src/tex-requirements.harness.mjs +238 -0
  755. package/src/tex-requirements.ts +181 -0
  756. package/src/toolchain.harness.mjs +651 -0
  757. package/src/toolchain.ts +755 -0
  758. package/src/types.ts +106 -0
  759. package/templates/paper/PIPELINE-STATUS.md +72 -0
  760. package/templates/paper/paper.md +4 -0
  761. package/templates/paper/paper.tex +8 -0
  762. package/tsconfig.json +23 -0
@@ -0,0 +1,178 @@
1
+ /**
2
+ * THE CONSUMER'S SETTINGS, PARSED AT THE BOUNDARY — unknown keys refused, and the `rules` key
3
+ * turned into ESLint config blocks.
4
+ *
5
+ * ── THE `rules` KEY: ESLint'S OWN SHAPE, NOT A NEW ONE ──────────────────────────
6
+ * A consumer turns an optional rule on, or changes a built-in rule's severity, by writing what they
7
+ * would write in an ESLint flat config — a list of `{ files, ignores, rules }` blocks:
8
+ *
9
+ * "rules": [ { "files": ["papers/agenticdev-2026/**"],
10
+ * "rules": { "pdf/last-page-balance": ["error", { "tolerancePt": 120 }] } } ]
11
+ *
12
+ * The blocks are appended AFTER rpp's built-in config, so a later block wins, exactly as in ESLint.
13
+ * `files` and `ignores` are resolved relative to the directory of the file that holds the settings,
14
+ * as ESLint resolves them relative to its config file: each block gets that directory as ESLint's
15
+ * own `basePath`, so paperlint matches nothing itself.
16
+ *
17
+ * ── WHY PARSED HERE, AND WHY STRICT ─────────────────────────────────────────────
18
+ * ESLint would reject a bad severity or an unknown rule too — but as a stack trace from inside
19
+ * `lintFiles`, naming neither the settings file nor the block. And a key paperlint does not read, at the
20
+ * top level or in a block, ESLint never sees at all: a typo would silently read as "not set". So
21
+ * every problem is one line naming the key path, before ESLint starts, and nothing downstream sees
22
+ * the raw object again.
23
+ *
24
+ * Only rules paperlint ships may be named: the settings configure paperlint, and a rule from another plugin
25
+ * would need that plugin, which JSON cannot carry. Which rules paperlint ships is read off its own
26
+ * config (`buildConfig`), not listed a second time.
27
+ */
28
+ import { SETTINGS_KEYS } from "../lib/paper-config.mjs";
29
+
30
+ /** ESLint's severities, in both of its spellings. */
31
+ export type Severity = "off" | "warn" | "error" | 0 | 1 | 2;
32
+ export type RuleEntry = Severity | readonly [Severity, ...unknown[]];
33
+
34
+ /** One block of the `rules` key, parsed. `basePath` is the settings file's directory. */
35
+ export interface RuleBlock {
36
+ readonly basePath: string;
37
+ readonly files?: readonly string[];
38
+ readonly ignores?: readonly string[];
39
+ readonly rules: Readonly<Record<string, RuleEntry>>;
40
+ }
41
+
42
+ export type Parsed<T> =
43
+ | { readonly ok: true; readonly value: T }
44
+ | { readonly ok: false; readonly error: string };
45
+
46
+ const SEVERITIES: readonly unknown[] = ["off", "warn", "error", 0, 1, 2];
47
+ const BLOCK_KEYS = new Set(["files", "ignores", "rules"]);
48
+
49
+ const bad = <T>(error: string): Parsed<T> => ({ ok: false, error });
50
+
51
+ /** Every top-level key must be one paperlint or its skills read. */
52
+ export function unknownKeys(settings: Record<string, unknown>): string[] {
53
+ return Object.keys(settings).filter((k) => !Object.hasOwn(SETTINGS_KEYS, k));
54
+ }
55
+
56
+ const isStringList = (v: unknown): v is string[] =>
57
+ Array.isArray(v) &&
58
+ v.length > 0 &&
59
+ v.every((x) => typeof x === "string" && x !== "");
60
+
61
+ /** A severity, or `[severity, ...options]`. */
62
+ function parseEntry(v: unknown): RuleEntry | null {
63
+ const sev = Array.isArray(v) ? v[0] : v;
64
+ if (!SEVERITIES.includes(sev)) return null;
65
+ return v as RuleEntry;
66
+ }
67
+
68
+ /** One block's `rules` object. */
69
+ function parseRules(
70
+ v: unknown,
71
+ at: string,
72
+ shipped: ReadonlySet<string>,
73
+ ): Parsed<Record<string, RuleEntry>> {
74
+ if (typeof v !== "object" || v === null || Array.isArray(v))
75
+ return bad(`${at}.rules must be an object of rule id → severity`);
76
+ const out: Record<string, RuleEntry> = {};
77
+ for (const [id, raw] of Object.entries(v)) {
78
+ if (!shipped.has(id))
79
+ return bad(
80
+ `${at}.rules: "${id}" is not a rule paperlint ships — known: ${[...shipped].sort().join(", ")}`,
81
+ );
82
+ const entry = parseEntry(raw);
83
+ if (entry === null)
84
+ return bad(
85
+ `${at}.rules["${id}"]: ${JSON.stringify(raw)} is not a severity — use "off", "warn" or "error", or ["error", { options }]`,
86
+ );
87
+ out[id] = entry;
88
+ }
89
+ return { ok: true, value: out };
90
+ }
91
+
92
+ /** `files` and `ignores`, each absent or a non-empty list of globs; the first bad one, or null. */
93
+ function badGlobs(o: Record<string, unknown>, at: string): string | null {
94
+ for (const k of ["files", "ignores"] as const)
95
+ if (o[k] !== undefined && !isStringList(o[k]))
96
+ return `${at}.${k} must be a non-empty list of glob strings`;
97
+ return null;
98
+ }
99
+
100
+ /** The block's `files`/`ignores`, present only when given. */
101
+ const globsOf = (o: Record<string, unknown>) => ({
102
+ ...(o["files"] ? { files: o["files"] as string[] } : {}),
103
+ ...(o["ignores"] ? { ignores: o["ignores"] as string[] } : {}),
104
+ });
105
+
106
+ /** One element of the `rules` list. */
107
+ function parseBlock(
108
+ v: unknown,
109
+ at: string,
110
+ ctx: { shipped: ReadonlySet<string>; baseDir: string },
111
+ ): Parsed<RuleBlock> {
112
+ if (typeof v !== "object" || v === null || Array.isArray(v))
113
+ return bad(
114
+ `${at} must be an object like { "files": [...], "rules": {...} }`,
115
+ );
116
+ const o = v as Record<string, unknown>;
117
+ const extra = Object.keys(o).filter((k) => !BLOCK_KEYS.has(k));
118
+ if (extra.length)
119
+ return bad(
120
+ `${at}: unknown key "${extra[0]}" — a block takes "files", "ignores" and "rules"`,
121
+ );
122
+ const globs = badGlobs(o, at);
123
+ if (globs) return bad(globs);
124
+ const rules = parseRules(o["rules"], at, ctx.shipped);
125
+ if (!rules.ok) return rules;
126
+ return {
127
+ ok: true,
128
+ value: { basePath: ctx.baseDir, ...globsOf(o), rules: rules.value },
129
+ };
130
+ }
131
+
132
+ /**
133
+ * The `rules` key → config blocks, or one error naming the key path.
134
+ *
135
+ * @param raw the value under `rules` (undefined when absent)
136
+ * @param where how the settings are named in messages, e.g. `package.json → "paperlint"`
137
+ * @param shipped the rule ids paperlint ships (`shippedRuleIds`)
138
+ * @param baseDir the directory of the file holding the settings
139
+ */
140
+ export function parseRuleBlocks(
141
+ raw: unknown,
142
+ where: string,
143
+ shipped: ReadonlySet<string>,
144
+ baseDir: string,
145
+ ): Parsed<readonly RuleBlock[]> {
146
+ if (raw === undefined) return { ok: true, value: [] };
147
+ if (!Array.isArray(raw))
148
+ return bad(
149
+ `${where}.rules must be a list of blocks, like ESLint's flat config: [{ "files": [...], "rules": {...} }]`,
150
+ );
151
+ const out: RuleBlock[] = [];
152
+ for (const [i, v] of raw.entries()) {
153
+ const b = parseBlock(v, `${where}.rules[${i}]`, { shipped, baseDir });
154
+ if (!b.ok) return b;
155
+ out.push(b.value);
156
+ }
157
+ return { ok: true, value: out };
158
+ }
159
+
160
+ /**
161
+ * The rule ids a config defines through its own plugins, as `<plugin>/<rule>`. Plugins passed in
162
+ * `foreign` (a dependency's plugin, such as `@eslint/markdown`) are not rpp's and are skipped.
163
+ */
164
+ export function shippedRuleIds(
165
+ config: readonly unknown[],
166
+ foreign: readonly unknown[],
167
+ ): Set<string> {
168
+ const ids = new Set<string>();
169
+ for (const block of config) {
170
+ const plugins = (block as { plugins?: Record<string, unknown> }).plugins;
171
+ for (const [name, plugin] of Object.entries(plugins ?? {})) {
172
+ if (foreign.includes(plugin)) continue;
173
+ const rules = (plugin as { rules?: Record<string, unknown> }).rules;
174
+ for (const rule of Object.keys(rules ?? {})) ids.add(`${name}/${rule}`);
175
+ }
176
+ }
177
+ return ids;
178
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Both halves for `structure.mjs` — the package's only check whose subject is a file's ABSENCE.
3
+ *
4
+ * 🔴 THE MAIN THING PINNED DOWN HERE IS NOT FIRING, IT'S SILENCE. An error-level check that
5
+ * fails on a correct tree does not get fixed, it gets turned off, and real findings go with it.
6
+ * So every "found" here has a paired "not found on the neighboring directory", and the defaults
7
+ * are separately checked against the shape of the live corpus.
8
+ */
9
+ import assert from "node:assert/strict";
10
+ import {
11
+ mkdtempSync,
12
+ mkdirSync,
13
+ writeFileSync,
14
+ rmSync,
15
+ realpathSync,
16
+ } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join, dirname } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+
21
+ const HERE = dirname(fileURLToPath(import.meta.url));
22
+ const { checkStructure, formatStructure, asEslintResults, STRUCTURE_DEFAULTS } =
23
+ await import(join(HERE, "structure.ts"));
24
+
25
+ let n = 0;
26
+ const check = (label, cond) => {
27
+ assert.ok(cond, label);
28
+ n++;
29
+ };
30
+
31
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "rpp-struct-")));
32
+ const papers = join(root, "papers");
33
+ const paper = (name, files) => {
34
+ const dir = join(papers, name);
35
+ mkdirSync(dir, { recursive: true });
36
+ for (const f of files) {
37
+ mkdirSync(dirname(join(dir, f)), { recursive: true });
38
+ writeFileSync(join(dir, f), "x");
39
+ }
40
+ return dir;
41
+ };
42
+
43
+ try {
44
+ paper("complete", ["PIPELINE-STATUS.md", "paper.tex", "refs.bib"]);
45
+ paper("complete-md", ["PIPELINE-STATUS.md", "paper.md"]);
46
+ // the marker is present (paper.tex), the scorecard is not — the directory is linted by ZERO
47
+ // rules and reports clean
48
+ paper("no-scorecard", ["paper.tex", "refs.bib"]);
49
+ // the scorecard is present, the source is not
50
+ paper("no-source", ["PIPELINE-STATUS.md"]);
51
+ // not a single marker — this is a sibling in the corpus, not a paper
52
+ paper("research", ["NOTES.md", "plan/ideas.md"]);
53
+ mkdirSync(join(papers, ".hidden"), { recursive: true });
54
+ writeFileSync(join(papers, ".hidden", "paper.tex"), "x");
55
+
56
+ const f = checkStructure([papers], undefined, { cwd: root });
57
+ const at = (name) => f.filter((x) => x.file.endsWith(name));
58
+
59
+ check(
60
+ "a complete paper directory — NOT A SINGLE finding",
61
+ at("complete").length === 0,
62
+ );
63
+ check(
64
+ "and `paper.md` counts on equal footing with `paper.tex` — the corpus holds both forms",
65
+ at("complete-md").length === 0,
66
+ );
67
+ check(
68
+ "a missing scorecard — a finding",
69
+ at("no-scorecard").length === 1 &&
70
+ /missing `PIPELINE-STATUS\.md`/.test(at("no-scorecard")[0].message),
71
+ );
72
+ check(
73
+ "and the message names the CONSEQUENCE, not a restatement of the condition",
74
+ /is checked/.test(at("no-scorecard")[0].message) &&
75
+ /stages, sources or authors/.test(at("no-scorecard")[0].message),
76
+ );
77
+ check(
78
+ "and the consequence names THIS directory by name",
79
+ /no-scorecard/.test(at("no-scorecard")[0].message),
80
+ );
81
+ check(
82
+ "a directory with no source — a finding, and BOTH accepted forms are listed",
83
+ at("no-source").length === 1 &&
84
+ /`paper\.tex`/.test(at("no-source")[0].message) &&
85
+ /`paper\.md`/.test(at("no-source")[0].message),
86
+ );
87
+
88
+ // 🔴 THE PAIRED HALF: detection is GENEROUS. Without this the check would scream about every
89
+ // neighboring directory in the corpus, get turned off, and take the three findings above with it.
90
+ check(
91
+ "a directory WITHOUT a single pipeline marker is left alone entirely",
92
+ at("research").length === 0,
93
+ );
94
+ check("and so are hidden directories", at(".hidden").length === 0);
95
+ check("exactly two findings total — nothing extra turned up", f.length === 2);
96
+
97
+ // ── consumer config ────────────────────────────────────────────────────────────────
98
+ check(
99
+ "`ignore` exempts a directory by name",
100
+ checkStructure(
101
+ [papers],
102
+ { ignore: ["no-scorecard", "no-source"] },
103
+ {
104
+ cwd: root,
105
+ },
106
+ ).length === 0,
107
+ );
108
+ check(
109
+ "`structure: false` turns the check off entirely",
110
+ checkStructure([papers], false, { cwd: root }).length === 0,
111
+ );
112
+ check(
113
+ "a requirement ON TOP OF the defaults fires — the config genuinely gets through",
114
+ checkStructure(
115
+ [papers],
116
+ { require: ["PIPELINE-STATUS.md", "refs.bib"] },
117
+ {
118
+ cwd: root,
119
+ },
120
+ ).some(
121
+ (x) => x.file.endsWith("complete-md") && /refs\.bib/.test(x.message),
122
+ ),
123
+ );
124
+ check(
125
+ "a nonexistent root does not crash it — that's the empty-set guard talking",
126
+ checkStructure([join(root, "nope")], undefined, { cwd: root }).length === 0,
127
+ );
128
+
129
+ // ── defaults: a measurement, not a taste ───────────────────────────────────────────────
130
+ check(
131
+ "`paper.pdf` is NOT in the defaults — two papers in the live corpus keep the pdf under a different name",
132
+ !STRUCTURE_DEFAULTS.require.includes("paper.pdf"),
133
+ );
134
+ check(
135
+ "and `venue.json` counts as a marker but not a requirement",
136
+ STRUCTURE_DEFAULTS.markers.includes("venue.json") &&
137
+ !STRUCTURE_DEFAULTS.require.includes("venue.json"),
138
+ );
139
+
140
+ // ── one schema for both halves ────────────────────────────────────────────────────────
141
+ const asResults = asEslintResults(f);
142
+ check(
143
+ "findings come back shaped like an ESLint result — `--json` stays one array",
144
+ asResults.length === 2 &&
145
+ asResults.every(
146
+ (r) =>
147
+ r.errorCount === 1 &&
148
+ r.warningCount === 0 &&
149
+ r.messages[0].ruleId === "structure/required-file" &&
150
+ r.messages[0].severity === 2,
151
+ ),
152
+ );
153
+ check(
154
+ "two findings in ONE directory collapse into one result with errorCount 2",
155
+ (() => {
156
+ const two = checkStructure(
157
+ [papers],
158
+ { require: ["PIPELINE-STATUS.md", "refs.bib"] },
159
+ { cwd: root },
160
+ ).filter((x) => x.file.endsWith("no-source"));
161
+ return asEslintResults(two)[0].errorCount === 2;
162
+ })(),
163
+ );
164
+ check(
165
+ "the human-readable format names the directory and the finding",
166
+ /no-scorecard/.test(formatStructure(f)) &&
167
+ /error {2}missing/.test(formatStructure(f)),
168
+ );
169
+ check(
170
+ "and the path is RELATIVE — an absolute path to the temp directory tells the reader nothing",
171
+ f.every((x) => !x.file.startsWith("/")),
172
+ );
173
+ } finally {
174
+ rmSync(root, { recursive: true, force: true });
175
+ }
176
+
177
+ console.log(
178
+ `✓ ${String(n)} assertions passed — structure: a missing file cannot complain for itself`,
179
+ );
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Battery for `structure.ts` — the package's only check whose subject is a file's ABSENCE.
3
+ *
4
+ * A check like this is especially vulnerable to silent failure: it reports on what is missing,
5
+ * so a broken one looks exactly like a clean corpus. This battery exists because the mutation gate
6
+ * refused to accept a harness that nothing could kill.
7
+ */
8
+ import { fileURLToPath } from "node:url";
9
+ import { dirname, join, resolve } from "node:path";
10
+ import { runMutations } from "../lib/mutation-driver.mjs";
11
+
12
+ const HERE = dirname(fileURLToPath(import.meta.url));
13
+ const ROOT = resolve(HERE, "..");
14
+ const SRC = join(HERE, "structure.ts");
15
+ const HARNESS = join(HERE, "structure.harness.mjs");
16
+
17
+ process.exit(
18
+ runMutations({
19
+ root: ROOT,
20
+ runner: "node",
21
+ cases: [
22
+ {
23
+ name: "generous detection is removed — any directory counts as a paper",
24
+ harness: HARNESS,
25
+ expect:
26
+ "a directory WITHOUT a single pipeline marker is left alone entirely",
27
+ disables:
28
+ "the condition the check uses at all to decide it is looking at a paper. Without it, " +
29
+ "findings would rain down on `data/`, `figures/` and any neighboring directory — and a " +
30
+ "rule that yells at the innocent gets turned off entirely, real findings and all",
31
+ edits: [
32
+ [
33
+ SRC,
34
+ " if (!rules.markers.some((m: string) => existsSync(join(dir, m))))\n continue;",
35
+ " if (false) continue;",
36
+ ],
37
+ ],
38
+ },
39
+ {
40
+ name: "`ignore` stops exempting",
41
+ harness: HARNESS,
42
+ expect: "`ignore` exempts a directory by name",
43
+ disables:
44
+ 'the consumer\'s only way to say "this directory is not a paper". Without it, an ' +
45
+ "exemption has to be expressed by renaming the directory",
46
+ edits: [
47
+ [SRC, " if (rules.ignore.includes(name)) continue;\n", ""],
48
+ ],
49
+ },
50
+ {
51
+ name: "the second accepted source form falls out of the defaults",
52
+ harness: HARNESS,
53
+ expect: "`paper.md` counts on equal footing with `paper.tex`",
54
+ disables:
55
+ "the second source form the live corpus keeps. A paper on `paper.md` would become a " +
56
+ '"no source" finding while its source is right there — i.e. the check would yell at ' +
57
+ "the innocent, and such rules get turned off entirely, real findings and all",
58
+ edits: [
59
+ [
60
+ SRC,
61
+ 'requireOneOf: [["paper.tex", "paper.md"]]',
62
+ 'requireOneOf: [["paper.tex"]]',
63
+ ],
64
+ ],
65
+ },
66
+ {
67
+ name: "the path in a finding goes absolute again",
68
+ harness: HARNESS,
69
+ expect: "and the path is RELATIVE",
70
+ disables:
71
+ "the readability of the finding. An absolute path to the run's temp directory tells " +
72
+ "the reader nothing and on top of that makes the output useless for comparing across machines",
73
+ edits: [
74
+ [
75
+ SRC,
76
+ ' return rel &&\n rel !== ".." &&\n !rel.startsWith(`..${sep}`) &&\n !isAbsolute(rel)\n ? rel\n : p;',
77
+ " return p;",
78
+ ],
79
+ ],
80
+ },
81
+ ],
82
+ }),
83
+ );
@@ -0,0 +1,166 @@
1
+ /**
2
+ * `paperlint lint` — the half ESLint cannot do BY CONSTRUCTION: checking that the required file
3
+ * IS THERE.
4
+ *
5
+ * 🔴 WHY THIS IS A SEPARATE MODULE AND NOT A RULE. A rule is invoked for a file that was handed
6
+ * to the linter. A file that does not exist is never handed over — so the rule is not invoked,
7
+ * and, not having been invoked, it physically cannot report the absence. A paper directory with
8
+ * no `PIPELINE-STATUS.md` yields NOT ONE finding: not a single pipeline rule runs over it, and
9
+ * the run is green. This is exactly the green zero the `nothing was linted` guard in the CLI
10
+ * stands for, only one level down — not "nothing was linted", but "not everything here was".
11
+ *
12
+ * ⚠️ THIS IS NOT ls-lint's JOB, AND THE TWO ARE NOT INTERCHANGEABLE. ls-lint judges the NAMES of
13
+ * files that exist (`versions/2026-07-22-submitted.pdf` is named to the scheme). About a file
14
+ * that does not exist it says nothing — it has nothing to match. The two halves of structure:
15
+ * ls-lint — "what is there is named correctly"
16
+ * this module — "what is required is there"
17
+ *
18
+ * 🔴 DETECTION IS GENEROUS, REQUIREMENTS ARE STRICT — and this is for the sake of false positives.
19
+ * An error-level rule that fails on a correct tree does not get fixed, it gets turned off, and the
20
+ * real findings go with it. So a directory counts as a paper only if it ALREADY holds at least one
21
+ * pipeline marker; `research/`, `plans/` and the other neighbours in the corpus are not touched at
22
+ * all.
23
+ */
24
+ // eslint-disable-next-line boundaries/dependencies -- legacy I/O, moves behind a port in #76
25
+ import { readdirSync, existsSync } from "node:fs";
26
+ import { join, relative, basename, isAbsolute, sep } from "node:path";
27
+ import type { StructureConfig, StructureFinding } from "./types.ts";
28
+
29
+ /** The requirements after the consumer's config is laid over the defaults. */
30
+ type Rules = Required<StructureConfig>;
31
+
32
+ /**
33
+ * The defaults are MEASURED against a live corpus of five paper directories, not picked by taste:
34
+ * under them it passes in full — zero findings. The counter-case was run too: `paper.pdf` added to
35
+ * `require` yields two findings straight away on papers that are perfectly fine — two of the four
36
+ * keep their pdf under a different name. That is why it is not in the defaults; declared pdfs are
37
+ * byte-compared by the `paper/stages` rule anyway.
38
+ */
39
+ export const STRUCTURE_DEFAULTS = {
40
+ markers: ["PIPELINE-STATUS.md", "paper.tex", "paper.md", "venue.json"],
41
+ require: ["PIPELINE-STATUS.md"],
42
+ requireOneOf: [["paper.tex", "paper.md"]],
43
+ ignore: [],
44
+ };
45
+
46
+ /** The consumer's config over the defaults; `structure: false` turns the check off entirely. */
47
+ export function structureRules(
48
+ structure: StructureConfig | false | undefined,
49
+ ): Rules | null {
50
+ if (structure === false) return null;
51
+ return { ...STRUCTURE_DEFAULTS, ...(structure ?? {}) };
52
+ }
53
+
54
+ const dirsIn = (dir: string): string[] => {
55
+ try {
56
+ return readdirSync(dir, { withFileTypes: true })
57
+ .filter((e) => e.isDirectory() && !e.name.startsWith("."))
58
+ .map((e) => e.name);
59
+ } catch {
60
+ // A non-existent directory is not our concern: the empty-set guard says so loudly.
61
+ return [];
62
+ }
63
+ };
64
+
65
+ /**
66
+ * @returns {{file: string, message: string}[]} findings, one per missing file.
67
+ */
68
+ export function checkStructure(
69
+ paths: readonly string[],
70
+ structure: StructureConfig | false | undefined,
71
+ // eslint-disable-next-line no-restricted-globals -- legacy I/O, moves behind a port in #76
72
+ { cwd = process.cwd() }: { cwd?: string } = {},
73
+ ): StructureFinding[] {
74
+ const rules = structureRules(structure);
75
+ if (!rules) return [];
76
+ const findings: StructureFinding[] = [];
77
+ // Relative to where the command was typed — unless the paper lives outside it (#48): then the
78
+ // absolute path, not a ladder of `../../../` that has to be counted to be read.
79
+ const say = (p: string): string => {
80
+ const rel = relative(cwd, p);
81
+ return rel &&
82
+ rel !== ".." &&
83
+ !rel.startsWith(`..${sep}`) &&
84
+ !isAbsolute(rel)
85
+ ? rel
86
+ : p;
87
+ };
88
+
89
+ for (const root of paths) {
90
+ for (const name of dirsIn(root)) {
91
+ if (rules.ignore.includes(name)) continue;
92
+ const dir = join(root, name);
93
+
94
+ // Generous detection: with not one marker this is just a neighbour directory, not a paper.
95
+ if (!rules.markers.some((m: string) => existsSync(join(dir, m))))
96
+ continue;
97
+
98
+ for (const required of rules.require)
99
+ if (!existsSync(join(dir, required)))
100
+ findings.push({
101
+ file: say(dir),
102
+ message: `missing \`${required}\` — ${whyMissingMatters(required, basename(dir))}`,
103
+ });
104
+
105
+ for (const group of rules.requireOneOf)
106
+ if (!group.some((f: string) => existsSync(join(dir, f))))
107
+ findings.push({
108
+ file: say(dir),
109
+ message: `missing all of ${group.map((f: string) => `\`${f}\``).join(", ")} — a paper directory with no source is not something the rules can check`,
110
+ });
111
+ }
112
+ }
113
+ return findings;
114
+ }
115
+
116
+ /**
117
+ * The message NAMES THE CONSEQUENCE rather than restating the condition. "missing
118
+ * PIPELINE-STATUS.md" without the second half reads as nitpicking about formatting; with it you
119
+ * can see which checks the directory silently loses — the rules that read the scorecard; the rules
120
+ * over `paper.md` / `paper.tex` still run.
121
+ */
122
+ function whyMissingMatters(file: string, dirName: string): string {
123
+ if (file === "PIPELINE-STATUS.md")
124
+ return `\`paper/stages\`, \`paper/source\` and \`paper/author-list\` read this file, so nothing \`${dirName}\` declares about its stages, sources or authors is checked`;
125
+ return `declared as required by your \`structure\` configuration`;
126
+ }
127
+
128
+ export function formatStructure(findings: readonly StructureFinding[]): string {
129
+ const byDir = new Map<string, string[]>();
130
+ for (const f of findings)
131
+ byDir.set(f.file, [...(byDir.get(f.file) ?? []), f.message]);
132
+ return [...byDir]
133
+ .map(([dir, msgs]) => [dir, ...msgs.map((m) => ` error ${m}`)].join("\n"))
134
+ .join("\n\n");
135
+ }
136
+
137
+ /**
138
+ * 🔴 ONE SCHEMA FOR BOTH HALVES. Findings about absence are emitted in THE SAME shape as ESLint
139
+ * findings, so `--json` stays a single parseable array. A separate channel would force every
140
+ * consumer to write a second parser — and the first one who did not write it would read "there
141
+ * are no structural findings" instead of "I do not parse them".
142
+ */
143
+ export function asEslintResults(
144
+ findings: readonly StructureFinding[],
145
+ ): unknown[] {
146
+ const byDir = new Map<string, string[]>();
147
+ for (const f of findings)
148
+ byDir.set(f.file, [...(byDir.get(f.file) ?? []), f.message]);
149
+ return [...byDir].map(([filePath, msgs]) => ({
150
+ filePath,
151
+ messages: msgs.map((message: string) => ({
152
+ ruleId: "structure/required-file",
153
+ severity: 2,
154
+ message,
155
+ line: 0,
156
+ column: 0,
157
+ })),
158
+ errorCount: msgs.length,
159
+ fatalErrorCount: 0,
160
+ warningCount: 0,
161
+ fixableErrorCount: 0,
162
+ fixableWarningCount: 0,
163
+ suppressedMessages: [],
164
+ usedDeprecatedRules: [],
165
+ }));
166
+ }