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,591 @@
1
+ /**
2
+ * AN ESLint LANGUAGE FOR `.tex` — so that the ~30 prose rules of a paper pipeline read papers
3
+ * written in LaTeX, and not only papers written in markdown. The spike it grew out of lives in
4
+ * the source corpus as a coverage measurement plus an executable prototype kept as history.
5
+ *
6
+ * ── CONSTRUCTION: two legs ──────────────────────────────────────────────────
7
+ * 1. AST — a flat mdast-like root (heading / code / html / strong) with REAL positions taken
8
+ * from `@unified-latex`.
9
+ * 2. TEXT — a projection of `.tex` into a markdown-like view THAT PRESERVES LENGTHS AND
10
+ * OFFSETS: comments, the preamble, math, service macros and code environments are blanked
11
+ * with spaces; `\section{X}` projects to `## X `, `\texttt{X}` to `` `X` ``.
12
+ * The point: a rule receives what it used to see in markdown and KNOWS NOTHING about
13
+ * LaTeX, while the position of a finding points into the real `.tex`. Not one rule was
14
+ * rewritten for LaTeX.
15
+ *
16
+ * ── HEADING SYNTHESIS: not decoration, but the repair of seven silent rules ──
17
+ * Measured 2026-08-26 across three real papers: NONE of them has a `Limitations` /
18
+ * `References` / `Appendix` heading, none has `\section*`, none has `\appendix`. The
19
+ * bibliography is declared by `\bibliography{refs}` or `\begin{thebibliography}`, the
20
+ * abstract by an environment. Rules take their "free half" from HEADINGS, so without
21
+ * synthesis `freeRanges` is empty and SEVEN rules are silent BY CONSTRUCTION —
22
+ * free-section-size · free-section-note · block-note · block-ungraded · block-verdict ·
23
+ * appendix-ref · appendix-ratio. This is exactly the case of "zero findings — a suspect, not
24
+ * a clean corpus".
25
+ *
26
+ * ── CAPTIONS AND FOOTNOTES: a boundary CLOSED on 2026-09-06 ─────────────────
27
+ * Until that day, prose inside `\caption{}` and `\footnote{}` was invisible to ALL 27 prose
28
+ * rules on `.tex` — 11 blocks / 384 words, of which 253 in one paper agree with `texcount`
29
+ * ("Words outside text: 253"). There were TWO locks, and lifting one did not help: a caption
30
+ * was blanked by `OPAQUE` as a macro, and inside `\begin{table}` / `\begin{figure}` it was
31
+ * blanked again by the whole environment under `CODEISH`. Now `caption` / `footnote` are
32
+ * pulled out into `FLOAT_PROSE`, and the `CODEISH` branch blanks the float MINUS the
33
+ * arguments of those two macros.
34
+ * 🔴 Removing `table` / `figure` from `CODEISH` was NOT an option — rules would then receive
35
+ * `&`, `\\`, the column specification and tikz nodes, i.e. findings on markup. That is pinned
36
+ * by the harness and by a "naive fix" mutation which that part kills.
37
+ * Measured after the fix: nine new findings across three papers, all of them captions and
38
+ * footnotes, reviewed one by one; zero false positives on markup.
39
+ *
40
+ * ── KNOWN BOUNDARIES. Written down rather than silently accepted ────────────
41
+ * 1. The projection is a SECOND TRUTH about the document: the text a rule sees is not the
42
+ * file (only lengths and offsets are equal). Any quotation in a message prints the
43
+ * projection, not the source — e.g. `§«Discussion and threats to validity »` with a
44
+ * trailing space left by a blanked `}`.
45
+ * 2. ESCAPED CHARACTERS ARE LOST. `\%`, `\$`, `\&` are single-character macros and get
46
+ * blanked together with their name: `95\% of it, costing \$5 and A\&B` →
47
+ * `95 of it, costing 5 and A B`. This matters: a word splitter counts `%` as part of
48
+ * a word, and a plain-language rule counts NUMBERS — so the percent sign detaches from
49
+ * its number. Not fixed: three exceptions would have to be written and pinned.
50
+ * 3. THRESHOLDS WERE CALIBRATED ON MARKDOWN. 60 / 350 / 700 words were taken from a markdown
51
+ * paper. The projection counts differently: the same place gave 1239 words before macro
52
+ * blanking and 1208 after — a 2.5% discrepancy. The discrepancy is ACCEPTED deliberately;
53
+ * the thresholds were not recalibrated.
54
+ * 4. A MULTI-FILE PAPER IS INVISIBLE. `\input` / `\include` are in OPAQUE — their contents
55
+ * are not read. The first `\input{sections/method}` yields a silent zero over all the
56
+ * prose it pulls in.
57
+ * 5. `\appendix` IS NOT IMPLEMENTED and is INEXPRESSIBLE in a length-preserving projection:
58
+ * `\appendix` is 9 characters, `## Appendix` is 11, the synthesis does not fit. On the
59
+ * first paper with a real `\appendix`, `appendix-ref` / `appendix-ratio` yield a silent
60
+ * zero.
61
+ * 6. ANYTHING LaTeX GENERATES does not exist for a rule: section numbers, appendix letters,
62
+ * reference numbers, real page numbers.
63
+ * 7. AUTHOR MACROS ARE NOT EXPANDED (`\newcommand{\tool}{…}` — the name is blanked, the body
64
+ * is not substituted). Anonymisation is normally done with exactly those, so coinage and
65
+ * jargon rules see less on an anonymised version than on a camera-ready.
66
+ * 8. `% eslint {"rules":…}` inside `.tex` does NOT work: `applyInlineConfig` returns empty.
67
+ * `% eslint-disable-next-line <rule> -- reason` DOES work.
68
+ * 9. `\section*{…}` is untested and does not occur in the corpus (measured: 0 occurrences).
69
+ * When it appears, `plain()` will glue all arguments together and the heading may come out
70
+ * as `*X`.
71
+ * 10. The assertion "projection length == source length" holds on every harness input, but
72
+ * `\r\n`, emoji inside `.tex`, `\verb|…|` and nested `filecontents` are untested.
73
+ * 11. INSIDE A CAPTION no bold lead-in is synthesised: `\emph{…}` in the first column is an
74
+ * editor's line break, not the heading of an appendix block, and a block-note rule would
75
+ * take it for an ungraded block. Held by the `inCaption` counter. The flip side: a real
76
+ * `\textbf{…}` at the start of a caption line does not exist for strong-lead detection.
77
+ * Measured 2026-09-06 across three papers: zero such captions in the free half, so the
78
+ * cost today is zero.
79
+ * 12. THE CODE/PROSE BOUNDARY INSIDE A FLOAT IS DRAWN BY LINES, not by offsets, and that is
80
+ * forced: a document splitter drops from its clean text EVERY line a `code` node touches,
81
+ * so one node spanning the whole float would carry the caption back into darkness right
82
+ * after the projection had saved it. Hence `code` nodes are emitted as runs of lines that
83
+ * step around the caption lines. The cost: a line where the caption sits TOGETHER with
84
+ * markup (`\begin{table}[t]\caption{…}` on one line — one real paper writes a table that
85
+ * way) is not dropped from the prose stream at all. No harm done, the markup on it is
86
+ * blanked with spaces, but for a prose rule it is an ordinary line of text rather than a
87
+ * gap. Measured: the set of findings agreed; not pinned by a separate assertion.
88
+ * 13. `\citep{}` / `\citet{}` LEAK THEIR KEY, and `\cite{}` does not. Found 2026-09-11 while
89
+ * extracting this language, by an assertion that expected the opposite. Both names are
90
+ * listed in OPAQUE, but OPAQUE only blanks a macro NAME plus the ARGUMENTS unified-latex
91
+ * attached to it — and unified-latex has no signature for the natbib pair, so `{someone2020}`
92
+ * comes back as a separate group node and survives as prose. Measured, one input per macro:
93
+ * `\cite{keyABC}` → ` ` (fully blanked)
94
+ * `\citep{keyABC}` → ` keyABC}` (key and closing brace survive)
95
+ * `\citet{keyABC}` → ` keyABC}`
96
+ * Consequence for a paper using natbib: every citation key is a word in the prose stream.
97
+ * NOT fixed here — the fix is a signature declaration for unified-latex, which is a change
98
+ * of behaviour rather than a move, and it wants its own measurement on a real corpus.
99
+ */
100
+ import { getParser } from "@unified-latex/unified-latex-util-parse";
101
+ import {
102
+ TextSourceCodeBase,
103
+ VisitNodeStep,
104
+ ConfigCommentParser,
105
+ Directive,
106
+ } from "@eslint/plugin-kit";
107
+
108
+ const HEADING = { part: 1, section: 2, subsection: 3, subsubsection: 4 };
109
+ const CODEISH =
110
+ /^(verbatim|lstlisting|minted|figure|figure\*|table|table\*|tabular|algorithm|algorithmic|thebibliography|tikzpicture|filecontents\*?)$/;
111
+ // Prose that a CODEISH environment has NO right to swallow together with the markup.
112
+ // A figure/table caption and a footnote are English sentences: the author writes them and the
113
+ // reviewer reads them. Everything else inside a float (`&`, `\\`, the column specification,
114
+ // `\includegraphics`) is not prose. Measured on the source corpus 2026-08-28: 11 blocks /
115
+ // 384 words were invisible to ALL 27 prose rules on `.tex`, and 253 of those words in one
116
+ // paper agree with `texcount`'s "Words outside text".
117
+ const FLOAT_PROSE = /^(caption|footnote)$/;
118
+ // Macros whose NAME and ARGUMENT are both non-prose.
119
+ const OPAQUE =
120
+ /^(label|ref|autoref|eqref|cite|citep|citet|input|include|usepackage|documentclass|bibliography|bibliographystyle|acmISBN|acmDOI|acmConference|setcopyright|ccsdesc|keywords|orcid|affiliation|email|author|copyrightyear|acmYear|acmBooktitle|acmPrice|settopmatter|definecolor|includegraphics|newcommand|renewcommand|def|let|makeatletter|makeatother|hypersetup|pagestyle|thispagestyle|vspace|hspace)$/;
121
+
122
+ const P = (n) => n?.position;
123
+
124
+ function plain(nodes) {
125
+ let out = "";
126
+ for (const n of nodes || []) {
127
+ if (n.type === "string") out += n.content;
128
+ else if (n.type === "whitespace") out += " ";
129
+ else if (n.type === "group" || n.type === "argument")
130
+ out += plain(n.content);
131
+ else if (n.type === "macro" && n.args)
132
+ out += plain(n.args.flatMap((a) => a.content || []));
133
+ }
134
+ return out.trim();
135
+ }
136
+ const argEnd = (node) => {
137
+ let end = P(node).end;
138
+ for (const a of node.args || [])
139
+ for (const c of a.content || [])
140
+ if (P(c) && P(c).end.offset > end.offset) end = P(c).end;
141
+ return end;
142
+ };
143
+
144
+ export function texToMdast(src) {
145
+ const ast = getParser().parse(src);
146
+ const chars = src.split("");
147
+ const blank = (from, to) => {
148
+ for (let i = from; i < to && i < chars.length; i++)
149
+ if (chars[i] !== "\n") chars[i] = " ";
150
+ };
151
+ const lineStarts = [0];
152
+ for (let i = 0; i < src.length; i++)
153
+ if (src[i] === "\n") lineStarts.push(i + 1);
154
+ const loc = (off) => {
155
+ let lo = 0,
156
+ hi = lineStarts.length - 1;
157
+ while (lo < hi) {
158
+ const m = (lo + hi + 1) >> 1;
159
+ if (lineStarts[m] <= off) lo = m;
160
+ else hi = m - 1;
161
+ }
162
+ return { line: lo + 1, column: off - lineStarts[lo] + 1, offset: off };
163
+ };
164
+ const children = [];
165
+ const pendingStrong = [];
166
+ // Nesting depth inside a `\caption{}` / `\footnote{}` argument. Exactly one place needs it:
167
+ // bold lead-in synthesis. An `\emph{…}` in the first column INSIDE a caption is an editor's
168
+ // line break, not the heading of an appendix block, and a block-note rule would take it for
169
+ // an ungraded block. Measured across three papers: today there are no such captions in the
170
+ // free half (0 findings), so this guards against tomorrow rather than repairing today.
171
+ let inCaption = 0;
172
+ /** A synthetic heading: the ATX marker is written at the START of the span, the tail is blanked. */
173
+ const synthHeading = (from, to, title, depth = 2) => {
174
+ const label = "#".repeat(depth) + " " + title;
175
+ if (label.length > to - from) return null; // does not fit — do not invent one silently
176
+ blank(from, to);
177
+ for (let i = 0; i < label.length; i++) chars[from + i] = label[i];
178
+ const node = {
179
+ type: "heading",
180
+ depth,
181
+ title,
182
+ children: [
183
+ {
184
+ type: "text",
185
+ value: title,
186
+ position: {
187
+ start: loc(from + depth + 1),
188
+ end: loc(from + label.length),
189
+ },
190
+ },
191
+ ],
192
+ position: { start: loc(from), end: loc(from + label.length) },
193
+ };
194
+ children.push(node);
195
+ return node;
196
+ };
197
+
198
+ const beginDoc = src.indexOf("\\begin{document}");
199
+ const preEnd = beginDoc > 0 ? beginDoc + "\\begin{document}".length : 0;
200
+ if (preEnd) {
201
+ blank(0, preEnd);
202
+ children.push({
203
+ type: "code",
204
+ lang: "latex",
205
+ value: "",
206
+ position: { start: loc(0), end: loc(preEnd) },
207
+ });
208
+ }
209
+
210
+ (function walk(node) {
211
+ if (Array.isArray(node)) return node.forEach((n) => walk(n));
212
+ if (!node || typeof node !== "object") return;
213
+ const pos = P(node);
214
+ // The preamble is already blanked in full; nothing inside it (including a
215
+ // `\renewcommand{\bibliography}`) is a node of the document. Without this cut-off, one real
216
+ // paper got a FALSE `## References` on line 178 — i.e. its bibliography "began" before the
217
+ // introduction, and the whole paper fell into the free half.
218
+ if (pos && pos.start.offset < preEnd) {
219
+ for (const k of ["content", "args"]) if (node[k]) walk(node[k]);
220
+ return;
221
+ }
222
+
223
+ if (node.type === "environment") {
224
+ const env = typeof node.env === "string" ? node.env : plain(node.env);
225
+ // 🔴 MEASURED 2026-08-26: none of the three real `.tex` papers has a `Limitations` /
226
+ // `References` / `Appendix` heading. In LaTeX the free half is declared by an
227
+ // ENVIRONMENT and a macro rather than by a heading, so without synthesis `freeRanges`
228
+ // is empty and seven rules are silent by construction.
229
+ if (env === "abstract" && pos) {
230
+ synthHeading(
231
+ pos.start.offset,
232
+ pos.start.offset + "\\begin{abstract}".length,
233
+ "Abstract",
234
+ );
235
+ for (const k of ["content", "args"]) if (node[k]) walk(node[k]);
236
+ return;
237
+ }
238
+ if (env === "thebibliography" && pos) {
239
+ const h = synthHeading(
240
+ pos.start.offset,
241
+ pos.start.offset + "\\begin{thebibliography}".length,
242
+ "References",
243
+ );
244
+ blank(
245
+ pos.start.offset + (h ? "## References".length : 0),
246
+ pos.end.offset,
247
+ );
248
+ children.push({
249
+ type: "code",
250
+ lang: env,
251
+ value: "",
252
+ position: { start: loc(pos.start.offset + 14), end: pos.end },
253
+ });
254
+ return;
255
+ }
256
+ if (CODEISH.test(env) && pos) {
257
+ // 🔴 THERE WERE TWO LOCKS, AND LIFTING ONE DID NOT HELP (proved by mutation): a
258
+ // caption was blanked by `OPAQUE` as a macro, and inside `table` / `figure` it was
259
+ // blanked AGAIN by the whole environment. So what is blanked here is not the float's
260
+ // borders but the float MINUS the arguments of `\caption{}` / `\footnote{}`.
261
+ //
262
+ // Removing `table` / `figure` from `CODEISH` was NOT an option: prose rules would then
263
+ // receive `&`, `\\` and the column specification, i.e. findings on markup — and for a
264
+ // rule somebody turns up to `error`, a false positive is worse than a miss.
265
+ const keep = [];
266
+ (function findProse(n) {
267
+ if (Array.isArray(n)) return n.forEach(findProse);
268
+ if (!n || typeof n !== "object") return;
269
+ if (n.type === "macro" && FLOAT_PROSE.test(n.content)) {
270
+ const cs = (n.args || [])
271
+ .flatMap((a) => a.content || [])
272
+ .filter((c) => P(c));
273
+ // Keep exactly the CONTENT of the argument: `\caption` itself and the braces
274
+ // stay in the blanked part, so the macro name never becomes prose.
275
+ if (cs.length)
276
+ keep.push({
277
+ node: n,
278
+ s: P(cs[0]).start.offset,
279
+ e: P(cs[cs.length - 1]).end.offset,
280
+ });
281
+ return; // do not look for a caption inside a caption
282
+ }
283
+ for (const k of ["content", "args"]) if (n[k]) findProse(n[k]);
284
+ })(node.content);
285
+ keep.sort((a, b) => a.s - b.s);
286
+ let cur = pos.start.offset;
287
+ for (const k of keep) {
288
+ blank(cur, k.s);
289
+ cur = Math.max(cur, k.e);
290
+ }
291
+ blank(cur, pos.end.offset);
292
+ // 🔴 The code node is emitted LINE BY LINE, stepping around the caption lines. A
293
+ // document splitter drops from its clean text every line a `code` node TOUCHES, so one
294
+ // node spanning the whole float would carry the caption back into darkness right after
295
+ // the projection had saved it.
296
+ const proseLines = new Set();
297
+ for (const k of keep)
298
+ for (
299
+ let l = loc(k.s).line;
300
+ l <= loc(Math.max(k.s, k.e - 1)).line;
301
+ l++
302
+ )
303
+ proseLines.add(l);
304
+ const L0 = loc(pos.start.offset).line;
305
+ const L1 = loc(Math.max(pos.start.offset, pos.end.offset - 1)).line;
306
+ for (let l = L0; l <= L1; l++) {
307
+ if (proseLines.has(l)) continue;
308
+ let r = l;
309
+ while (r + 1 <= L1 && !proseLines.has(r + 1)) r++;
310
+ const s0 = lineStarts[l - 1];
311
+ const e0 = r < lineStarts.length ? lineStarts[r] - 1 : src.length;
312
+ children.push({
313
+ type: "code",
314
+ lang: env,
315
+ value: "",
316
+ position: { start: loc(s0), end: loc(e0) },
317
+ });
318
+ l = r;
319
+ }
320
+ // We walk into the caption itself the ordinary way: `\label`, `\cite` and math inside
321
+ // it are blanked by the same branches as in the body, not by a separate parser.
322
+ for (const k of keep) walk(k.node);
323
+ return;
324
+ }
325
+ // other environments: blank only the `\begin{x}` / `\end{x}` fences
326
+ if (pos) {
327
+ blank(pos.start.offset, pos.start.offset + env.length + 8);
328
+ blank(Math.max(0, pos.end.offset - env.length - 6), pos.end.offset);
329
+ }
330
+ }
331
+ if (
332
+ (node.type === "inlinemath" ||
333
+ node.type === "displaymath" ||
334
+ node.type === "verbatim") &&
335
+ pos
336
+ ) {
337
+ blank(pos.start.offset, pos.end.offset);
338
+ return;
339
+ }
340
+ if (node.type === "comment" && pos) {
341
+ // 🔴 unified-latex includes the TRAILING newline in a comment; mdast does not. A rule
342
+ // that binds an author note to the heading below it requires a newline BETWEEN the end of
343
+ // the comment and the heading; with the `\n` swallowed the gap is empty and the note is
344
+ // never found.
345
+ let ce = pos.end.offset;
346
+ while (
347
+ ce > pos.start.offset &&
348
+ (src[ce - 1] === "\n" || src[ce - 1] === "\r")
349
+ )
350
+ ce--;
351
+ // 🔴 AND SYMMETRICALLY ON THE LEFT (found by a run on 2026-08-27, not by reading). A
352
+ // comment that follows a line of text directly, rather than a blank line, comes back from
353
+ // unified-latex with a position starting at the PREVIOUS newline instead of at `%`.
354
+ // Consumers recognise a comment by its first character (`<!--` in markdown, `%` in
355
+ // LaTeX), so such a node was not a comment to them: notes above bold appendix lead-ins
356
+ // never bound, and the block-note / block-verdict rules could NEVER fire. Measured:
357
+ // 2 out of 2 blocks were reported as "no note AT ALL" while the notes were right there.
358
+ let cs = pos.start.offset;
359
+ while (cs < ce && src[cs] !== "%") cs++;
360
+ children.push({
361
+ type: "html",
362
+ value: node.content,
363
+ position: { start: loc(cs), end: loc(ce) },
364
+ });
365
+ return; // blanking is the document splitter's job — it also takes the note body
366
+ }
367
+ if (node.type === "macro" && pos) {
368
+ const depth = HEADING[node.content];
369
+ if (depth) {
370
+ const title = plain(node.args?.flatMap((a) => a.content || []));
371
+ const end = argEnd(node);
372
+ // project to ATX: `##` where `\se` was, the rest spaces; the closing `}` too
373
+ blank(pos.start.offset, end.offset + 1);
374
+ for (let i = 0; i < depth; i++) chars[pos.start.offset + i] = "#";
375
+ const tStart = end.offset - title.length;
376
+ for (let i = 0; i < title.length; i++) chars[tStart + i] = title[i];
377
+ const tPos = { start: loc(tStart), end: loc(end.offset) };
378
+ children.push({
379
+ type: "heading",
380
+ depth,
381
+ title,
382
+ children: [{ type: "text", value: title, position: tPos }],
383
+ position: {
384
+ start: pos.start,
385
+ end: { ...end, offset: end.offset + 1, column: end.column + 1 },
386
+ },
387
+ });
388
+ return;
389
+ }
390
+ if (node.content === "bibliography" && pos) {
391
+ synthHeading(pos.start.offset, argEnd(node).offset + 1, "References");
392
+ return;
393
+ }
394
+ if (OPAQUE.test(node.content)) {
395
+ blank(pos.start.offset, argEnd(node).offset + 1);
396
+ return;
397
+ }
398
+ // `\texttt{X}` → `` `X` ``: exactly the same character count, so offsets do not move.
399
+ // This is the only markdown markup the projection can reproduce without shifting: `**`
400
+ // needs TWO characters after the content, and there is only `}`.
401
+ if (node.content === "texttt" || node.content === "lstinline") {
402
+ const e = argEnd(node);
403
+ const cs = (node.args || [])
404
+ .flatMap((a) => a.content || [])
405
+ .filter((c) => P(c));
406
+ if (cs.length) {
407
+ const s0 = P(cs[0]).start.offset;
408
+ blank(pos.start.offset, s0 - 1);
409
+ chars[s0 - 1] = "`";
410
+ chars[e.offset] = "`";
411
+ return;
412
+ }
413
+ }
414
+ if (
415
+ (node.content === "textbf" || node.content === "emph") &&
416
+ pos.start.column === 1 &&
417
+ !inCaption
418
+ ) {
419
+ const end = argEnd(node);
420
+ const sv = plain(node.args?.flatMap((a) => a.content || []));
421
+ children.push({
422
+ type: "strong",
423
+ children: [
424
+ {
425
+ type: "text",
426
+ value: sv,
427
+ position: {
428
+ start: loc(end.offset - sv.length),
429
+ end: loc(end.offset),
430
+ },
431
+ },
432
+ ],
433
+ position: {
434
+ start: pos.start,
435
+ end: { ...end, offset: end.offset + 1, column: end.column + 1 },
436
+ },
437
+ });
438
+ // 🔴 The `**` are written IN PLACE OF `\\te`, not before the content: a block-note rule
439
+ // locates a lead-in by the OFFSET of the line's first non-space character. Leave spaces
440
+ // at the start of the line and the note above the block never binds, so block-* go
441
+ // silent.
442
+ pendingStrong.push(pos.start.offset);
443
+ }
444
+ // an ordinary macro: blank the NAME (`\emph`) and the argument's BRACES; the content is prose
445
+ blank(pos.start.offset, pos.end.offset);
446
+ while (pendingStrong.length) {
447
+ const o = pendingStrong.pop();
448
+ chars[o] = "*";
449
+ chars[o + 1] = "*";
450
+ }
451
+ for (const a of node.args || []) {
452
+ const cs = (a.content || []).filter((c) => P(c));
453
+ if (!cs.length) continue;
454
+ const s0 = P(cs[0]).start.offset,
455
+ e0 = P(cs[cs.length - 1]).end.offset;
456
+ if (a.openMark) blank(s0 - a.openMark.length, s0);
457
+ if (a.closeMark) blank(e0, e0 + a.closeMark.length);
458
+ }
459
+ if (FLOAT_PROSE.test(node.content)) {
460
+ inCaption++;
461
+ for (const k of ["content", "args"]) if (node[k]) walk(node[k]);
462
+ inCaption--;
463
+ return;
464
+ }
465
+ }
466
+ for (const k of ["content", "args"]) if (node[k]) walk(node[k]);
467
+ })(ast.content);
468
+
469
+ children.sort((a, b) => a.position.start.offset - b.position.start.offset);
470
+ return {
471
+ root: {
472
+ type: "root",
473
+ children,
474
+ position: { start: loc(0), end: loc(src.length) },
475
+ },
476
+ text: chars.join(""),
477
+ };
478
+ }
479
+
480
+ const commentParser = new ConfigCommentParser();
481
+ const directiveStart =
482
+ /^\s*eslint(?:-enable|-disable(?:(?:-next)?-line)?)?(?:\s|$)/u;
483
+
484
+ class TexSourceCode extends TextSourceCodeBase {
485
+ #steps;
486
+ #parents = new WeakMap();
487
+ #comments = [];
488
+ #inline;
489
+ ast;
490
+ /**
491
+ * `raw` is the UNPROJECTED `.tex`, and it is here because the projection is lossy by design:
492
+ * it blanks macros, so a rule that must count `\S\ref` or `Fig.~\ref` sees spaces. Those
493
+ * rules are about MARKUP, not prose, and the projection exists precisely to hide markup.
494
+ *
495
+ * 🔴 Positions stay interchangeable, which is what makes this safe rather than a second
496
+ * coordinate system: `blank()` overwrites characters with spaces and never changes length,
497
+ * so an offset computed on `raw` addresses the same byte of `text`. A rule may therefore
498
+ * scan `raw` and report with the offset it found.
499
+ */
500
+ raw;
501
+ constructor({ text, ast, raw }) {
502
+ super({ ast, text, lineEndingPattern: /\r?\n/u });
503
+ this.ast = ast;
504
+ this.raw = raw ?? text;
505
+ this.traverse();
506
+ }
507
+ getParent(node) {
508
+ return this.#parents.get(node);
509
+ }
510
+ getInlineConfigNodes() {
511
+ if (!this.#inline)
512
+ this.#inline = this.#comments
513
+ .filter((c) => directiveStart.test(c.value))
514
+ .map((c) => ({ value: c.value.trim(), position: c.position }));
515
+ return this.#inline;
516
+ }
517
+ getDisableDirectives() {
518
+ const directives = [];
519
+ for (const comment of this.getInlineConfigNodes()) {
520
+ const { label, value, justification } = commentParser.parseDirective(
521
+ comment.value,
522
+ );
523
+ if (
524
+ [
525
+ "eslint-disable",
526
+ "eslint-enable",
527
+ "eslint-disable-next-line",
528
+ "eslint-disable-line",
529
+ ].includes(label)
530
+ )
531
+ directives.push(
532
+ new Directive({
533
+ type: label.slice(7),
534
+ node: comment,
535
+ value,
536
+ justification,
537
+ }),
538
+ );
539
+ }
540
+ return { problems: [], directives };
541
+ }
542
+ applyInlineConfig() {
543
+ return { configs: [], problems: [] };
544
+ }
545
+ traverse() {
546
+ if (this.#steps) return this.#steps.values();
547
+ const steps = (this.#steps = []);
548
+ const visit = (node, parent) => {
549
+ this.#parents.set(node, parent);
550
+ steps.push(
551
+ new VisitNodeStep({ target: node, phase: 1, args: [node, parent] }),
552
+ );
553
+ if (node.type === "html")
554
+ this.#comments.push({ value: node.value, position: node.position });
555
+ for (const c of node.children || []) visit(c, node);
556
+ steps.push(
557
+ new VisitNodeStep({ target: node, phase: 2, args: [node, parent] }),
558
+ );
559
+ };
560
+ visit(this.ast);
561
+ return steps.values();
562
+ }
563
+ }
564
+
565
+ export const texLanguage = {
566
+ fileType: "text",
567
+ lineStart: 1,
568
+ columnStart: 1,
569
+ nodeTypeKey: "type",
570
+ defaultLanguageOptions: {},
571
+ validateLanguageOptions() {},
572
+ parse(file) {
573
+ try {
574
+ const raw = String(file.body);
575
+ const { root, text } = texToMdast(raw);
576
+ return { ok: true, ast: root, projected: text, raw };
577
+ } catch (ex) {
578
+ return { ok: false, errors: [ex] };
579
+ }
580
+ },
581
+ createSourceCode(file, parseResult) {
582
+ // 🔴 LOAD-BEARING: the SourceCode receives the PROJECTION, not the source. Lengths and
583
+ // offsets are equal, so the position of a finding points into the real `.tex` while the
584
+ // rule sees prose.
585
+ return new TexSourceCode({
586
+ text: parseResult.projected,
587
+ ast: parseResult.ast,
588
+ raw: parseResult.raw,
589
+ });
590
+ },
591
+ };