paperlint 3.1.3 → 4.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 (527) hide show
  1. package/.github/workflows/ci.yml +12 -7
  2. package/CLAUDE.md +5 -5
  3. package/CONTRIBUTING.md +34 -5
  4. package/dist/adapters/banal/failure.d.ts +1 -7
  5. package/dist/adapters/banal/failure.d.ts.map +1 -1
  6. package/dist/adapters/banal/failure.js +5 -7
  7. package/dist/adapters/banal/failure.js.map +1 -1
  8. package/dist/adapters/banal/index.d.ts.map +1 -1
  9. package/dist/adapters/banal/index.js +4 -3
  10. package/dist/adapters/banal/index.js.map +1 -1
  11. package/dist/adapters/banal/install.d.ts.map +1 -1
  12. package/dist/adapters/banal/install.js +4 -3
  13. package/dist/adapters/banal/install.js.map +1 -1
  14. package/dist/adapters/banal/invocation.d.ts +10 -5
  15. package/dist/adapters/banal/invocation.d.ts.map +1 -1
  16. package/dist/adapters/banal/invocation.js +13 -1
  17. package/dist/adapters/banal/invocation.js.map +1 -1
  18. package/dist/adapters/banal/locate.d.ts +12 -9
  19. package/dist/adapters/banal/locate.d.ts.map +1 -1
  20. package/dist/adapters/banal/locate.js +19 -12
  21. package/dist/adapters/banal/locate.js.map +1 -1
  22. package/dist/adapters/banal/run.d.ts +10 -1
  23. package/dist/adapters/banal/run.d.ts.map +1 -1
  24. package/dist/adapters/banal/run.js +8 -4
  25. package/dist/adapters/banal/run.js.map +1 -1
  26. package/dist/adapters/banal/settings.d.ts +1 -1
  27. package/dist/adapters/banal/settings.d.ts.map +1 -1
  28. package/dist/adapters/banal/settings.js +2 -1
  29. package/dist/adapters/banal/settings.js.map +1 -1
  30. package/dist/adapters/memory/index.d.ts.map +1 -1
  31. package/dist/adapters/memory/index.js +8 -2
  32. package/dist/adapters/memory/index.js.map +1 -1
  33. package/dist/adapters/node/files.io.d.ts.map +1 -1
  34. package/dist/adapters/node/files.io.js +2 -1
  35. package/dist/adapters/node/files.io.js.map +1 -1
  36. package/dist/adapters/node/process.io.d.ts +5 -7
  37. package/dist/adapters/node/process.io.d.ts.map +1 -1
  38. package/dist/adapters/node/process.io.js +4 -3
  39. package/dist/adapters/node/process.io.js.map +1 -1
  40. package/dist/adapters/node/workspace.io.d.ts.map +1 -1
  41. package/dist/adapters/node/workspace.io.js +4 -2
  42. package/dist/adapters/node/workspace.io.js.map +1 -1
  43. package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
  44. package/dist/adapters/pdfjs/fill.js +13 -7
  45. package/dist/adapters/pdfjs/fill.js.map +1 -1
  46. package/dist/adapters/references/index.d.ts.map +1 -1
  47. package/dist/adapters/references/index.js +8 -7
  48. package/dist/adapters/references/index.js.map +1 -1
  49. package/dist/adapters/references/pool.d.ts.map +1 -1
  50. package/dist/adapters/references/pool.js +4 -5
  51. package/dist/adapters/references/pool.js.map +1 -1
  52. package/dist/adapters/references/reach.io.d.ts.map +1 -1
  53. package/dist/adapters/references/reach.io.js +2 -1
  54. package/dist/adapters/references/reach.io.js.map +1 -1
  55. package/dist/build-engine.d.ts +1 -1
  56. package/dist/build-engine.d.ts.map +1 -1
  57. package/dist/build-engine.js +10 -3
  58. package/dist/build-engine.js.map +1 -1
  59. package/dist/build.d.ts +2 -3
  60. package/dist/build.d.ts.map +1 -1
  61. package/dist/build.js +19 -17
  62. package/dist/build.js.map +1 -1
  63. package/dist/caller-path.d.ts +3 -0
  64. package/dist/caller-path.d.ts.map +1 -0
  65. package/dist/caller-path.js +13 -0
  66. package/dist/caller-path.js.map +1 -0
  67. package/dist/cli.d.ts +10 -17
  68. package/dist/cli.d.ts.map +1 -1
  69. package/dist/cli.js +93 -46
  70. package/dist/cli.js.map +1 -1
  71. package/dist/doctor.d.ts +12 -3
  72. package/dist/doctor.d.ts.map +1 -1
  73. package/dist/doctor.js +5 -3
  74. package/dist/doctor.js.map +1 -1
  75. package/dist/domain/lookup-cache.d.ts.map +1 -1
  76. package/dist/domain/lookup-cache.js +10 -8
  77. package/dist/domain/lookup-cache.js.map +1 -1
  78. package/dist/domain/paths.d.ts +8 -2
  79. package/dist/domain/paths.d.ts.map +1 -1
  80. package/dist/domain/paths.js +12 -1
  81. package/dist/domain/paths.js.map +1 -1
  82. package/dist/domain/ready.d.ts +24 -0
  83. package/dist/domain/ready.d.ts.map +1 -0
  84. package/dist/domain/ready.js +11 -0
  85. package/dist/domain/ready.js.map +1 -0
  86. package/dist/domain/record.d.ts +8 -0
  87. package/dist/domain/record.d.ts.map +1 -0
  88. package/dist/domain/record.js +8 -0
  89. package/dist/domain/record.js.map +1 -0
  90. package/dist/domain/sha256.d.ts.map +1 -1
  91. package/dist/domain/sha256.js +4 -2
  92. package/dist/domain/sha256.js.map +1 -1
  93. package/dist/domain/text.d.ts +12 -0
  94. package/dist/domain/text.d.ts.map +1 -1
  95. package/dist/domain/text.js +17 -0
  96. package/dist/domain/text.js.map +1 -1
  97. package/dist/engine.d.ts +23 -22
  98. package/dist/engine.d.ts.map +1 -1
  99. package/dist/engine.js +4 -3
  100. package/dist/engine.js.map +1 -1
  101. package/dist/eslint-rules/bib-reachable-entry.d.ts +22 -0
  102. package/dist/eslint-rules/bib-reachable-entry.d.ts.map +1 -0
  103. package/dist/eslint-rules/bib-reachable-entry.js +75 -0
  104. package/dist/eslint-rules/bib-reachable-entry.js.map +1 -0
  105. package/dist/eslint-rules/install-path-literals.d.ts +80 -0
  106. package/dist/eslint-rules/install-path-literals.d.ts.map +1 -0
  107. package/dist/eslint-rules/install-path-literals.js +105 -0
  108. package/dist/eslint-rules/install-path-literals.js.map +1 -0
  109. package/dist/eslint-rules/latex-language.d.ts +136 -0
  110. package/dist/eslint-rules/latex-language.d.ts.map +1 -0
  111. package/dist/eslint-rules/latex-language.js +639 -0
  112. package/dist/eslint-rules/latex-language.js.map +1 -0
  113. package/dist/eslint-rules/paper-research-question.d.ts +31 -0
  114. package/dist/eslint-rules/paper-research-question.d.ts.map +1 -0
  115. package/dist/eslint-rules/paper-research-question.js +188 -0
  116. package/dist/eslint-rules/paper-research-question.js.map +1 -0
  117. package/dist/eslint-rules/paper-stages.d.ts +83 -0
  118. package/dist/eslint-rules/paper-stages.d.ts.map +1 -0
  119. package/dist/eslint-rules/paper-stages.js +358 -0
  120. package/dist/eslint-rules/paper-stages.js.map +1 -0
  121. package/dist/eslint-rules/paper-typography.d.ts +74 -0
  122. package/dist/eslint-rules/paper-typography.d.ts.map +1 -0
  123. package/dist/eslint-rules/paper-typography.js +404 -0
  124. package/dist/eslint-rules/paper-typography.js.map +1 -0
  125. package/dist/eslint-rules/papers.d.ts +42 -0
  126. package/dist/eslint-rules/papers.d.ts.map +1 -0
  127. package/dist/eslint-rules/papers.js +145 -0
  128. package/dist/eslint-rules/papers.js.map +1 -0
  129. package/dist/eslint-rules/pdf-last-page-balance.d.ts +72 -0
  130. package/dist/eslint-rules/pdf-last-page-balance.d.ts.map +1 -0
  131. package/dist/eslint-rules/pdf-last-page-balance.js +237 -0
  132. package/dist/eslint-rules/pdf-last-page-balance.js.map +1 -0
  133. package/dist/eslint-rules/review-frontmatter.d.ts +57 -0
  134. package/dist/eslint-rules/review-frontmatter.d.ts.map +1 -0
  135. package/dist/eslint-rules/review-frontmatter.js +126 -0
  136. package/dist/eslint-rules/review-frontmatter.js.map +1 -0
  137. package/dist/eslint-rules/review-frontmatter.schema.json +36 -0
  138. package/dist/eslint-rules/rule-context.d.ts +55 -0
  139. package/dist/eslint-rules/rule-context.d.ts.map +1 -0
  140. package/dist/eslint-rules/rule-context.js +9 -0
  141. package/dist/eslint-rules/rule-context.js.map +1 -0
  142. package/dist/eslint-rules/sibling-frontmatter.d.ts +25 -0
  143. package/dist/eslint-rules/sibling-frontmatter.d.ts.map +1 -0
  144. package/dist/eslint-rules/sibling-frontmatter.js +16 -0
  145. package/dist/eslint-rules/sibling-frontmatter.js.map +1 -0
  146. package/dist/eslint-rules/sibling-frontmatter.schema.json +14 -0
  147. package/dist/eslint-rules/temp-root-realpath.d.ts +71 -0
  148. package/dist/eslint-rules/temp-root-realpath.d.ts.map +1 -0
  149. package/dist/eslint-rules/temp-root-realpath.js +139 -0
  150. package/dist/eslint-rules/temp-root-realpath.js.map +1 -0
  151. package/dist/eslint-rules/tex-build.d.ts +35 -0
  152. package/dist/eslint-rules/tex-build.d.ts.map +1 -0
  153. package/dist/eslint-rules/tex-build.js +313 -0
  154. package/dist/eslint-rules/tex-build.js.map +1 -0
  155. package/dist/facts-file.d.ts.map +1 -1
  156. package/dist/facts-file.js +31 -27
  157. package/dist/facts-file.js.map +1 -1
  158. package/dist/hooks-settings.d.ts +1 -1
  159. package/dist/hooks-settings.d.ts.map +1 -1
  160. package/dist/hooks-settings.js +31 -11
  161. package/dist/hooks-settings.js.map +1 -1
  162. package/dist/init.d.ts +8 -9
  163. package/dist/init.d.ts.map +1 -1
  164. package/dist/init.js +33 -15
  165. package/dist/init.js.map +1 -1
  166. package/dist/latex-loop.d.ts +2 -2
  167. package/dist/latex-loop.d.ts.map +1 -1
  168. package/dist/latex-loop.js +2 -2
  169. package/dist/latex-loop.js.map +1 -1
  170. package/dist/lib/agent-cli-version.d.ts +65 -0
  171. package/dist/lib/agent-cli-version.d.ts.map +1 -0
  172. package/dist/lib/agent-cli-version.js +77 -0
  173. package/dist/lib/agent-cli-version.js.map +1 -0
  174. package/dist/lib/check.d.ts +19 -0
  175. package/dist/lib/check.d.ts.map +1 -0
  176. package/dist/lib/check.js +52 -0
  177. package/dist/lib/check.js.map +1 -0
  178. package/dist/lib/markdown.d.ts +180 -0
  179. package/dist/lib/markdown.d.ts.map +1 -0
  180. package/dist/lib/markdown.js +302 -0
  181. package/dist/lib/markdown.js.map +1 -0
  182. package/dist/lib/paper-config.d.ts +84 -0
  183. package/dist/lib/paper-config.d.ts.map +1 -0
  184. package/dist/lib/paper-config.js +138 -0
  185. package/dist/lib/paper-config.js.map +1 -0
  186. package/dist/lib/skill-eval-fixture.d.ts +29 -0
  187. package/dist/lib/skill-eval-fixture.d.ts.map +1 -0
  188. package/dist/lib/skill-eval-fixture.js +62 -0
  189. package/dist/lib/skill-eval-fixture.js.map +1 -0
  190. package/dist/link-skills.d.ts.map +1 -1
  191. package/dist/link-skills.js +5 -4
  192. package/dist/link-skills.js.map +1 -1
  193. package/dist/new-paper.d.ts.map +1 -1
  194. package/dist/new-paper.js +7 -3
  195. package/dist/new-paper.js.map +1 -1
  196. package/dist/paper-files.d.ts +4 -4
  197. package/dist/paper-files.d.ts.map +1 -1
  198. package/dist/paper-files.js +6 -6
  199. package/dist/paper-files.js.map +1 -1
  200. package/dist/paper-settings.d.ts.map +1 -1
  201. package/dist/paper-settings.js +8 -7
  202. package/dist/paper-settings.js.map +1 -1
  203. package/dist/pdf-facts.d.ts +51 -1
  204. package/dist/pdf-facts.d.ts.map +1 -1
  205. package/dist/pdf-facts.js +25 -16
  206. package/dist/pdf-facts.js.map +1 -1
  207. package/dist/pdf-geometry.d.ts.map +1 -1
  208. package/dist/pdf-geometry.js +3 -4
  209. package/dist/pdf-geometry.js.map +1 -1
  210. package/dist/ports/tool-installer.d.ts +2 -14
  211. package/dist/ports/tool-installer.d.ts.map +1 -1
  212. package/dist/presets.d.ts.map +1 -1
  213. package/dist/presets.js +19 -6
  214. package/dist/presets.js.map +1 -1
  215. package/dist/references.d.ts.map +1 -1
  216. package/dist/references.js +15 -10
  217. package/dist/references.js.map +1 -1
  218. package/dist/rules-config.d.ts +26 -1
  219. package/dist/rules-config.d.ts.map +1 -1
  220. package/dist/rules-config.js +15 -12
  221. package/dist/rules-config.js.map +1 -1
  222. package/dist/structure.js +1 -1
  223. package/dist/structure.js.map +1 -1
  224. package/dist/tex-requirements.d.ts +4 -0
  225. package/dist/tex-requirements.d.ts.map +1 -1
  226. package/dist/tex-requirements.js +22 -5
  227. package/dist/tex-requirements.js.map +1 -1
  228. package/dist/toolchain.d.ts.map +1 -1
  229. package/dist/toolchain.js +21 -16
  230. package/dist/toolchain.js.map +1 -1
  231. package/dist/types.d.ts +5 -3
  232. package/dist/types.d.ts.map +1 -1
  233. package/dist/venue-rules.d.ts.map +1 -1
  234. package/dist/venue-rules.js +21 -8
  235. package/dist/venue-rules.js.map +1 -1
  236. package/docs/configuration.md +2 -2
  237. package/docs/e2e.md +7 -7
  238. package/docs/install.md +1 -1
  239. package/docs/prior-art/README.md +1 -1
  240. package/docs/prior-art/package-location.md +5 -5
  241. package/docs/prior-art/repro/README.md +1 -1
  242. package/docs/prior-art/test-tooling.md +2 -2
  243. package/docs/testing.md +11 -8
  244. package/docs/toolchain.md +1 -1
  245. package/eslint-rules/{bib-reachable-entry.mjs → bib-reachable-entry.ts} +5 -4
  246. package/eslint-rules/install-path-literals.harness.mjs +1 -1
  247. package/eslint-rules/{install-path-literals.mjs → install-path-literals.ts} +18 -10
  248. package/eslint-rules/latex-language.harness.mjs +2 -2
  249. package/eslint-rules/latex-language.test.mjs +7 -1
  250. package/eslint-rules/{latex-language.mjs → latex-language.ts} +297 -109
  251. package/eslint-rules/paper-research-question.harness.mjs +3 -3
  252. package/eslint-rules/{paper-research-question.mjs → paper-research-question.ts} +35 -16
  253. package/eslint-rules/paper-stages.harness.mjs +2 -2
  254. package/eslint-rules/paper-stages.test.mjs +8 -2
  255. package/eslint-rules/{paper-stages.mjs → paper-stages.ts} +76 -31
  256. package/eslint-rules/paper-typography.test.mjs +3 -3
  257. package/eslint-rules/{paper-typography.mjs → paper-typography.ts} +134 -56
  258. package/eslint-rules/papers.harness.mjs +2 -2
  259. package/eslint-rules/{papers.mjs → papers.ts} +7 -4
  260. package/eslint-rules/pdf-last-page-balance.harness.mjs +3 -3
  261. package/eslint-rules/{pdf-last-page-balance.mjs → pdf-last-page-balance.ts} +89 -32
  262. package/eslint-rules/review-frontmatter.test.mjs +2 -2
  263. package/eslint-rules/{review-frontmatter.mjs → review-frontmatter.ts} +38 -16
  264. package/eslint-rules/rule-context.ts +62 -0
  265. package/eslint-rules/{sibling-frontmatter.mjs → sibling-frontmatter.ts} +1 -1
  266. package/eslint-rules/temp-root-realpath.harness.mjs +1 -1
  267. package/eslint-rules/{temp-root-realpath.mjs → temp-root-realpath.ts} +20 -7
  268. package/eslint-rules/tex-build.harness.mjs +2 -2
  269. package/eslint-rules/{tex-build.mjs → tex-build.ts} +6 -5
  270. package/eslint.config.mjs +146 -30
  271. package/fixtures/install-path-literals/clean.fixture.mjs +1 -1
  272. package/fixtures/install-path-literals/clean.md +2 -2
  273. package/fixtures/install-path-literals/defect.fixture.mjs +1 -1
  274. package/fixtures/install-path-literals/defect.md +2 -2
  275. package/fixtures/pdf-facts/README.md +2 -2
  276. package/fixtures/real-markdown-paper/{baseline.mjs → baseline.ts} +31 -10
  277. package/hooks/{hooks.harness.mjs → hooks.harness.ts} +51 -34
  278. package/hooks/paper-edit-guard.hook.mjs +1 -1
  279. package/hooks/{paper-edit-guard.test.mjs → paper-edit-guard.test.ts} +3 -7
  280. package/hooks/paper-skills-nudge.hook.mjs +1 -1
  281. package/hooks/{paper-skills-nudge.test.mjs → paper-skills-nudge.test.ts} +2 -6
  282. package/hooks/paper-status-gates.sh +9 -19
  283. package/hooks/{paper-status-gates.test.mjs → paper-status-gates.test.ts} +2 -6
  284. package/lib/agent-cli-version.harness.mjs +1 -1
  285. package/lib/{agent-cli-version.mjs → agent-cli-version.ts} +32 -18
  286. package/lib/check.test.ts +14 -9
  287. package/lib/{check.mjs → check.ts} +22 -9
  288. package/lib/markdown.test.mjs +7 -7
  289. package/lib/{markdown.mjs → markdown.ts} +68 -29
  290. package/lib/paper-config.harness.mjs +9 -9
  291. package/lib/{paper-config.mjs → paper-config.ts} +20 -19
  292. package/lib/skill-checks.mjs +24 -381
  293. package/lib/skill-checks.test.mjs +64 -116
  294. package/lib/skill-corpus.mjs +10 -251
  295. package/lib/skill-corpus.test.mjs +2 -13
  296. package/lib/skill-eval-kit.mjs +1 -1
  297. package/lib/skill-trigger-cases.mjs +1 -1
  298. package/package.json +42 -10
  299. package/scripts/{check.harness.mjs → check.harness.ts} +111 -10
  300. package/scripts/{check.test.mjs → check.test.ts} +13 -10
  301. package/scripts/{check.mjs → check.ts} +116 -42
  302. package/scripts/coverage-config.test.ts +80 -38
  303. package/scripts/eslint-report-guard.test.mjs +1 -1
  304. package/scripts/harness-api.frozen.json +14 -16
  305. package/scripts/harness-api.test.ts +8 -5
  306. package/scripts/layer-legacy-frozen.test.ts +17 -13
  307. package/scripts/{layer-legacy-frozen.mjs → layer-legacy-frozen.ts} +128 -39
  308. package/scripts/release-config.test.ts +12 -4
  309. package/scripts/rules-see-files.test.mjs +1 -1
  310. package/scripts/test.test.ts +78 -0
  311. package/scripts/test.ts +87 -0
  312. package/skills/analyze-sibling-paper/SKILL.md +1 -39
  313. package/skills/analyze-sibling-paper/SKILL.md.spec.ts +0 -38
  314. package/skills/argument-arc/SKILL.md +2 -36
  315. package/skills/argument-arc/SKILL.md.spec.ts +1 -43
  316. package/skills/build-benchmark/SKILL.md +1 -41
  317. package/skills/build-benchmark/SKILL.md.spec.ts +0 -40
  318. package/skills/camera-ready/SKILL.md +1 -34
  319. package/skills/camera-ready/SKILL.md.spec.ts +0 -33
  320. package/skills/cold-read-diff/SKILL.md +1 -35
  321. package/skills/cold-read-diff/SKILL.md.spec.ts +0 -34
  322. package/skills/draft-paper/SKILL.md +2 -41
  323. package/skills/draft-paper/SKILL.md.spec.ts +1 -50
  324. package/skills/extend-paper/SKILL.md +2 -33
  325. package/skills/extend-paper/SKILL.md.spec.ts +1 -42
  326. package/skills/find-venue/SKILL.md +2 -38
  327. package/skills/find-venue/SKILL.md.spec.ts +1 -47
  328. package/skills/grade-paper-writing/SKILL.md +3 -38
  329. package/skills/grade-paper-writing/SKILL.md.spec.ts +2 -47
  330. package/skills/grade-paper-writing/prose-lint.mjs +3 -4
  331. package/skills/grade-paper-writing/{prose-lint.test.mjs → prose-lint.test.ts} +3 -3
  332. package/skills/harden-paper/SKILL.md +1 -39
  333. package/skills/harden-paper/SKILL.md.spec.ts +0 -38
  334. package/skills/map-prior-work/SKILL.md +1 -35
  335. package/skills/map-prior-work/SKILL.md.spec.ts +0 -34
  336. package/skills/osf-artifact-upload/{osf-artifact-upload.harness.mjs → osf-artifact-upload.harness.ts} +3 -4
  337. package/skills/paper-adversarial-review/SKILL.md +2 -35
  338. package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -43
  339. package/skills/paper-pipeline/SKILL.md +2 -28
  340. package/skills/paper-pipeline/SKILL.md.spec.ts +1 -27
  341. package/skills/paper-pipeline/description-language.eval.mjs +1 -1
  342. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +1 -1
  343. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +2 -2
  344. package/skills/paper-pipeline/paper-pipeline.harness.mjs +25 -40
  345. package/skills/paper-pipeline/pipeline-firing.eval.mjs +1 -1
  346. package/skills/paper-pipeline/pipeline-language.eval.mjs +1 -1
  347. package/skills/paper-pipeline/references/acceptance-gate.md +1 -6
  348. package/skills/paper-pipeline/references/pipeline-status-template.md +8 -35
  349. package/skills/paper-pipeline/scripts/README.md +30 -225
  350. package/skills/paper-pipeline/scripts/artifact-coverage.harness.mjs +4 -8
  351. package/skills/paper-pipeline/scripts/artifact-coverage.mjs +1 -1
  352. package/skills/paper-pipeline/scripts/{artifact-coverage.test.mjs → artifact-coverage.test.ts} +2 -2
  353. package/skills/paper-pipeline/scripts/consumer.harness.mjs +6 -104
  354. package/skills/paper-pipeline/scripts/consumer.mjs +24 -109
  355. package/skills/paper-pipeline/scripts/consumer.test.mjs +1 -1
  356. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +1 -1
  357. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +1 -4
  358. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +1 -1
  359. package/skills/paper-pipeline/scripts/generated-code.harness.mjs +2 -3
  360. package/skills/paper-pipeline/scripts/{generated-code.test.mjs → generated-code.test.ts} +3 -3
  361. package/skills/paper-pipeline/scripts/population-map.harness.mjs +3 -3
  362. package/skills/paper-pipeline/scripts/population-map.mjs +1 -4
  363. package/skills/paper-pipeline/scripts/population-map.test.mjs +1 -1
  364. package/skills/paper-pipeline/scripts/{round-diff.harness.mjs → round-diff.harness.ts} +45 -18
  365. package/skills/paper-pipeline/scripts/round-diff.mjs +1 -1
  366. package/skills/paper-pipeline/scripts/round-diff.test.mjs +1 -1
  367. package/skills/paper-status/SKILL.md +2 -42
  368. package/skills/paper-status/SKILL.md.spec.ts +1 -41
  369. package/skills/pc-panel-review/SKILL.md +2 -46
  370. package/skills/pc-panel-review/SKILL.md.spec.ts +1 -45
  371. package/skills/plan-paper-timeline/SKILL.md +2 -36
  372. package/skills/plan-paper-timeline/SKILL.md.spec.ts +0 -36
  373. package/skills/plan-paper-timeline/fixtures/{fake-google-calendar.mjs → fake-google-calendar.ts} +101 -70
  374. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +3 -3
  375. package/skills/render-paper/SKILL.md +1 -37
  376. package/skills/render-paper/SKILL.md.spec.ts +0 -36
  377. package/skills/render-paper/{extract-pdf-facts.harness.mjs → extract-pdf-facts.harness.ts} +34 -9
  378. package/skills/render-paper/render-paper.harness.mjs +2 -2
  379. package/skills/research-ideate/SKILL.md +2 -39
  380. package/skills/research-ideate/SKILL.md.spec.ts +1 -47
  381. package/skills/study-accepted-papers/SKILL.md +1 -34
  382. package/skills/study-accepted-papers/SKILL.md.spec.ts +0 -33
  383. package/skills/submit-paper/SKILL.md +1 -33
  384. package/skills/submit-paper/SKILL.md.spec.ts +0 -32
  385. package/skills/sweep-design-space/SKILL.md +1 -38
  386. package/skills/sweep-design-space/SKILL.md.spec.ts +0 -37
  387. package/skills/tighten-paper/SKILL.md +2 -39
  388. package/skills/tighten-paper/SKILL.md.spec.ts +1 -47
  389. package/skills/tighten-paper/structure.mjs +5 -12
  390. package/skills/tighten-paper/{structure.test.mjs → structure.test.ts} +4 -3
  391. package/skills/verify-citations/SKILL.md +2 -38
  392. package/skills/verify-citations/SKILL.md.spec.ts +1 -37
  393. package/skills/verify-citations/scripts/{bib-authors.cli.test.mjs → bib-authors.cli.test.ts} +14 -4
  394. package/skills/verify-citations/scripts/bib-authors.d.mts +63 -0
  395. package/skills/verify-citations/scripts/bib-authors.mjs +1 -4
  396. package/skills/verify-citations/scripts/{verify-cites.cli.test.mjs → verify-cites.cli.test.ts} +32 -9
  397. package/skills/verify-citations/scripts/verify-cites.d.mts +58 -0
  398. package/src/CLAUDE.md +1 -1
  399. package/src/adapters/banal/failure.ts +9 -10
  400. package/src/adapters/banal/index.test.ts +5 -8
  401. package/src/adapters/banal/index.ts +8 -7
  402. package/src/adapters/banal/install.ts +9 -4
  403. package/src/adapters/banal/invocation.test.ts +23 -9
  404. package/src/adapters/banal/invocation.ts +24 -9
  405. package/src/adapters/banal/locate.test.ts +2 -2
  406. package/src/adapters/banal/locate.ts +27 -21
  407. package/src/adapters/banal/output.test.ts +1 -1
  408. package/src/adapters/banal/run.test.ts +5 -5
  409. package/src/adapters/banal/run.ts +16 -10
  410. package/src/adapters/banal/settings.ts +2 -2
  411. package/src/adapters/banal/xml.test.ts +1 -1
  412. package/src/adapters/curl/download.test.ts +8 -6
  413. package/src/adapters/memory/index.ts +4 -3
  414. package/src/adapters/node/files.io.ts +2 -2
  415. package/src/adapters/node/files.test.ts +8 -4
  416. package/src/adapters/node/process.io.ts +13 -5
  417. package/src/adapters/node/process.test.ts +5 -5
  418. package/src/adapters/node/workspace.io.ts +9 -4
  419. package/src/adapters/node/workspace.test.ts +3 -1
  420. package/src/adapters/pdfjs/fill.test.ts +2 -2
  421. package/src/adapters/pdfjs/fill.ts +14 -7
  422. package/src/adapters/references/index.test.ts +31 -38
  423. package/src/adapters/references/index.ts +33 -60
  424. package/src/adapters/references/pool.ts +3 -5
  425. package/src/adapters/references/reach.io.ts +2 -1
  426. package/src/build-engine.harness.mjs +1 -1
  427. package/src/build-engine.test.ts +13 -8
  428. package/src/build-engine.ts +12 -7
  429. package/src/build.harness.mjs +2 -2
  430. package/src/build.test.ts +24 -24
  431. package/src/build.ts +34 -26
  432. package/src/caller-path.ts +13 -0
  433. package/src/cli.harness.mjs +6 -8
  434. package/src/cli.test.ts +31 -13
  435. package/src/cli.ts +117 -58
  436. package/src/doctor.harness.mjs +3 -3
  437. package/src/doctor.test.ts +1 -1
  438. package/src/doctor.ts +17 -7
  439. package/src/domain/lookup-cache.ts +21 -11
  440. package/src/domain/paths.test.ts +15 -0
  441. package/src/domain/paths.ts +20 -1
  442. package/src/domain/ready.ts +31 -0
  443. package/src/domain/record.test.ts +20 -0
  444. package/src/domain/record.ts +12 -0
  445. package/src/domain/result.test.ts +2 -2
  446. package/src/domain/sha256.ts +5 -3
  447. package/src/domain/text.test.ts +32 -0
  448. package/src/domain/text.ts +23 -0
  449. package/src/engine.harness.mjs +2 -2
  450. package/src/engine.ts +34 -5
  451. package/src/facts-file.test.ts +9 -7
  452. package/src/facts-file.ts +45 -29
  453. package/src/{hooks-settings.harness.mjs → hooks-settings.harness.ts} +56 -20
  454. package/src/hooks-settings.ts +48 -13
  455. package/src/init.test.ts +37 -14
  456. package/src/init.ts +48 -24
  457. package/src/{latex-log.harness.mjs → latex-log.harness.ts} +14 -14
  458. package/src/{latex-loop.harness.mjs → latex-loop.harness.ts} +64 -36
  459. package/src/latex-loop.ts +3 -3
  460. package/src/{link-skills.harness.mjs → link-skills.harness.ts} +19 -19
  461. package/src/link-skills.test.ts +3 -3
  462. package/src/link-skills.ts +5 -4
  463. package/src/lint-fix.test.ts +4 -6
  464. package/src/linted-set.test.ts +2 -2
  465. package/src/new-paper.harness.mjs +9 -25
  466. package/src/new-paper.test.ts +8 -10
  467. package/src/new-paper.ts +9 -5
  468. package/src/paper-files.test.ts +25 -24
  469. package/src/paper-files.ts +16 -27
  470. package/src/paper-settings-commands.test.ts +5 -4
  471. package/src/paper-settings.test.ts +7 -7
  472. package/src/paper-settings.ts +13 -14
  473. package/src/{pdf-facts.harness.mjs → pdf-facts.harness.ts} +9 -8
  474. package/src/pdf-facts.test.ts +25 -15
  475. package/src/pdf-facts.ts +81 -23
  476. package/src/{pdf-geometry.harness.mjs → pdf-geometry.harness.ts} +19 -12
  477. package/src/pdf-geometry.ts +8 -4
  478. package/src/ports/tool-installer.ts +2 -17
  479. package/src/presets.ts +31 -16
  480. package/src/references.test.ts +76 -62
  481. package/src/references.ts +18 -12
  482. package/src/{rules-config.harness.mjs → rules-config.harness.ts} +7 -13
  483. package/src/rules-config.ts +46 -14
  484. package/src/rules-off.test.ts +6 -4
  485. package/src/{structure.harness.mjs → structure.harness.ts} +26 -15
  486. package/src/structure.ts +1 -1
  487. package/src/{tex-requirements.harness.mjs → tex-requirements.harness.ts} +50 -22
  488. package/src/tex-requirements.test.ts +21 -0
  489. package/src/tex-requirements.ts +37 -11
  490. package/src/toolchain.harness.mjs +2 -2
  491. package/src/toolchain.test.ts +10 -6
  492. package/src/toolchain.ts +31 -17
  493. package/src/types.ts +5 -3
  494. package/src/venue-rules.test.ts +70 -68
  495. package/src/venue-rules.ts +22 -12
  496. package/tsconfig.json +6 -3
  497. package/tsconfig.pkg.json +10 -0
  498. package/lib/check.d.mts +0 -17
  499. package/lib/paper-config.d.mts +0 -31
  500. package/scripts/layer-legacy-frozen.d.mts +0 -36
  501. package/scripts/rules-are-content-only.harness.mjs +0 -105
  502. package/scripts/rules-are-content-only.mjs +0 -148
  503. package/scripts/rules-are-content-only.test.mjs +0 -61
  504. package/scripts/test.mjs +0 -44
  505. package/scripts/test.test.mjs +0 -52
  506. package/skills/paper-pipeline/scripts/announce.mjs +0 -67
  507. package/skills/paper-pipeline/scripts/announce.test.mjs +0 -102
  508. package/skills/paper-pipeline/scripts/fixtures/clean/PIPELINE-STATUS.md +0 -51
  509. package/skills/paper-pipeline/scripts/fixtures/dirty/PIPELINE-STATUS.md +0 -52
  510. package/skills/paper-pipeline/scripts/fixtures/dirty/paper.md +0 -6
  511. package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +0 -71
  512. package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +0 -302
  513. package/skills/paper-pipeline/scripts/ledger.mjs +0 -623
  514. package/skills/paper-pipeline/scripts/ledger.test.mjs +0 -254
  515. package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +0 -474
  516. package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +0 -328
  517. package/skills/paper-pipeline/scripts/pipeline-check.mjs +0 -753
  518. package/skills/paper-pipeline/scripts/pipeline-edges.mjs +0 -169
  519. package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +0 -73
  520. package/skills/paper-pipeline/scripts/run-mechanical.mjs +0 -648
  521. package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +0 -468
  522. package/skills/paper-pipeline/scripts/status.mjs +0 -295
  523. package/skills/paper-pipeline/scripts/status.test.mjs +0 -232
  524. /package/bin/{paperlint.test.mjs → paperlint.test.ts} +0 -0
  525. /package/lib/{skill-eval-fixture.mjs → skill-eval-fixture.ts} +0 -0
  526. /package/skills/grade-paper-writing/__snapshots__/{prose-lint.test.mjs.snap → prose-lint.test.ts.snap} +0 -0
  527. /package/skills/tighten-paper/__snapshots__/{structure.test.mjs.snap → structure.test.ts.snap} +0 -0
