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,599 @@
1
+ /**
2
+ * Colocated test for the LaTeX language `eslint-rules/latex-language.mjs`.
3
+ * Run: `npx vigiles test eslint-rules/latex-language.harness.mjs`
4
+ *
5
+ * FIVE PARTS, AND THE ORDER IS LOAD-BEARING:
6
+ * 0. THE PROJECTION INVARIANT — length, line count and newline offsets equal the source.
7
+ * EVERYTHING else stands on this: once offsets drift, every finding lies, and lies
8
+ * quietly. Checked on both fixtures and on every synthetic input below.
9
+ * I. QUIET ON A CORRECT INPUT — before "fires on a planted defect", deliberately: a check
10
+ * that fails a correct input is more dangerous than no check. `fixtures/latex-language/
11
+ * clean.tex` goes through the repository's real `eslint.config.mjs` and must be silent.
12
+ * II. THE PROJECTION ACTUALLY DID ITS WORK — headings (including SYNTHESISED ones), caption
13
+ * and footnote prose kept, float markup blanked, opaque macros blanked, math blanked,
14
+ * comments recognised. This is the half that a mutation battery can kill; without it the
15
+ * language could be gutted and part I would stay green, because a projection that blanks
16
+ * everything is also silent.
17
+ * III. FIRES ON A PLANTED DEFECT — `fixtures/latex-language/defect.tex`, through the real
18
+ * config, must produce findings, and at the right addresses.
19
+ * IV. THE LANGUAGE'S BOUNDARIES AS ASSERTIONS. A known limitation recorded only in prose
20
+ * silently stops being known. Pinned here: change the behaviour and this test goes red
21
+ * and forces the language's header to be rewritten, instead of the drift surfacing in a
22
+ * month.
23
+ * V. CONFIG — the language is wired to `.tex`, and the glob it is wired on is NOT EMPTY.
24
+ *
25
+ * ⚠️ WHAT WAS LOST WHEN THIS TEST LEFT THE PRIVATE CORPUS BEHIND, stated rather than glossed.
26
+ * The original harness drove SIX rule modules (walls, prose, claims, structure, craft,
27
+ * registry) through this language and froze their finding counts on three real papers
28
+ * (35 · 52 · 15 at the 2026-08-27 measurement, later 32 · 53 · 20 after captions became
29
+ * visible on 2026-09-06). None of those modules was extracted, and the papers are private, so:
30
+ * - the "rules see LaTeX the way they saw markdown" half is GONE. What is left proves the
31
+ * projection is correct, not that a downstream rule consumes it correctly;
32
+ * - the config half that checked rule-set PARITY between `.tex` and `paper.md`, and the
33
+ * list of rules explicitly switched `off` on `.tex` WITH A REASON, is GONE with the rules;
34
+ * - the only rule that still runs through this language here is `tex/future-promise`, and it
35
+ * reads the file from disk rather than the projection — so end-to-end it proves the
36
+ * language parses and dispatches, not that the projection is what a rule reads.
37
+ * That last gap is why part II asserts the projection DIRECTLY on `texToMdast`, rather than
38
+ * inferring it from someone's finding count.
39
+ *
40
+ * 🔴 Assertions live at MODULE TOP LEVEL: `vigiles test` imports the file and treats "it did
41
+ * not throw" as success; an exported `tests` object is run by nothing.
42
+ */
43
+ import { ESLint } from "eslint";
44
+ import assert from "node:assert/strict";
45
+ import {
46
+ existsSync,
47
+ mkdirSync,
48
+ mkdtempSync,
49
+ readFileSync,
50
+ rmSync,
51
+ writeFileSync,
52
+ realpathSync,
53
+ } from "node:fs";
54
+ import { tmpdir } from "node:os";
55
+ import { dirname, join, relative } from "node:path";
56
+ import { fileURLToPath } from "node:url";
57
+
58
+ const HERE = dirname(fileURLToPath(import.meta.url));
59
+ const ROOT = join(HERE, ".."); // eslint-rules → repository root
60
+ const LANG = join(HERE, "latex-language.mjs");
61
+ assert.ok(existsSync(LANG), `no entry point for the test: ${LANG}`);
62
+
63
+ const { texLanguage, texToMdast } = await import(LANG);
64
+
65
+ const TMP = realpathSync(mkdtempSync(join(tmpdir(), "latex-language-")));
66
+ // Cleanup is registered IMMEDIATELY, not at the end of the file: assertions throw, and an
67
+ // `rmSync` at the bottom never runs in exactly the runs that are red.
68
+ process.on("exit", () => rmSync(TMP, { recursive: true, force: true }));
69
+
70
+ const CLEAN_FIXTURE = join(ROOT, "fixtures/latex-language/clean.tex");
71
+ const DEFECT_FIXTURE = join(ROOT, "fixtures/latex-language/defect.tex");
72
+ for (const f of [CLEAN_FIXTURE, DEFECT_FIXTURE])
73
+ assert.ok(existsSync(f), `missing fixture: ${f}`);
74
+
75
+ // ═════════════════════════════════════════════════════════════════════════════
76
+ // 0. THE PROJECTION INVARIANT
77
+ // ═════════════════════════════════════════════════════════════════════════════
78
+ // The projection must be character-for-character COMMENSURATE with the source: same length,
79
+ // same newlines at the same offsets. Otherwise a finding's `file:line:col` points somewhere
80
+ // else — and, worse, lies plausibly: the line exists, it is just not the right one.
81
+ function assertProjection(label, src) {
82
+ const { text } = texToMdast(src);
83
+ assert.equal(
84
+ text.length,
85
+ src.length,
86
+ `${label}: projection length drifted from the source`,
87
+ );
88
+ const nl = (s) => {
89
+ const out = [];
90
+ for (let i = 0; i < s.length; i++) if (s[i] === "\n") out.push(i);
91
+ return out;
92
+ };
93
+ assert.deepEqual(
94
+ nl(text),
95
+ nl(src),
96
+ `${label}: projection newlines moved relative to the source`,
97
+ );
98
+ return text;
99
+ }
100
+ /** Reads a fixture, checks the invariant on it, and returns `{src, text, root}`. */
101
+ function project(label, src) {
102
+ const text = assertProjection(label, src);
103
+ return { src, text, root: texToMdast(src).root };
104
+ }
105
+
106
+ const CLEAN_SRC = readFileSync(CLEAN_FIXTURE, "utf8");
107
+ const DEFECT_SRC = readFileSync(DEFECT_FIXTURE, "utf8");
108
+ const clean = project("fixtures/latex-language/clean.tex", CLEAN_SRC);
109
+ const defect = project("fixtures/latex-language/defect.tex", DEFECT_SRC);
110
+
111
+ // ═════════════════════════════════════════════════════════════════════════════
112
+ // I. QUIET ON A CORRECT INPUT — through the repository's real config
113
+ // ═════════════════════════════════════════════════════════════════════════════
114
+ // 🔴 Deliberately NOT an in-process config. A language that is correct and unreferenced would
115
+ // pass every assertion in part II and still be wired to nothing; only a run through
116
+ // `eslint.config.mjs` as it sits on disk can fail on that.
117
+ const real = new ESLint({ cwd: ROOT });
118
+ async function lintTex(file) {
119
+ const [res] = await real.lintFiles([file]);
120
+ assert.deepEqual(
121
+ res.messages.filter((m) => m.fatal || !m.ruleId),
122
+ [],
123
+ `${file}: the language failed to parse or a rule threw — that is a linter crash, not a finding`,
124
+ );
125
+ return res.messages.map((m) => ({
126
+ rule: m.ruleId,
127
+ line: m.line,
128
+ column: m.column,
129
+ text: /«([^»]+)»/.exec(m.message)?.[1] ?? null,
130
+ }));
131
+ }
132
+
133
+ assert.deepEqual(
134
+ await lintTex(CLEAN_FIXTURE),
135
+ [],
136
+ "a CORRECT LaTeX paper produced findings — a check that fails a correct input gets switched " +
137
+ "off the same day",
138
+ );
139
+
140
+ // ═════════════════════════════════════════════════════════════════════════════
141
+ // II. THE PROJECTION ACTUALLY DID ITS WORK
142
+ // ═════════════════════════════════════════════════════════════════════════════
143
+ // Without this part, part I is satisfied by a language that blanks the entire document: zero
144
+ // findings either way. Each assertion below names one property of the projection and is the
145
+ // thing a mutation of that property kills.
146
+ const headings = (root) => root.children.filter((c) => c.type === "heading");
147
+
148
+ {
149
+ const titles = headings(clean.root).map((h) => h.title);
150
+ // 🔴 SYNTHESIS. Neither `Abstract` nor `References` is written as a heading anywhere in the
151
+ // fixture: the abstract is an environment, the bibliography is a macro. Measured on the
152
+ // source corpus 2026-08-26 — none of the three real papers had a `Limitations` /
153
+ // `References` / `Appendix` heading, so without synthesis the "free half" of a paper is
154
+ // empty and seven downstream rules are silent by construction.
155
+ assert.ok(
156
+ titles.includes("Abstract"),
157
+ `no synthesised Abstract heading; got ${titles.join(" | ")}`,
158
+ );
159
+ assert.ok(
160
+ titles.includes("References"),
161
+ `no synthesised References heading; got ${titles.join(" | ")}`,
162
+ );
163
+ assert.equal(titles[0], "Abstract", "Abstract is not the first heading");
164
+ assert.equal(
165
+ titles[titles.length - 1],
166
+ "References",
167
+ "References is not the last heading",
168
+ );
169
+ // Real `\section{}` headings arrived as well — otherwise "the LaTeX was read" is a claim
170
+ // about two synthetic nodes.
171
+ assert.deepEqual(
172
+ titles,
173
+ ["Abstract", "Introduction", "Method", "Results", "References"],
174
+ "the heading set of clean.tex drifted — LaTeX was parsed differently",
175
+ );
176
+ // ATX projection: `##` is written where `\se` used to be, so a markdown-shaped consumer sees
177
+ // a heading at the real offset.
178
+ assert.match(
179
+ clean.text,
180
+ /^##\s+Introduction\s*$/m,
181
+ "`\\section{Introduction}` did not project to an ATX heading",
182
+ );
183
+ }
184
+
185
+ {
186
+ // 🔴 THE PREAMBLE CUT-OFF. Nothing before `\begin{document}` is a node of the document.
187
+ // Without the cut-off, one real paper of the source corpus got a FALSE `## References` on
188
+ // line 178 — its bibliography "began" before the introduction, so the whole paper fell into
189
+ // the "free half" that several downstream rules measure.
190
+ //
191
+ // ⚠️ MEASURED WHILE EXTRACTING (2026-09-11), and it changed this fixture: the corpus paper's
192
+ // actual trigger was a `\renewcommand{\bibliography}` in the preamble, and that construct
193
+ // does NOT reproduce the defect here. unified-latex knows `\renewcommand`'s signature, so
194
+ // `{\bibliography}` arrives as an ATTACHED ARGUMENT, `renewcommand` is in OPAQUE, and the
195
+ // walk blanks it and returns before it can ever reach the inner macro. The trigger is any
196
+ // preamble construct whose braces come back as a SIBLING GROUP instead — measured with the
197
+ // cut-off removed:
198
+ // `\AtBeginDocument{\bibliography{refs}}` → false `References` heading on line 2
199
+ // `{\bibliography{refs}}` → false `References` heading on line 2
200
+ // `\section{Ghost}` → a real heading, also on line 2
201
+ // `\renewcommand{\bibliography}[1]{...}` → nothing (the case that used to be quoted)
202
+ // So the property is real and the old example of it was wrong. `\AtBeginDocument` is used
203
+ // here because it is both a genuine thing papers write and a reproducible trigger.
204
+ const src = [
205
+ "\\documentclass{article}",
206
+ "\\AtBeginDocument{\\bibliography{refs}}",
207
+ "\\begin{document}",
208
+ "\\begin{abstract}",
209
+ "One paragraph, nothing more.",
210
+ "\\end{abstract}",
211
+ "\\section{Introduction}",
212
+ "We measured one thing and report it plainly.",
213
+ "\\bibliography{refs}",
214
+ "\\end{document}",
215
+ "",
216
+ ].join("\n");
217
+ const { root } = project("preamble-cutoff", src);
218
+ const titles = headings(root).map((h) => h.title);
219
+ assert.deepEqual(
220
+ titles,
221
+ ["Abstract", "Introduction", "References"],
222
+ "a preamble construct produced a heading — the cut-off was lost, and with a References " +
223
+ "heading landing before the introduction the whole document reads as its own free half",
224
+ );
225
+ assert.ok(
226
+ headings(root).find((h) => h.title === "References").position.start.line >
227
+ 8,
228
+ "References was synthesised in the preamble rather than at the document's bibliography",
229
+ );
230
+ }
231
+
232
+ {
233
+ // THE PREAMBLE IS BLANKED. Everything before `\begin{document}` is markup, and if it stays
234
+ // visible the package list and the author macros become prose.
235
+ const preEnd =
236
+ CLEAN_SRC.indexOf("\\begin{document}") + "\\begin{document}".length;
237
+ assert.equal(
238
+ clean.text.slice(0, preEnd).trim(),
239
+ "",
240
+ "the preamble is not blanked — package names and author macros leaked into the prose",
241
+ );
242
+ assert.ok(
243
+ CLEAN_SRC.includes("\\usepackage{natbib}"),
244
+ "the fixture lost its preamble content — the assertion above no longer has a subject",
245
+ );
246
+ }
247
+
248
+ {
249
+ // 🔴 CAPTIONS AND FOOTNOTES ARE PROSE, AND THIS WAS A DOUBLE LOCK. Until 2026-09-06 the text
250
+ // inside `\caption{}` / `\footnote{}` was invisible to every prose rule reading `.tex`: it
251
+ // was blanked once as an opaque macro argument, and again as part of the surrounding
252
+ // `table` / `figure`. Lifting either lock alone changed nothing. Measured on the source
253
+ // corpus: 11 blocks / 384 words of author English that no check could see, of which 253 in
254
+ // one paper agree with `texcount`'s own "Words outside text" count.
255
+ assert.ok(
256
+ clean.text.includes(
257
+ "Deterministic audit of ten files, reported exactly as they were counted.",
258
+ ),
259
+ "the caption inside `table` was swallowed — prose rules are blind to captions again",
260
+ );
261
+ assert.ok(
262
+ clean.text.includes(
263
+ "Both\nruns used the same machine and the same working directory, in that order.",
264
+ ),
265
+ "the footnote text was swallowed",
266
+ );
267
+ // 🔴 AND THE OTHER HALF, without which the one above invites the naive fix of dropping
268
+ // `table` / `figure` from the code-ish set: the MARKUP of the float must still be blanked.
269
+ // Otherwise prose rules receive `&`, `\\` and the column specification, i.e. findings on
270
+ // markup — and for a rule someone turns up to `error`, a false positive is worse than a miss.
271
+ const floatStart = CLEAN_SRC.indexOf("\\begin{table}");
272
+ const floatEnd = CLEAN_SRC.indexOf("\\end{table}") + "\\end{table}".length;
273
+ const projectedFloat = clean.text.slice(floatStart, floatEnd);
274
+ for (const markup of ["tabular", "lrr", "&", "\\\\", "label", "tab:results"])
275
+ assert.ok(
276
+ !projectedFloat.includes(markup),
277
+ `float markup «${markup}» survived into the prose projection`,
278
+ );
279
+ }
280
+
281
+ {
282
+ // OPAQUE MACROS: name AND argument are blanked. A `\cite` key or a `\label` is not English.
283
+ for (const token of ["someone2020", "sec:intro", "acmart", "sigconf"])
284
+ assert.ok(
285
+ !clean.text.includes(token),
286
+ `opaque macro content «${token}» leaked into the prose projection`,
287
+ );
288
+ assert.ok(
289
+ CLEAN_SRC.includes("\\cite{someone2020}") &&
290
+ CLEAN_SRC.includes("\\label{sec:intro}"),
291
+ "the fixture lost its opaque macros — the assertion above no longer has a subject",
292
+ );
293
+ }
294
+
295
+ {
296
+ // MATH IS BLANKED — inline and display alike. A formula is not a sentence, and a word counter
297
+ // that eats `E = m c^2` reports prose that was never written.
298
+ assert.ok(
299
+ !clean.text.includes("m c^2"),
300
+ "display math leaked into the prose projection",
301
+ );
302
+ assert.ok(
303
+ !clean.text.includes("n = 10"),
304
+ "inline math leaked into the prose projection",
305
+ );
306
+ assert.ok(
307
+ CLEAN_SRC.includes("E = m c^2") && CLEAN_SRC.includes("$n = 10$"),
308
+ "the fixture lost its math — the assertion above no longer has a subject",
309
+ );
310
+ }
311
+
312
+ {
313
+ // `\texttt{X}` → `` `X` ``: the same character count, so offsets do not move. It is the only
314
+ // markdown markup the projection can reproduce without shifting — `**` needs TWO characters
315
+ // after the content, and LaTeX leaves only `}`.
316
+ assert.ok(
317
+ clean.text.includes("`shadow-budget`"),
318
+ "`\\texttt{X}` no longer projects to a backticked span",
319
+ );
320
+ }
321
+
322
+ {
323
+ // 🔴 A BOLD LEAD-IN AT COLUMN 1 becomes a `strong` node, and its `**` are written IN PLACE OF
324
+ // `\te` rather than before the content. A downstream block-note rule locates a lead-in by the
325
+ // OFFSET of the line's first non-space character; leave spaces at the start of the line and
326
+ // the note above the block never binds, so those rules go silent.
327
+ const src = [
328
+ "\\documentclass{article}",
329
+ "\\begin{document}",
330
+ "\\section{Limitations}",
331
+ "\\textbf{Sample size.} Ten files is not a corpus and we do not pretend it is.",
332
+ "\\end{document}",
333
+ "",
334
+ ].join("\n");
335
+ const { text, root } = project("strong-lead", src);
336
+ const strong = root.children.filter((c) => c.type === "strong");
337
+ assert.equal(
338
+ strong.length,
339
+ 1,
340
+ "a `\\textbf{}` in column 1 did not become a strong node",
341
+ );
342
+ assert.equal(strong[0].children[0].value, "Sample size.");
343
+ assert.ok(
344
+ text.split("\n").some((l) => l.startsWith("**")),
345
+ "the `**` were not written at the start of the line — a note above the block cannot bind",
346
+ );
347
+ }
348
+
349
+ {
350
+ // 🔴 COMMENTS, AND BOTH OF THEIR EDGES. unified-latex includes the TRAILING newline in a
351
+ // comment while mdast does not; and a comment that follows a line of text directly comes back
352
+ // with a position starting at the PREVIOUS newline rather than at `%`. Consumers recognise a
353
+ // comment by its first character, so before both edges were trimmed such a node was not a
354
+ // comment to them at all — notes above bold lead-ins never bound, and the rules reading them
355
+ // could NEVER fire. Measured at the time: 2 out of 2 blocks were reported as "no note at all"
356
+ // while the notes were right there.
357
+ const src = [
358
+ "\\documentclass{article}",
359
+ "\\begin{document}",
360
+ "\\section{Limitations}",
361
+ "A line of ordinary text.",
362
+ "% a note bound to the block below it",
363
+ "\\textbf{Sample size.} Ten files is not a corpus.",
364
+ "\\end{document}",
365
+ "",
366
+ ].join("\n");
367
+ const { root } = project("comment-edges", src);
368
+ const html = root.children.filter((c) => c.type === "html");
369
+ assert.equal(html.length, 1, "the LaTeX comment did not become an html node");
370
+ const { start, end } = html[0].position;
371
+ assert.equal(
372
+ src[start.offset],
373
+ "%",
374
+ "the comment node does not start at `%` — its LEFT edge swallowed the previous newline, " +
375
+ "and every consumer that recognises a comment by its first character stops seeing it",
376
+ );
377
+ assert.notEqual(
378
+ src[end.offset - 1],
379
+ "\n",
380
+ "the comment node ends on a newline — its RIGHT edge swallowed the line break, so the gap " +
381
+ "between the note and what it annotates is empty and the note never binds",
382
+ );
383
+ }
384
+
385
+ // ═════════════════════════════════════════════════════════════════════════════
386
+ // III. FIRES ON A PLANTED DEFECT
387
+ // ═════════════════════════════════════════════════════════════════════════════
388
+ // ⚠️ The honest shape of this part after the extraction: the only rule wired to `.tex` in this
389
+ // repository is `tex/future-promise`, and it reads the file from disk rather than the
390
+ // projection. So this run proves the language parses the document and ESLint dispatches through
391
+ // it — the projection's own both-halves are part II, asserted directly.
392
+ {
393
+ const ms = await lintTex(DEFECT_FIXTURE);
394
+ assert.ok(
395
+ ms.length >= 1,
396
+ "fixtures/latex-language/defect.tex produced no findings at all",
397
+ );
398
+ assert.deepEqual(
399
+ ms.map((m) => `${m.line}:${m.text}`),
400
+ ["30:will be released"],
401
+ "the finding set of defect.tex drifted from the frozen measurement",
402
+ );
403
+ // The promise sits inside a `\caption{}`, which is the point: the defect fixture differs from
404
+ // the clean one exactly in the prose the language spent a double lock to make visible.
405
+ assert.ok(
406
+ /\\caption\{[^}]*will be released/.test(DEFECT_SRC),
407
+ "the defect fixture moved its promise out of the caption — it stopped exercising the " +
408
+ "caption path the clean/defect pair was built around",
409
+ );
410
+ assert.ok(
411
+ defect.text.includes("The full table will be released later."),
412
+ "the caption carrying the defect is invisible in the projection",
413
+ );
414
+ }
415
+
416
+ // ═════════════════════════════════════════════════════════════════════════════
417
+ // IV. THE LANGUAGE'S BOUNDARIES — pinned by assertions, not only by prose
418
+ // ═════════════════════════════════════════════════════════════════════════════
419
+ // A known limitation that lives only in a comment silently stops being known: it either gets
420
+ // "fixed" unnoticed, or gets quoted as current a year after it went away. The assertions below
421
+ // describe CURRENT behaviour. Each is an invitation to rewrite the language's header, not a ban
422
+ // on changing the code.
423
+ {
424
+ // BOUNDARY 2: escaped characters are lost together with the macro name.
425
+ const { text } = texToMdast(
426
+ "\\documentclass{article}\n\\begin{document}\n" +
427
+ String.raw`We cut 95\% of it, costing \$5 and A\&B.` +
428
+ "\n\\end{document}\n",
429
+ );
430
+ assert.ok(
431
+ text.includes("We cut 95 of it, costing 5 and A B."),
432
+ "the projection of escaped characters changed — rewrite boundary 2 in the language header:\n" +
433
+ text,
434
+ );
435
+ }
436
+ {
437
+ // BOUNDARY 4: `\input{}` — the contents of the included file are invisible.
438
+ const dir = join(TMP, "input-boundary");
439
+ mkdirSync(join(dir, "sections"), { recursive: true });
440
+ writeFileSync(
441
+ join(dir, "sections/method.tex"),
442
+ "The solver walks the syntax tree.\n",
443
+ );
444
+ const src = [
445
+ "\\documentclass{article}",
446
+ "\\begin{document}",
447
+ "\\section{Method}",
448
+ "\\input{sections/method}",
449
+ "\\end{document}",
450
+ "",
451
+ ].join("\n");
452
+ const { text } = project("input-boundary", src);
453
+ assert.ok(
454
+ !text.includes("solver"),
455
+ "the contents of `\\input{}` became visible — boundary 4 in the language header is stale",
456
+ );
457
+ }
458
+ {
459
+ // BOUNDARY 5: `\appendix` is inexpressible in a length-preserving projection (9 characters
460
+ // against the 11 of `## Appendix`).
461
+ const src = [
462
+ "\\documentclass{article}",
463
+ "\\begin{document}",
464
+ "\\section{Results}",
465
+ "We found four of the ten.",
466
+ "\\appendix",
467
+ "\\section{The full table}",
468
+ "\\end{document}",
469
+ "",
470
+ ].join("\n");
471
+ const { root } = project("appendix-boundary", src);
472
+ const titles = headings(root).map((h) => h.title);
473
+ assert.ok(
474
+ !titles.some((t) => /^Appendix/.test(t)),
475
+ "`\\appendix` started producing a heading — boundary 5 in the language header is stale",
476
+ );
477
+ }
478
+ {
479
+ // 🔴 BOUNDARY 13, found 2026-09-11 BY THIS HARNESS, by an assertion that expected the
480
+ // opposite: `\citep{}` / `\citet{}` leak their key into the prose, while `\cite{}` does not.
481
+ // All three are listed in OPAQUE, but OPAQUE blanks a macro name plus the arguments
482
+ // unified-latex ATTACHED to it, and unified-latex has no signature for the natbib pair — so
483
+ // `{someone2020}` comes back as a separate group node and survives as prose. For a paper
484
+ // using natbib that means every citation key is a word in the prose stream.
485
+ const keyed = (macro) => {
486
+ const src = `\\documentclass{article}\n\\begin{document}\nWe saw it \\${macro}{keyABC} here.\n\\end{document}\n`;
487
+ return project(`opaque-${macro}`, src).text.split("\n")[2];
488
+ };
489
+ assert.ok(
490
+ !keyed("cite").includes("keyABC"),
491
+ "`\\cite{}` started leaking its key",
492
+ );
493
+ assert.ok(
494
+ keyed("citep").includes("keyABC"),
495
+ "`\\citep{}` stopped leaking its key — boundary 13 was fixed; rewrite it in the language " +
496
+ "header instead of leaving a stale limitation on record",
497
+ );
498
+ assert.ok(
499
+ keyed("citet").includes("keyABC"),
500
+ "`\\citet{}` stopped leaking its key — boundary 13 was fixed; rewrite it in the language header",
501
+ );
502
+ }
503
+ {
504
+ // BOUNDARY 8: an inline `% eslint {...}` config does NOT work inside `.tex`
505
+ // (`applyInlineConfig` returns empty), while `% eslint-disable-next-line` DOES.
506
+ // ⚠️ This one runs on an in-process config rather than the repository's, and deliberately:
507
+ // the file has to exist on disk (the rule reads it from disk, and a missing path makes it
508
+ // silent for the wrong reason), and a `.tex` file written inside the repository would also
509
+ // be picked up by the glob assertions in part V.
510
+ const texBuild = (await import(join(HERE, "tex-build.mjs"))).default;
511
+ const local = new ESLint({
512
+ cwd: TMP,
513
+ overrideConfigFile: true,
514
+ overrideConfig: [
515
+ {
516
+ files: ["**/*.tex"],
517
+ plugins: {
518
+ tex: { languages: { latex: texLanguage }, rules: texBuild },
519
+ },
520
+ language: "tex/latex",
521
+ rules: { "tex/future-promise": "warn" },
522
+ },
523
+ ],
524
+ });
525
+ const dir = join(TMP, "inline-config");
526
+ mkdirSync(dir, { recursive: true });
527
+ const body = [
528
+ "\\documentclass[sigconf,screen]{acmart}",
529
+ "\\begin{document}",
530
+ "PLACEHOLDER",
531
+ "The harness will be released later.",
532
+ "\\end{document}",
533
+ "",
534
+ ];
535
+ // Both halves: the same line fires without the directive, and is suppressed with it. A
536
+ // suppression test that never saw the finding proves nothing.
537
+ const withoutDirective = join(dir, "without.tex");
538
+ writeFileSync(
539
+ withoutDirective,
540
+ body.filter((l) => l !== "PLACEHOLDER").join("\n"),
541
+ );
542
+ const [bare] = await local.lintFiles([withoutDirective]);
543
+ assert.deepEqual(
544
+ bare.messages.map((m) => m.ruleId),
545
+ ["tex/future-promise"],
546
+ "the un-suppressed control produced no finding — the suppression test below would pass " +
547
+ "for the wrong reason",
548
+ );
549
+ const withDirective = join(dir, "with.tex");
550
+ writeFileSync(
551
+ withDirective,
552
+ body
553
+ .map((l) =>
554
+ l === "PLACEHOLDER"
555
+ ? "% eslint-disable-next-line tex/future-promise -- the boundary-8 assertion needs it"
556
+ : l,
557
+ )
558
+ .join("\n"),
559
+ );
560
+ const [suppressed] = await local.lintFiles([withDirective]);
561
+ assert.deepEqual(
562
+ suppressed.messages.map((m) => m.ruleId),
563
+ [],
564
+ "`% eslint-disable-next-line` stopped working inside `.tex` — boundary 8 is stale",
565
+ );
566
+ }
567
+
568
+ // ═════════════════════════════════════════════════════════════════════════════
569
+ // V. CONFIG — the language is wired, over a NON-EMPTY glob
570
+ // ═════════════════════════════════════════════════════════════════════════════
571
+ {
572
+ const config = (await import(join(ROOT, "eslint.config.mjs"))).default;
573
+ const tex = config.find((b) => b.language === "tex/latex");
574
+ assert.ok(
575
+ tex,
576
+ "eslint.config.mjs declares no block using the `tex/latex` language",
577
+ );
578
+ assert.ok(
579
+ tex.plugins?.tex?.languages?.latex === texLanguage,
580
+ "the `.tex` block no longer wires up `latex-language.mjs` — the language stopped executing",
581
+ );
582
+
583
+ // 🔴 THE HALF WITHOUT WHICH ALL OF THE ABOVE IS DECORATION: the glob must match files that
584
+ // exist. A language wired over an empty glob is never invoked, and the run is green with zero
585
+ // findings — byte-identical to a run that checked everything. See
586
+ // `scripts/rules-see-files.mjs` for the general check; this is the cheap local copy so that
587
+ // this language's own wiring fails in this language's own test.
588
+ const seen = (await real.lintFiles(["."]))
589
+ .map((r) => relative(ROOT, r.filePath))
590
+ .filter((f) => f.endsWith(".tex"));
591
+ assert.ok(
592
+ seen.length >= 4,
593
+ "the `.tex` glob of eslint.config.mjs matched " +
594
+ seen.length +
595
+ " files; the fixtures alone " +
596
+ "are four, so the language was invoked on less than its own test data: " +
597
+ seen.join(", "),
598
+ );
599
+ }