@@ -0,0 +1,639 @@
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.
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
+ * The disable directives DO work — `% eslint-disable-next-line <rule> -- reason`,
68
+ * `% eslint-disable-line`, `% eslint-disable` / `% eslint-enable` — and since 3.0.0 also
69
+ * inside a `filecontents` bibliography, whose `%` lines are made comment nodes below.
70
+ * 9. `\section*{…}` is untested and does not occur in the corpus (measured: 0 occurrences).
71
+ * When it appears, `plain()` will glue all arguments together and the heading may come out
72
+ * as `*X`.
73
+ * 10. The assertion "projection length == source length" holds on every harness input, but
74
+ * `\r\n`, emoji inside `.tex`, `\verb|…|` and nested `filecontents` are untested.
75
+ * 11. INSIDE A CAPTION no bold lead-in is synthesised: `\emph{…}` in the first column is an
76
+ * editor's line break, not the heading of an appendix block, and a block-note rule would
77
+ * take it for an ungraded block. Held by the `inCaption` counter. The flip side: a real
78
+ * `\textbf{…}` at the start of a caption line does not exist for strong-lead detection.
79
+ * Measured 2026-09-06 across three papers: zero such captions in the free half, so the
80
+ * cost today is zero.
81
+ * 12. THE CODE/PROSE BOUNDARY INSIDE A FLOAT IS DRAWN BY LINES, not by offsets, and that is
82
+ * forced: a document splitter drops from its clean text EVERY line a `code` node touches,
83
+ * so one node spanning the whole float would carry the caption back into darkness right
84
+ * after the projection had saved it. Hence `code` nodes are emitted as runs of lines that
85
+ * step around the caption lines. The cost: a line where the caption sits TOGETHER with
86
+ * markup (`\begin{table}[t]\caption{…}` on one line — one real paper writes a table that
87
+ * way) is not dropped from the prose stream at all. No harm done, the markup on it is
88
+ * blanked with spaces, but for a prose rule it is an ordinary line of text rather than a
89
+ * gap. Measured: the set of findings agreed; not pinned by a separate assertion.
90
+ * 13. `\citep{}` / `\citet{}` LEAK THEIR KEY, and `\cite{}` does not. Found 2026-09-11 while
91
+ * extracting this language, by an assertion that expected the opposite. Both names are
92
+ * listed in OPAQUE, but OPAQUE only blanks a macro NAME plus the ARGUMENTS unified-latex
93
+ * attached to it — and unified-latex has no signature for the natbib pair, so `{someone2020}`
94
+ * comes back as a separate group node and survives as prose. Measured, one input per macro:
95
+ * `\cite{keyABC}` → ` ` (fully blanked)
96
+ * `\citep{keyABC}` → ` keyABC}` (key and closing brace survive)
97
+ * `\citet{keyABC}` → ` keyABC}`
98
+ * Consequence for a paper using natbib: every citation key is a word in the prose stream.
99
+ * NOT fixed here — the fix is a signature declaration for unified-latex, which is a change
100
+ * of behaviour rather than a move, and it wants its own measurement on a real corpus.
101
+ */
102
+ import { getParser } from "@unified-latex/unified-latex-util-parse";
103
+ import { TextSourceCodeBase, VisitNodeStep, ConfigCommentParser, Directive, } from "@eslint/plugin-kit";
104
+ import assert from "node:assert/strict";
105
+ /**
106
+ * `xs[i]` for an index this module computed from `xs` itself. The default is what arithmetic on a
107
+ * missing element gave in the JavaScript this was written in: `undefined` becomes NaN, not 0.
108
+ */
109
+ const nth = (xs, i) => {
110
+ const { [i]: v = Number.NaN } = xs;
111
+ return v;
112
+ };
113
+ const HEADING = {
114
+ part: 1,
115
+ section: 2,
116
+ subsection: 3,
117
+ subsubsection: 4,
118
+ };
119
+ const CODEISH = /^(verbatim|lstlisting|minted|figure|figure\*|table|table\*|tabular|algorithm|algorithmic|thebibliography|tikzpicture|filecontents\*?)$/;
120
+ // Prose that a CODEISH environment has NO right to swallow together with the markup.
121
+ // A figure/table caption and a footnote are English sentences: the author writes them and the
122
+ // reviewer reads them. Everything else inside a float (`&`, `\\`, the column specification,
123
+ // `\includegraphics`) is not prose. Measured on the source corpus 2026-08-28: 11 blocks /
124
+ // 384 words were invisible to ALL 27 prose rules on `.tex`, and 253 of those words in one
125
+ // paper agree with `texcount`'s "Words outside text".
126
+ const FLOAT_PROSE = /^(caption|footnote)$/;
127
+ // Macros whose NAME and ARGUMENT are both non-prose.
128
+ const OPAQUE = /^(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)$/;
129
+ const P = (n) => n?.position;
130
+ /**
131
+ * Where the positioned nodes among `nodes` begin and end, or `undefined` when none is positioned.
132
+ * Nodes without a position are skipped rather than trusted, the way the projection always has.
133
+ */
134
+ const spanOf = (nodes) => {
135
+ const spans = nodes.map(P).filter((p) => p !== undefined);
136
+ const [first] = spans;
137
+ const last = spans.at(-1);
138
+ return first && last
139
+ ? { s: first.start.offset, e: last.end.offset }
140
+ : undefined;
141
+ };
142
+ function plain(nodes) {
143
+ let out = "";
144
+ for (const n of nodes || []) {
145
+ if (n.type === "string")
146
+ out += n.content;
147
+ else if (n.type === "whitespace")
148
+ out += " ";
149
+ else if (n.type === "group" || n.type === "argument")
150
+ out += plain(n.content);
151
+ else if (n.type === "macro" && n.args)
152
+ out += plain(n.args.flatMap((a) => a.content || []));
153
+ }
154
+ return out.trim();
155
+ }
156
+ const argEnd = (node, start) => {
157
+ let end = start;
158
+ for (const a of node.args || [])
159
+ for (const c of a.content || []) {
160
+ const at = P(c);
161
+ if (at && at.end.offset > end.offset)
162
+ end = at.end;
163
+ }
164
+ return end;
165
+ };
166
+ /**
167
+ * The projection of `src`. `parse` is unified-latex by default; a test passes one returning the
168
+ * node shapes today's parser never emits (a macro without `args`, an argument without `content`, an
169
+ * `env` given as nodes), which the fallbacks below keep reading instead of throwing on.
170
+ */
171
+ /** Calls `f` on each node in `content`: none for text or nothing, the node itself, or each of a list. */
172
+ function forEachNode(content, f) {
173
+ if (!content || typeof content !== "object")
174
+ return;
175
+ if ("type" in content)
176
+ f(content);
177
+ else
178
+ for (const n of content)
179
+ forEachNode(n, f);
180
+ }
181
+ export function texToMdast(src, { parse = (s) => getParser().parse(s), } = {}) {
182
+ const ast = parse(src);
183
+ const chars = src.split("");
184
+ const blank = (from, to) => {
185
+ for (let i = from; i < to && i < chars.length; i++)
186
+ if (chars[i] !== "\n")
187
+ chars[i] = " ";
188
+ };
189
+ const lineStarts = [0];
190
+ for (let i = 0; i < src.length; i++)
191
+ if (src[i] === "\n")
192
+ lineStarts.push(i + 1);
193
+ const loc = (off) => {
194
+ let lo = 0, hi = lineStarts.length - 1;
195
+ while (lo < hi) {
196
+ const m = (lo + hi + 1) >> 1;
197
+ if (nth(lineStarts, m) <= off)
198
+ lo = m;
199
+ else
200
+ hi = m - 1;
201
+ }
202
+ return { line: lo + 1, column: off - nth(lineStarts, lo) + 1, offset: off };
203
+ };
204
+ const children = [];
205
+ const pendingStrong = [];
206
+ // Nesting depth inside a `\caption{}` / `\footnote{}` argument. Exactly one place needs it:
207
+ // bold lead-in synthesis. An `\emph{…}` in the first column INSIDE a caption is an editor's
208
+ // line break, not the heading of an appendix block, and a block-note rule would take it for
209
+ // an ungraded block. Measured across three papers: today there are no such captions in the
210
+ // free half (0 findings), so this guards against tomorrow rather than repairing today.
211
+ let inCaption = 0;
212
+ /** A synthetic heading: the ATX marker is written at the START of the span, the tail is blanked. */
213
+ const synthHeading = (from, to, title, depth = 2) => {
214
+ const label = "#".repeat(depth) + " " + title;
215
+ if (label.length > to - from)
216
+ return null; // does not fit — do not invent one silently
217
+ blank(from, to);
218
+ label.split("").forEach((ch, i) => {
219
+ chars[from + i] = ch;
220
+ });
221
+ const node = {
222
+ type: "heading",
223
+ depth,
224
+ title,
225
+ children: [
226
+ {
227
+ type: "text",
228
+ value: title,
229
+ position: {
230
+ start: loc(from + depth + 1),
231
+ end: loc(from + label.length),
232
+ },
233
+ },
234
+ ],
235
+ position: { start: loc(from), end: loc(from + label.length) },
236
+ };
237
+ children.push(node);
238
+ return node;
239
+ };
240
+ const beginDoc = src.indexOf("\\begin{document}");
241
+ const preEnd = beginDoc > 0 ? beginDoc + "\\begin{document}".length : 0;
242
+ if (preEnd) {
243
+ blank(0, preEnd);
244
+ children.push({
245
+ type: "code",
246
+ lang: "latex",
247
+ value: "",
248
+ position: { start: loc(0), end: loc(preEnd) },
249
+ });
250
+ }
251
+ // 🔴 A `filecontents` body is not LaTeX — it is the `.bib` the paper writes, usually in the
252
+ // preamble — so the parser returns it as one verbatim node and no comment nodes from it. A
253
+ // disable directive above a bibliography entry (`% eslint-disable-next-line bib/reachable-entry
254
+ // -- …`) was therefore invisible. Its `%` lines are made comments here, the only way
255
+ // `getInlineConfigNodes` sees them. BibTeX ignores text between entries, so the line is
256
+ // harmless to the build.
257
+ for (const n of ast.content) {
258
+ const np = P(n);
259
+ if (n.type === "verbatim" && /^filecontents\*?$/.test(n.env) && np) {
260
+ let at = np.start.offset;
261
+ for (const line of src.slice(at, np.end.offset).split("\n")) {
262
+ const text = line.replace(/\r$/, "");
263
+ const pct = /^\s*%/.test(text) ? text.indexOf("%") : -1;
264
+ if (pct >= 0)
265
+ children.push({
266
+ type: "html",
267
+ value: text.slice(pct + 1),
268
+ position: { start: loc(at + pct), end: loc(at + text.length) },
269
+ });
270
+ at += line.length + 1;
271
+ }
272
+ }
273
+ }
274
+ /** What `walk` descends into: a node's `content` (nodes, or a string it skips) and `args`. */
275
+ const parts = (n) => [
276
+ "content" in n ? n.content : undefined,
277
+ "args" in n ? n.args : undefined,
278
+ ];
279
+ forEachNode(ast.content, function walk(node) {
280
+ const pos = P(node);
281
+ // The preamble is already blanked in full; nothing inside it (including a
282
+ // `\renewcommand{\bibliography}`) is a node of the document. Without this cut-off, one real
283
+ // paper got a FALSE `## References` on line 178 — i.e. its bibliography "began" before the
284
+ // introduction, and the whole paper fell into the free half.
285
+ if (pos && pos.start.offset < preEnd) {
286
+ for (const v of parts(node))
287
+ forEachNode(v, walk);
288
+ return;
289
+ }
290
+ if (node.type === "environment") {
291
+ const env = typeof node.env === "string" ? node.env : plain(node.env);
292
+ // 🔴 MEASURED 2026-08-26: none of the three real `.tex` papers has a `Limitations` /
293
+ // `References` / `Appendix` heading. In LaTeX the free half is declared by an
294
+ // ENVIRONMENT and a macro rather than by a heading, so without synthesis `freeRanges`
295
+ // is empty and seven rules are silent by construction.
296
+ if (env === "abstract" && pos) {
297
+ synthHeading(pos.start.offset, pos.start.offset + "\\begin{abstract}".length, "Abstract");
298
+ for (const v of parts(node))
299
+ forEachNode(v, walk);
300
+ return;
301
+ }
302
+ if (env === "thebibliography" && pos) {
303
+ // `\begin{thebibliography}` (23 characters) always holds `## References` (13).
304
+ synthHeading(pos.start.offset, pos.start.offset + "\\begin{thebibliography}".length, "References");
305
+ blank(pos.start.offset + "## References".length, pos.end.offset);
306
+ children.push({
307
+ type: "code",
308
+ lang: env,
309
+ value: "",
310
+ position: { start: loc(pos.start.offset + 14), end: pos.end },
311
+ });
312
+ return;
313
+ }
314
+ if (CODEISH.test(env) && pos) {
315
+ // 🔴 THERE WERE TWO LOCKS, AND LIFTING ONE DID NOT HELP: a
316
+ // caption was blanked by `OPAQUE` as a macro, and inside `table` / `figure` it was
317
+ // blanked AGAIN by the whole environment. So what is blanked here is not the float's
318
+ // borders but the float MINUS the arguments of `\caption{}` / `\footnote{}`.
319
+ //
320
+ // Removing `table` / `figure` from `CODEISH` was NOT an option: prose rules would then
321
+ // receive `&`, `\\` and the column specification, i.e. findings on markup — and for a
322
+ // rule somebody turns up to `error`, a false positive is worse than a miss.
323
+ const keep = [];
324
+ forEachNode(node.content, function findProse(n) {
325
+ if (n.type === "macro" && FLOAT_PROSE.test(n.content)) {
326
+ const span = spanOf((n.args || []).flatMap((a) => a.content || []));
327
+ // Keep exactly the CONTENT of the argument: `\caption` itself and the braces
328
+ // stay in the blanked part, so the macro name never becomes prose.
329
+ if (span)
330
+ keep.push({ node: n, ...span });
331
+ return; // do not look for a caption inside a caption
332
+ }
333
+ for (const v of parts(n))
334
+ forEachNode(v, findProse);
335
+ });
336
+ keep.sort((a, b) => a.s - b.s);
337
+ let cur = pos.start.offset;
338
+ for (const k of keep) {
339
+ blank(cur, k.s);
340
+ cur = Math.max(cur, k.e);
341
+ }
342
+ blank(cur, pos.end.offset);
343
+ // 🔴 The code node is emitted LINE BY LINE, stepping around the caption lines. A
344
+ // document splitter drops from its clean text every line a `code` node TOUCHES, so one
345
+ // node spanning the whole float would carry the caption back into darkness right after
346
+ // the projection had saved it.
347
+ const proseLines = new Set();
348
+ for (const k of keep)
349
+ for (let l = loc(k.s).line; l <= loc(Math.max(k.s, k.e - 1)).line; l++)
350
+ proseLines.add(l);
351
+ const L0 = loc(pos.start.offset).line;
352
+ const L1 = loc(Math.max(pos.start.offset, pos.end.offset - 1)).line;
353
+ for (let l = L0; l <= L1; l++) {
354
+ if (proseLines.has(l))
355
+ continue;
356
+ let r = l;
357
+ while (r + 1 <= L1 && !proseLines.has(r + 1))
358
+ r++;
359
+ const s0 = nth(lineStarts, l - 1);
360
+ const e0 = r < lineStarts.length ? nth(lineStarts, r) - 1 : src.length;
361
+ children.push({
362
+ type: "code",
363
+ lang: env,
364
+ value: "",
365
+ position: { start: loc(s0), end: loc(e0) },
366
+ });
367
+ l = r;
368
+ }
369
+ // We walk into the caption itself the ordinary way: `\label`, `\cite` and math inside
370
+ // it are blanked by the same branches as in the body, not by a separate parser.
371
+ for (const k of keep)
372
+ walk(k.node);
373
+ return;
374
+ }
375
+ // other environments: blank only the `\begin{x}` / `\end{x}` fences
376
+ if (pos) {
377
+ blank(pos.start.offset, pos.start.offset + env.length + 8);
378
+ blank(Math.max(0, pos.end.offset - env.length - 6), pos.end.offset);
379
+ }
380
+ }
381
+ if ((node.type === "inlinemath" ||
382
+ node.type === "displaymath" ||
383
+ node.type === "verbatim") &&
384
+ pos) {
385
+ blank(pos.start.offset, pos.end.offset);
386
+ return;
387
+ }
388
+ if (node.type === "comment" && pos) {
389
+ // 🔴 unified-latex includes the TRAILING newline in a comment; mdast does not. A rule
390
+ // that binds an author note to the heading below it requires a newline BETWEEN the end of
391
+ // the comment and the heading; with the `\n` swallowed the gap is empty and the note is
392
+ // never found.
393
+ let ce = pos.end.offset;
394
+ while (ce > pos.start.offset &&
395
+ (src[ce - 1] === "\n" || src[ce - 1] === "\r"))
396
+ ce--;
397
+ // 🔴 AND SYMMETRICALLY ON THE LEFT (found by a run on 2026-08-27, not by reading). A
398
+ // comment that follows a line of text directly, rather than a blank line, comes back from
399
+ // unified-latex with a position starting at the PREVIOUS newline instead of at `%`.
400
+ // Consumers recognise a comment by its first character (`<!--` in markdown, `%` in
401
+ // LaTeX), so such a node was not a comment to them: notes above bold appendix lead-ins
402
+ // never bound, and the block-note / block-verdict rules could NEVER fire. Measured:
403
+ // 2 out of 2 blocks were reported as "no note AT ALL" while the notes were right there.
404
+ let cs = pos.start.offset;
405
+ while (cs < ce && src[cs] !== "%")
406
+ cs++;
407
+ children.push({
408
+ type: "html",
409
+ value: node.content,
410
+ position: { start: loc(cs), end: loc(ce) },
411
+ });
412
+ return; // blanking is the document splitter's job — it also takes the note body
413
+ }
414
+ if (node.type === "macro" && pos) {
415
+ const depth = HEADING[node.content];
416
+ if (depth) {
417
+ const title = plain(node.args?.flatMap((a) => a.content || []));
418
+ const end = argEnd(node, pos.end);
419
+ // project to ATX: `##` where `\se` was, the rest spaces; the closing `}` too
420
+ blank(pos.start.offset, end.offset + 1);
421
+ for (let i = 0; i < depth; i++)
422
+ chars[pos.start.offset + i] = "#";
423
+ const tStart = end.offset - title.length;
424
+ title.split("").forEach((ch, i) => {
425
+ chars[tStart + i] = ch;
426
+ });
427
+ const tPos = { start: loc(tStart), end: loc(end.offset) };
428
+ children.push({
429
+ type: "heading",
430
+ depth,
431
+ title,
432
+ children: [{ type: "text", value: title, position: tPos }],
433
+ position: {
434
+ start: pos.start,
435
+ end: { ...end, offset: end.offset + 1, column: end.column + 1 },
436
+ },
437
+ });
438
+ return;
439
+ }
440
+ if (node.content === "bibliography") {
441
+ synthHeading(pos.start.offset, argEnd(node, pos.end).offset + 1, "References");
442
+ return;
443
+ }
444
+ if (OPAQUE.test(node.content)) {
445
+ blank(pos.start.offset, argEnd(node, pos.end).offset + 1);
446
+ return;
447
+ }
448
+ // `\texttt{X}` → `` `X` ``: exactly the same character count, so offsets do not move.
449
+ // This is the only markdown markup the projection can reproduce without shifting: `**`
450
+ // needs TWO characters after the content, and there is only `}`.
451
+ if (node.content === "texttt" || node.content === "lstinline") {
452
+ const e = argEnd(node, pos.end);
453
+ const span = spanOf((node.args || []).flatMap((a) => a.content || []));
454
+ if (span) {
455
+ const s0 = span.s;
456
+ blank(pos.start.offset, s0 - 1);
457
+ chars[s0 - 1] = "`";
458
+ chars[e.offset] = "`";
459
+ return;
460
+ }
461
+ }
462
+ if ((node.content === "textbf" || node.content === "emph") &&
463
+ pos.start.column === 1 &&
464
+ !inCaption) {
465
+ const end = argEnd(node, pos.end);
466
+ const sv = plain(node.args?.flatMap((a) => a.content || []));
467
+ children.push({
468
+ type: "strong",
469
+ children: [
470
+ {
471
+ type: "text",
472
+ value: sv,
473
+ position: {
474
+ start: loc(end.offset - sv.length),
475
+ end: loc(end.offset),
476
+ },
477
+ },
478
+ ],
479
+ position: {
480
+ start: pos.start,
481
+ end: { ...end, offset: end.offset + 1, column: end.column + 1 },
482
+ },
483
+ });
484
+ // 🔴 The `**` are written IN PLACE OF `\\te`, not before the content: a block-note rule
485
+ // locates a lead-in by the OFFSET of the line's first non-space character. Leave spaces
486
+ // at the start of the line and the note above the block never binds, so block-* go
487
+ // silent.
488
+ pendingStrong.push(pos.start.offset);
489
+ }
490
+ // an ordinary macro: blank the NAME (`\emph`) and the argument's BRACES; the content is prose
491
+ blank(pos.start.offset, pos.end.offset);
492
+ for (const o of pendingStrong.splice(0)) {
493
+ chars[o] = "*";
494
+ chars[o + 1] = "*";
495
+ }
496
+ for (const a of node.args || []) {
497
+ const span = spanOf(a.content || []);
498
+ if (!span)
499
+ continue;
500
+ const { s: s0, e: e0 } = span;
501
+ if (a.openMark)
502
+ blank(s0 - a.openMark.length, s0);
503
+ if (a.closeMark)
504
+ blank(e0, e0 + a.closeMark.length);
505
+ }
506
+ if (FLOAT_PROSE.test(node.content)) {
507
+ inCaption++;
508
+ for (const v of parts(node))
509
+ forEachNode(v, walk);
510
+ inCaption--;
511
+ return;
512
+ }
513
+ }
514
+ for (const v of parts(node))
515
+ forEachNode(v, walk);
516
+ });
517
+ children.sort((a, b) => a.position.start.offset - b.position.start.offset);
518
+ return {
519
+ root: {
520
+ type: "root",
521
+ children,
522
+ position: { start: loc(0), end: loc(src.length) },
523
+ },
524
+ text: chars.join(""),
525
+ };
526
+ }
527
+ const commentParser = new ConfigCommentParser();
528
+ const directiveStart = /^\s*eslint(?:-enable|-disable(?:(?:-next)?-line)?)?(?:\s|$)/u;
529
+ const DIRECTIVES = new Map([
530
+ ["eslint-disable", "disable"],
531
+ ["eslint-enable", "enable"],
532
+ ["eslint-disable-next-line", "disable-next-line"],
533
+ ["eslint-disable-line", "disable-line"],
534
+ ]);
535
+ class TexSourceCode extends TextSourceCodeBase {
536
+ #steps;
537
+ #parents = new WeakMap();
538
+ #comments = [];
539
+ #inline;
540
+ ast;
541
+ /**
542
+ * `raw` is the UNPROJECTED `.tex`, and it is here because the projection is lossy by design:
543
+ * it blanks macros, so a rule that must count `\S\ref` or `Fig.~\ref` sees spaces. Those
544
+ * rules are about MARKUP, not prose, and the projection exists precisely to hide markup.
545
+ *
546
+ * 🔴 Positions stay interchangeable, which is what makes this safe rather than a second
547
+ * coordinate system: `blank()` overwrites characters with spaces and never changes length,
548
+ * so an offset computed on `raw` addresses the same byte of `text`. A rule may therefore
549
+ * scan `raw` and report with the offset it found.
550
+ */
551
+ raw;
552
+ constructor({ text, ast, raw, }) {
553
+ super({ ast, text, lineEndingPattern: /\r?\n/u });
554
+ this.ast = ast;
555
+ this.raw = raw ?? text;
556
+ this.traverse();
557
+ }
558
+ getParent(node) {
559
+ return this.#parents.get(node);
560
+ }
561
+ getInlineConfigNodes() {
562
+ if (!this.#inline)
563
+ this.#inline = this.#comments
564
+ .filter((c) => directiveStart.test(c.value))
565
+ .map((c) => ({ value: c.value.trim(), position: c.position }));
566
+ return this.#inline;
567
+ }
568
+ getDisableDirectives() {
569
+ const directives = [];
570
+ for (const comment of this.getInlineConfigNodes()) {
571
+ const parsed = commentParser.parseDirective(comment.value);
572
+ // `directiveStart` admitted this comment, and every text it admits parses as a directive.
573
+ assert.ok(parsed, `unparsable directive: ${comment.value}`);
574
+ const { label, value, justification } = parsed;
575
+ const type = DIRECTIVES.get(label);
576
+ if (type)
577
+ directives.push(new Directive({ type, node: comment, value, justification }));
578
+ }
579
+ return { problems: [], directives };
580
+ }
581
+ applyInlineConfig() {
582
+ return { configs: [], problems: [] };
583
+ }
584
+ traverse() {
585
+ if (this.#steps)
586
+ return this.#steps.values();
587
+ const steps = (this.#steps = []);
588
+ const visit = (node, parent) => {
589
+ this.#parents.set(node, parent);
590
+ steps.push(new VisitNodeStep({ target: node, phase: 1, args: [node, parent] }));
591
+ if (node.type === "html")
592
+ this.#comments.push({ value: node.value, position: node.position });
593
+ for (const c of "children" in node ? node.children : [])
594
+ visit(c, node);
595
+ steps.push(new VisitNodeStep({ target: node, phase: 2, args: [node, parent] }));
596
+ };
597
+ visit(this.ast);
598
+ return steps.values();
599
+ }
600
+ }
601
+ export const texLanguage = {
602
+ fileType: "text",
603
+ lineStart: 1,
604
+ columnStart: 1,
605
+ nodeTypeKey: "type",
606
+ defaultLanguageOptions: {},
607
+ validateLanguageOptions() { },
608
+ parse(file) {
609
+ try {
610
+ const raw = String(file.body);
611
+ const { root, text } = texToMdast(raw);
612
+ return { ok: true, ast: root, projected: text, raw };
613
+ }
614
+ catch (ex) {
615
+ // What the parser throws is an Error: its own syntax error, or the stack exhausted. ESLint
616
+ // prints a parse failure at its `line` and `column`, and a failure of the whole parse has
617
+ // no position of its own, so it is placed at the start of the file.
618
+ assert.ok(ex instanceof Error);
619
+ return { ok: false, errors: [Object.assign(ex, { line: 1, column: 1 })] };
620
+ }
621
+ },
622
+ createSourceCode(file, parseResult) {
623
+ // What `parse` above returned, read back: ESLint hands a language's extra fields through
624
+ // untyped, so they are narrowed here rather than trusted.
625
+ const projected = parseResult["projected"];
626
+ const raw = parseResult["raw"];
627
+ assert.ok(typeof projected === "string");
628
+ // 🔴 LOAD-BEARING: the SourceCode receives the PROJECTION, not the source. Lengths and
629
+ // offsets are equal, so the position of a finding points into the real `.tex` while the
630
+ // rule sees prose.
631
+ return new TexSourceCode({
632
+ text: projected,
633
+ ast: parseResult.ast,
634
+ // Absent from a parse result made by hand; the source code then reads the projection.
635
+ raw: typeof raw === "string" ? raw : undefined,
636
+ });
637
+ },
638
+ };
639
+ //# sourceMappingURL=latex-language.js.map