paperlint 3.1.3 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. package/.github/workflows/ci.yml +12 -7
  2. package/CLAUDE.md +3 -3
  3. package/CONTRIBUTING.md +34 -5
  4. package/dist/adapters/banal/index.d.ts.map +1 -1
  5. package/dist/adapters/banal/index.js +9 -5
  6. package/dist/adapters/banal/index.js.map +1 -1
  7. package/dist/adapters/banal/invocation.d.ts +1 -1
  8. package/dist/adapters/node/files.io.d.ts.map +1 -1
  9. package/dist/adapters/node/files.io.js +2 -1
  10. package/dist/adapters/node/files.io.js.map +1 -1
  11. package/dist/adapters/node/process.io.d.ts +0 -5
  12. package/dist/adapters/node/process.io.d.ts.map +1 -1
  13. package/dist/adapters/node/process.io.js +4 -3
  14. package/dist/adapters/node/process.io.js.map +1 -1
  15. package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
  16. package/dist/adapters/pdfjs/fill.js +13 -7
  17. package/dist/adapters/pdfjs/fill.js.map +1 -1
  18. package/dist/adapters/references/index.d.ts.map +1 -1
  19. package/dist/adapters/references/index.js +4 -5
  20. package/dist/adapters/references/index.js.map +1 -1
  21. package/dist/adapters/references/pool.d.ts.map +1 -1
  22. package/dist/adapters/references/pool.js +4 -5
  23. package/dist/adapters/references/pool.js.map +1 -1
  24. package/dist/adapters/references/reach.io.d.ts.map +1 -1
  25. package/dist/adapters/references/reach.io.js +2 -1
  26. package/dist/adapters/references/reach.io.js.map +1 -1
  27. package/dist/build-engine.js +2 -2
  28. package/dist/build-engine.js.map +1 -1
  29. package/dist/build.d.ts.map +1 -1
  30. package/dist/build.js +14 -11
  31. package/dist/build.js.map +1 -1
  32. package/dist/cli.d.ts.map +1 -1
  33. package/dist/cli.js +31 -34
  34. package/dist/cli.js.map +1 -1
  35. package/dist/doctor.d.ts.map +1 -1
  36. package/dist/doctor.js +3 -2
  37. package/dist/doctor.js.map +1 -1
  38. package/dist/domain/lookup-cache.d.ts.map +1 -1
  39. package/dist/domain/lookup-cache.js +10 -8
  40. package/dist/domain/lookup-cache.js.map +1 -1
  41. package/dist/domain/text.d.ts +12 -0
  42. package/dist/domain/text.d.ts.map +1 -1
  43. package/dist/domain/text.js +17 -0
  44. package/dist/domain/text.js.map +1 -1
  45. package/dist/engine.d.ts.map +1 -1
  46. package/dist/engine.js +4 -3
  47. package/dist/engine.js.map +1 -1
  48. package/dist/eslint-rules/bib-reachable-entry.d.ts +22 -0
  49. package/dist/eslint-rules/bib-reachable-entry.d.ts.map +1 -0
  50. package/dist/eslint-rules/bib-reachable-entry.js +75 -0
  51. package/dist/eslint-rules/bib-reachable-entry.js.map +1 -0
  52. package/dist/eslint-rules/install-path-literals.d.ts +80 -0
  53. package/dist/eslint-rules/install-path-literals.d.ts.map +1 -0
  54. package/dist/eslint-rules/install-path-literals.js +105 -0
  55. package/dist/eslint-rules/install-path-literals.js.map +1 -0
  56. package/dist/eslint-rules/latex-language.d.ts +136 -0
  57. package/dist/eslint-rules/latex-language.d.ts.map +1 -0
  58. package/dist/eslint-rules/latex-language.js +639 -0
  59. package/dist/eslint-rules/latex-language.js.map +1 -0
  60. package/dist/eslint-rules/paper-research-question.d.ts +31 -0
  61. package/dist/eslint-rules/paper-research-question.d.ts.map +1 -0
  62. package/dist/eslint-rules/paper-research-question.js +188 -0
  63. package/dist/eslint-rules/paper-research-question.js.map +1 -0
  64. package/dist/eslint-rules/paper-stages.d.ts +83 -0
  65. package/dist/eslint-rules/paper-stages.d.ts.map +1 -0
  66. package/dist/eslint-rules/paper-stages.js +358 -0
  67. package/dist/eslint-rules/paper-stages.js.map +1 -0
  68. package/dist/eslint-rules/paper-typography.d.ts +74 -0
  69. package/dist/eslint-rules/paper-typography.d.ts.map +1 -0
  70. package/dist/eslint-rules/paper-typography.js +404 -0
  71. package/dist/eslint-rules/paper-typography.js.map +1 -0
  72. package/dist/eslint-rules/papers.d.ts +42 -0
  73. package/dist/eslint-rules/papers.d.ts.map +1 -0
  74. package/dist/eslint-rules/papers.js +145 -0
  75. package/dist/eslint-rules/papers.js.map +1 -0
  76. package/dist/eslint-rules/pdf-last-page-balance.d.ts +72 -0
  77. package/dist/eslint-rules/pdf-last-page-balance.d.ts.map +1 -0
  78. package/dist/eslint-rules/pdf-last-page-balance.js +237 -0
  79. package/dist/eslint-rules/pdf-last-page-balance.js.map +1 -0
  80. package/dist/eslint-rules/review-frontmatter.d.ts +57 -0
  81. package/dist/eslint-rules/review-frontmatter.d.ts.map +1 -0
  82. package/dist/eslint-rules/review-frontmatter.js +126 -0
  83. package/dist/eslint-rules/review-frontmatter.js.map +1 -0
  84. package/dist/eslint-rules/review-frontmatter.schema.json +36 -0
  85. package/dist/eslint-rules/rule-context.d.ts +55 -0
  86. package/dist/eslint-rules/rule-context.d.ts.map +1 -0
  87. package/dist/eslint-rules/rule-context.js +9 -0
  88. package/dist/eslint-rules/rule-context.js.map +1 -0
  89. package/dist/eslint-rules/sibling-frontmatter.d.ts +25 -0
  90. package/dist/eslint-rules/sibling-frontmatter.d.ts.map +1 -0
  91. package/dist/eslint-rules/sibling-frontmatter.js +16 -0
  92. package/dist/eslint-rules/sibling-frontmatter.js.map +1 -0
  93. package/dist/eslint-rules/sibling-frontmatter.schema.json +14 -0
  94. package/dist/eslint-rules/temp-root-realpath.d.ts +71 -0
  95. package/dist/eslint-rules/temp-root-realpath.d.ts.map +1 -0
  96. package/dist/eslint-rules/temp-root-realpath.js +139 -0
  97. package/dist/eslint-rules/temp-root-realpath.js.map +1 -0
  98. package/dist/eslint-rules/tex-build.d.ts +35 -0
  99. package/dist/eslint-rules/tex-build.d.ts.map +1 -0
  100. package/dist/eslint-rules/tex-build.js +313 -0
  101. package/dist/eslint-rules/tex-build.js.map +1 -0
  102. package/dist/facts-file.d.ts.map +1 -1
  103. package/dist/facts-file.js +2 -1
  104. package/dist/facts-file.js.map +1 -1
  105. package/dist/hooks-settings.d.ts +1 -1
  106. package/dist/hooks-settings.d.ts.map +1 -1
  107. package/dist/hooks-settings.js +14 -9
  108. package/dist/hooks-settings.js.map +1 -1
  109. package/dist/init.d.ts +5 -5
  110. package/dist/init.d.ts.map +1 -1
  111. package/dist/init.js +15 -7
  112. package/dist/init.js.map +1 -1
  113. package/dist/latex-loop.d.ts +2 -2
  114. package/dist/latex-loop.d.ts.map +1 -1
  115. package/dist/latex-loop.js +2 -2
  116. package/dist/latex-loop.js.map +1 -1
  117. package/dist/lib/agent-cli-version.d.ts +65 -0
  118. package/dist/lib/agent-cli-version.d.ts.map +1 -0
  119. package/dist/lib/agent-cli-version.js +77 -0
  120. package/dist/lib/agent-cli-version.js.map +1 -0
  121. package/dist/lib/check.d.ts +19 -0
  122. package/dist/lib/check.d.ts.map +1 -0
  123. package/dist/lib/check.js +52 -0
  124. package/dist/lib/check.js.map +1 -0
  125. package/dist/lib/markdown.d.ts +180 -0
  126. package/dist/lib/markdown.d.ts.map +1 -0
  127. package/dist/lib/markdown.js +302 -0
  128. package/dist/lib/markdown.js.map +1 -0
  129. package/dist/lib/paper-config.d.ts +84 -0
  130. package/dist/lib/paper-config.d.ts.map +1 -0
  131. package/dist/lib/paper-config.js +142 -0
  132. package/dist/lib/paper-config.js.map +1 -0
  133. package/dist/lib/skill-eval-fixture.d.ts +29 -0
  134. package/dist/lib/skill-eval-fixture.d.ts.map +1 -0
  135. package/dist/lib/skill-eval-fixture.js +62 -0
  136. package/dist/lib/skill-eval-fixture.js.map +1 -0
  137. package/dist/link-skills.d.ts.map +1 -1
  138. package/dist/link-skills.js +5 -4
  139. package/dist/link-skills.js.map +1 -1
  140. package/dist/new-paper.d.ts.map +1 -1
  141. package/dist/new-paper.js +2 -2
  142. package/dist/new-paper.js.map +1 -1
  143. package/dist/paper-settings.d.ts.map +1 -1
  144. package/dist/paper-settings.js +5 -4
  145. package/dist/paper-settings.js.map +1 -1
  146. package/dist/pdf-facts.d.ts.map +1 -1
  147. package/dist/pdf-facts.js +2 -1
  148. package/dist/pdf-facts.js.map +1 -1
  149. package/dist/pdf-geometry.d.ts.map +1 -1
  150. package/dist/pdf-geometry.js +1 -1
  151. package/dist/pdf-geometry.js.map +1 -1
  152. package/dist/presets.d.ts.map +1 -1
  153. package/dist/presets.js +3 -2
  154. package/dist/presets.js.map +1 -1
  155. package/dist/references.d.ts.map +1 -1
  156. package/dist/references.js +11 -6
  157. package/dist/references.js.map +1 -1
  158. package/dist/rules-config.d.ts.map +1 -1
  159. package/dist/rules-config.js +6 -3
  160. package/dist/rules-config.js.map +1 -1
  161. package/dist/structure.js +1 -1
  162. package/dist/structure.js.map +1 -1
  163. package/dist/tex-requirements.d.ts.map +1 -1
  164. package/dist/tex-requirements.js +8 -3
  165. package/dist/tex-requirements.js.map +1 -1
  166. package/dist/toolchain.d.ts.map +1 -1
  167. package/dist/toolchain.js +21 -16
  168. package/dist/toolchain.js.map +1 -1
  169. package/dist/venue-rules.js +1 -1
  170. package/dist/venue-rules.js.map +1 -1
  171. package/docs/e2e.md +7 -7
  172. package/docs/install.md +1 -1
  173. package/docs/prior-art/README.md +1 -1
  174. package/docs/prior-art/package-location.md +5 -5
  175. package/docs/prior-art/repro/README.md +1 -1
  176. package/docs/prior-art/test-tooling.md +2 -2
  177. package/docs/testing.md +11 -8
  178. package/docs/toolchain.md +1 -1
  179. package/eslint-rules/{bib-reachable-entry.mjs → bib-reachable-entry.ts} +4 -3
  180. package/eslint-rules/install-path-literals.harness.mjs +1 -1
  181. package/eslint-rules/{install-path-literals.mjs → install-path-literals.ts} +16 -8
  182. package/eslint-rules/latex-language.harness.mjs +2 -2
  183. package/eslint-rules/latex-language.test.mjs +7 -1
  184. package/eslint-rules/{latex-language.mjs → latex-language.ts} +297 -109
  185. package/eslint-rules/paper-research-question.harness.mjs +3 -3
  186. package/eslint-rules/{paper-research-question.mjs → paper-research-question.ts} +34 -15
  187. package/eslint-rules/paper-stages.harness.mjs +2 -2
  188. package/eslint-rules/paper-stages.test.mjs +8 -2
  189. package/eslint-rules/{paper-stages.mjs → paper-stages.ts} +74 -29
  190. package/eslint-rules/paper-typography.test.mjs +3 -3
  191. package/eslint-rules/{paper-typography.mjs → paper-typography.ts} +132 -54
  192. package/eslint-rules/papers.harness.mjs +2 -2
  193. package/eslint-rules/{papers.mjs → papers.ts} +7 -4
  194. package/eslint-rules/pdf-last-page-balance.harness.mjs +3 -3
  195. package/eslint-rules/{pdf-last-page-balance.mjs → pdf-last-page-balance.ts} +88 -31
  196. package/eslint-rules/review-frontmatter.test.mjs +2 -2
  197. package/eslint-rules/{review-frontmatter.mjs → review-frontmatter.ts} +37 -15
  198. package/eslint-rules/rule-context.ts +62 -0
  199. package/eslint-rules/{sibling-frontmatter.mjs → sibling-frontmatter.ts} +1 -1
  200. package/eslint-rules/temp-root-realpath.harness.mjs +1 -1
  201. package/eslint-rules/{temp-root-realpath.mjs → temp-root-realpath.ts} +19 -6
  202. package/eslint-rules/tex-build.harness.mjs +2 -2
  203. package/eslint-rules/{tex-build.mjs → tex-build.ts} +4 -3
  204. package/eslint.config.mjs +139 -30
  205. package/fixtures/pdf-facts/README.md +2 -2
  206. package/fixtures/real-markdown-paper/{baseline.mjs → baseline.ts} +31 -10
  207. package/hooks/{hooks.harness.mjs → hooks.harness.ts} +51 -34
  208. package/hooks/paper-edit-guard.hook.mjs +1 -1
  209. package/hooks/{paper-edit-guard.test.mjs → paper-edit-guard.test.ts} +3 -7
  210. package/hooks/paper-skills-nudge.hook.mjs +1 -1
  211. package/hooks/{paper-skills-nudge.test.mjs → paper-skills-nudge.test.ts} +2 -6
  212. package/hooks/{paper-status-gates.test.mjs → paper-status-gates.test.ts} +2 -6
  213. package/lib/agent-cli-version.harness.mjs +1 -1
  214. package/lib/{agent-cli-version.mjs → agent-cli-version.ts} +32 -18
  215. package/lib/check.test.ts +14 -9
  216. package/lib/{check.mjs → check.ts} +22 -9
  217. package/lib/markdown.test.mjs +7 -7
  218. package/lib/{markdown.mjs → markdown.ts} +68 -29
  219. package/lib/paper-config.harness.mjs +8 -8
  220. package/lib/{paper-config.mjs → paper-config.ts} +19 -14
  221. package/lib/skill-checks.mjs +1 -1
  222. package/lib/skill-checks.test.mjs +1 -1
  223. package/lib/skill-corpus.mjs +1 -1
  224. package/lib/skill-eval-kit.mjs +1 -1
  225. package/package.json +42 -10
  226. package/scripts/{check.harness.mjs → check.harness.ts} +111 -10
  227. package/scripts/{check.test.mjs → check.test.ts} +13 -10
  228. package/scripts/{check.mjs → check.ts} +116 -42
  229. package/scripts/coverage-config.test.ts +80 -37
  230. package/scripts/eslint-report-guard.test.mjs +1 -1
  231. package/scripts/harness-api.frozen.json +14 -15
  232. package/scripts/harness-api.test.ts +8 -5
  233. package/scripts/layer-legacy-frozen.test.ts +17 -13
  234. package/scripts/{layer-legacy-frozen.mjs → layer-legacy-frozen.ts} +128 -39
  235. package/scripts/release-config.test.ts +12 -4
  236. package/scripts/rules-see-files.test.mjs +1 -1
  237. package/scripts/test.test.ts +78 -0
  238. package/scripts/test.ts +87 -0
  239. package/skills/grade-paper-writing/prose-lint.mjs +1 -1
  240. package/skills/grade-paper-writing/{prose-lint.test.mjs → prose-lint.test.ts} +2 -2
  241. package/skills/paper-pipeline/description-language.eval.mjs +1 -1
  242. package/skills/paper-pipeline/framing-vs-vocabulary.eval.mjs +1 -1
  243. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +1 -1
  244. package/skills/paper-pipeline/pipeline-firing.eval.mjs +1 -1
  245. package/skills/paper-pipeline/pipeline-language.eval.mjs +1 -1
  246. package/skills/paper-pipeline/scripts/README.md +1 -1
  247. package/skills/paper-pipeline/scripts/announce.test.mjs +1 -1
  248. package/skills/paper-pipeline/scripts/artifact-coverage.mjs +1 -1
  249. package/skills/paper-pipeline/scripts/{artifact-coverage.test.mjs → artifact-coverage.test.ts} +2 -2
  250. package/skills/paper-pipeline/scripts/consumer.mjs +1 -5
  251. package/skills/paper-pipeline/scripts/consumer.test.mjs +1 -1
  252. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +1 -1
  253. package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +1 -4
  254. package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +1 -1
  255. package/skills/paper-pipeline/scripts/fixtures/{scorecard.mjs → scorecard.ts} +36 -7
  256. package/skills/paper-pipeline/scripts/{generated-code.test.mjs → generated-code.test.ts} +3 -3
  257. package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +1 -1
  258. package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +2 -2
  259. package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -2
  260. package/skills/paper-pipeline/scripts/pipeline-check.mjs +1 -4
  261. package/skills/paper-pipeline/scripts/population-map.mjs +1 -4
  262. package/skills/paper-pipeline/scripts/population-map.test.mjs +1 -1
  263. package/skills/paper-pipeline/scripts/{round-diff.harness.mjs → round-diff.harness.ts} +45 -18
  264. package/skills/paper-pipeline/scripts/round-diff.mjs +1 -1
  265. package/skills/paper-pipeline/scripts/round-diff.test.mjs +1 -1
  266. package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +1 -1
  267. package/skills/paper-pipeline/scripts/status.test.mjs +1 -1
  268. package/skills/plan-paper-timeline/fixtures/{fake-google-calendar.mjs → fake-google-calendar.ts} +101 -70
  269. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +3 -3
  270. package/skills/render-paper/{extract-pdf-facts.harness.mjs → extract-pdf-facts.harness.ts} +34 -9
  271. package/skills/render-paper/render-paper.harness.mjs +2 -2
  272. package/skills/tighten-paper/structure.mjs +1 -1
  273. package/skills/tighten-paper/{structure.test.mjs → structure.test.ts} +4 -3
  274. package/skills/verify-citations/scripts/{bib-authors.cli.test.mjs → bib-authors.cli.test.ts} +14 -4
  275. package/skills/verify-citations/scripts/bib-authors.d.mts +63 -0
  276. package/skills/verify-citations/scripts/bib-authors.mjs +1 -4
  277. package/skills/verify-citations/scripts/{verify-cites.cli.test.mjs → verify-cites.cli.test.ts} +32 -9
  278. package/skills/verify-citations/scripts/verify-cites.d.mts +58 -0
  279. package/src/CLAUDE.md +1 -1
  280. package/src/adapters/banal/index.test.ts +3 -6
  281. package/src/adapters/banal/index.ts +7 -4
  282. package/src/adapters/banal/invocation.test.ts +3 -1
  283. package/src/adapters/banal/invocation.ts +1 -1
  284. package/src/adapters/banal/output.test.ts +1 -1
  285. package/src/adapters/banal/run.test.ts +3 -3
  286. package/src/adapters/banal/xml.test.ts +1 -1
  287. package/src/adapters/curl/download.test.ts +4 -2
  288. package/src/adapters/node/files.io.ts +2 -2
  289. package/src/adapters/node/files.test.ts +3 -1
  290. package/src/adapters/node/process.io.ts +4 -3
  291. package/src/adapters/node/process.test.ts +1 -1
  292. package/src/adapters/node/workspace.test.ts +3 -1
  293. package/src/adapters/pdfjs/fill.test.ts +2 -2
  294. package/src/adapters/pdfjs/fill.ts +14 -7
  295. package/src/adapters/references/index.test.ts +31 -38
  296. package/src/adapters/references/index.ts +26 -58
  297. package/src/adapters/references/pool.ts +3 -5
  298. package/src/adapters/references/reach.io.ts +2 -1
  299. package/src/build-engine.harness.mjs +1 -1
  300. package/src/build-engine.test.ts +2 -2
  301. package/src/build-engine.ts +2 -2
  302. package/src/build.harness.mjs +2 -2
  303. package/src/build.test.ts +15 -14
  304. package/src/build.ts +28 -19
  305. package/src/cli.harness.mjs +6 -6
  306. package/src/cli.test.ts +13 -6
  307. package/src/cli.ts +39 -37
  308. package/src/doctor.harness.mjs +3 -3
  309. package/src/doctor.ts +3 -2
  310. package/src/domain/lookup-cache.ts +21 -11
  311. package/src/domain/result.test.ts +2 -2
  312. package/src/domain/text.test.ts +32 -0
  313. package/src/domain/text.ts +23 -0
  314. package/src/engine.harness.mjs +2 -2
  315. package/src/engine.ts +4 -3
  316. package/src/facts-file.test.ts +9 -7
  317. package/src/facts-file.ts +2 -1
  318. package/src/{hooks-settings.harness.mjs → hooks-settings.harness.ts} +39 -20
  319. package/src/hooks-settings.ts +20 -9
  320. package/src/init.test.ts +12 -9
  321. package/src/init.ts +22 -12
  322. package/src/{latex-log.harness.mjs → latex-log.harness.ts} +14 -14
  323. package/src/{latex-loop.harness.mjs → latex-loop.harness.ts} +64 -36
  324. package/src/latex-loop.ts +3 -3
  325. package/src/{link-skills.harness.mjs → link-skills.harness.ts} +19 -19
  326. package/src/link-skills.test.ts +3 -3
  327. package/src/link-skills.ts +5 -4
  328. package/src/lint-fix.test.ts +4 -6
  329. package/src/linted-set.test.ts +2 -2
  330. package/src/new-paper.harness.mjs +2 -2
  331. package/src/new-paper.test.ts +8 -10
  332. package/src/new-paper.ts +2 -2
  333. package/src/paper-files.test.ts +3 -5
  334. package/src/paper-settings-commands.test.ts +5 -4
  335. package/src/paper-settings.test.ts +5 -5
  336. package/src/paper-settings.ts +8 -9
  337. package/src/{pdf-facts.harness.mjs → pdf-facts.harness.ts} +9 -8
  338. package/src/pdf-facts.test.ts +10 -11
  339. package/src/pdf-facts.ts +2 -1
  340. package/src/{pdf-geometry.harness.mjs → pdf-geometry.harness.ts} +19 -12
  341. package/src/pdf-geometry.ts +3 -1
  342. package/src/presets.ts +3 -2
  343. package/src/references.test.ts +51 -43
  344. package/src/references.ts +14 -7
  345. package/src/{rules-config.harness.mjs → rules-config.harness.ts} +7 -13
  346. package/src/rules-config.ts +7 -4
  347. package/src/rules-off.test.ts +6 -4
  348. package/src/{structure.harness.mjs → structure.harness.ts} +26 -15
  349. package/src/structure.ts +1 -1
  350. package/src/{tex-requirements.harness.mjs → tex-requirements.harness.ts} +50 -22
  351. package/src/tex-requirements.ts +8 -5
  352. package/src/toolchain.harness.mjs +2 -2
  353. package/src/toolchain.test.ts +1 -1
  354. package/src/toolchain.ts +31 -17
  355. package/src/venue-rules.test.ts +62 -64
  356. package/src/venue-rules.ts +1 -1
  357. package/tsconfig.json +6 -3
  358. package/tsconfig.pkg.json +10 -0
  359. package/lib/check.d.mts +0 -17
  360. package/lib/paper-config.d.mts +0 -31
  361. package/scripts/layer-legacy-frozen.d.mts +0 -36
  362. package/scripts/rules-are-content-only.harness.mjs +0 -105
  363. package/scripts/rules-are-content-only.mjs +0 -148
  364. package/scripts/rules-are-content-only.test.mjs +0 -61
  365. package/scripts/test.mjs +0 -44
  366. package/scripts/test.test.mjs +0 -52
  367. /package/bin/{paperlint.test.mjs → paperlint.test.ts} +0 -0
  368. /package/lib/{skill-eval-fixture.mjs → skill-eval-fixture.ts} +0 -0
  369. /package/skills/grade-paper-writing/__snapshots__/{prose-lint.test.mjs.snap → prose-lint.test.ts.snap} +0 -0
  370. /package/skills/osf-artifact-upload/{osf-artifact-upload.harness.mjs → osf-artifact-upload.harness.ts} +0 -0
  371. /package/skills/tighten-paper/__snapshots__/{structure.test.mjs.snap → structure.test.ts.snap} +0 -0
@@ -0,0 +1,25 @@
1
+ declare const _default: {
2
+ rules: {
3
+ frontmatter: {
4
+ meta: {
5
+ type: string;
6
+ docs: {
7
+ description: string;
8
+ };
9
+ schema: never[];
10
+ messages: {
11
+ malformed: string;
12
+ invalid: string;
13
+ };
14
+ };
15
+ create(context: import("./rule-context.ts").RuleContext): {
16
+ yaml(node: {
17
+ readonly value?: string;
18
+ }): void;
19
+ "root:exit"(root: object): void;
20
+ };
21
+ };
22
+ };
23
+ };
24
+ export default _default;
25
+ //# sourceMappingURL=sibling-frontmatter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sibling-frontmatter.d.ts","sourceRoot":"","sources":["../../eslint-rules/sibling-frontmatter.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;kCAmBgmI,CAAC;;;;;;;AARjmI,wBAOE"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `sibling/frontmatter` — a sibling card (`<paper>/siblings/*.md`, written by the
3
+ * `analyze-sibling-paper` skill) says in its frontmatter how much of the competing paper was READ:
4
+ * `read: full | abstract | none`. A verdict like "not a scoop" means something different when only
5
+ * the abstract was read, and a card that does not say is a finding.
6
+ *
7
+ * The package writes the cards, so it ships the schema: `sibling-frontmatter.schema.json`. Same
8
+ * mechanism as `review/frontmatter`.
9
+ */
10
+ import { frontmatterRule } from "./review-frontmatter.js";
11
+ export default {
12
+ rules: {
13
+ frontmatter: frontmatterRule(new URL("./sibling-frontmatter.schema.json", import.meta.url), "a sibling card's frontmatter records how much of the competing paper was read (`read: full | abstract | none`)"),
14
+ },
15
+ };
16
+ //# sourceMappingURL=sibling-frontmatter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sibling-frontmatter.js","sourceRoot":"","sources":["../../eslint-rules/sibling-frontmatter.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE1D,eAAe;IACb,KAAK,EAAE;QACL,WAAW,EAAE,eAAe,CAC1B,IAAI,GAAG,CAAC,mCAAmC,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAC7D,gHAAgH,CACjH;KACF;CACF,CAAC"}
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "urn:paperlint:sibling-frontmatter",
4
+ "title": "paperlint sibling-card frontmatter",
5
+ "description": "The YAML frontmatter of a sibling card under <paper>/siblings/ (written by the analyze-sibling-paper skill): how much of the competing paper was actually read. A verdict about a paper read only at the abstract is a different claim from one read in full, and the card must say which.",
6
+ "type": "object",
7
+ "required": ["read"],
8
+ "properties": {
9
+ "read": {
10
+ "description": "full: the whole paper was read. abstract: only the abstract (and maybe the figures). none: the card is from citations or a summary alone.",
11
+ "enum": ["full", "abstract", "none"]
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * `local/temp-root-realpath` — a temp root taken from `tmpdir()` that is not resolved to its
3
+ * realpath.
4
+ *
5
+ * ── THE DEFECT THIS RULE WAS WRITTEN FOR (measured from issue #9) ──────────────────
6
+ * On macOS `os.tmpdir()` returns a path under `/var/folders/…`, and `/var` is itself a symlink
7
+ * to `/private/var`. Node resolves a module's `import.meta.url` to the REAL path, while
8
+ * `process.argv[1]` and any path a test composes from the root "as typed" keep the old
9
+ * spelling. The same directory gets two names, and every path comparison starts lying:
10
+ *
11
+ * meta: "file:///private/var/folders/…/probe.mjs"
12
+ * argv: "/var/folders/…/probe.mjs"
13
+ *
14
+ * On `ubuntu-latest` `/tmp` is a real directory, the two spellings coincide, and the same
15
+ * asserts pass. Three harnesses failed on macOS on a CLEAN checkout, while CI only ran Linux,
16
+ * so green there was a statement about Linux and nothing else.
17
+ *
18
+ * 🔴 WHY A RULE, NOT THREE FIXES. The issue's author fixed the three files they named and
19
+ * honestly wrote that they had not audited the rest. FIFTEEN files in this repository set up a
20
+ * root; every next harness sets up a sixteenth. A fix patches today's list, a rule fixes the
21
+ * shape — and, crucially, it is RED ON LINUX, where the defect itself cannot be reproduced.
22
+ * Without it the only guard is a Mac in someone's hands.
23
+ *
24
+ * ── THE PREDICATE, AND WHY IT IS EXACTLY THIS ──────────────────────────────────────
25
+ * Finding: a call to `mkdtempSync` whose ARGUMENTS contain a call to `tmpdir()`, and which is
26
+ * not itself the sole argument of `realpathSync(…)`.
27
+ *
28
+ * ⚠️ `tmpdir()` in the arguments is the load-bearing part, not decoration. A nested root of the
29
+ * shape `mkdtempSync(join(TMP, "repo-"))` is NOT flagged: it inherits its spelling from `TMP`,
30
+ * and `TMP` is caught at its own point of creation. This turns the issue's "resolve each temp
31
+ * root ONCE at creation" into something checkable character by character rather than by
32
+ * judgment: the rule does not try to guess whether a variable is resolved — it demands that
33
+ * exactly the spot where a platform path enters the program is resolved.
34
+ *
35
+ * ⚠️ WHAT THE RULE DOES NOT CATCH, named out loud so it is not mistaken for more. A root
36
+ * assembled around it (`const t = tmpdir(); const d = mkdtempSync(t + "/x")`) will pass: there
37
+ * is no call to `tmpdir()` in `mkdtempSync`'s arguments. On this corpus the defect has exactly
38
+ * one shape — `mkdtempSync(join(tmpdir(), …))` — and a wider predicate would have to be built
39
+ * on interprocedural analysis, whose cost outweighs the benefit across fifteen sites.
40
+ *
41
+ * 🔴 AND WHAT THE RULE DELIBERATELY LEAVES ALONE: `symlinkSync`. A symlink created BY A TEST
42
+ * inside an already-resolved root is exactly the subject three harnesses test, and resolving
43
+ * the root does not cancel that, it makes it the only symlink in the frame. Before the fix
44
+ * there were two: the one the test placed and the one the platform planted — and the test's
45
+ * assertion was about their sum.
46
+ */
47
+ import type { CallExpression, Node } from "estree";
48
+ import type { RuleContext } from "./rule-context.ts";
49
+ declare const _default: {
50
+ rules: {
51
+ "temp-root-realpath": {
52
+ meta: {
53
+ type: string;
54
+ docs: {
55
+ description: string;
56
+ };
57
+ schema: never[];
58
+ messages: {
59
+ unresolved: string;
60
+ };
61
+ };
62
+ create(context: RuleContext): {
63
+ CallExpression(node: CallExpression & {
64
+ readonly parent?: Node;
65
+ }): void;
66
+ };
67
+ };
68
+ };
69
+ };
70
+ export default _default;
71
+ //# sourceMappingURL=temp-root-realpath.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"temp-root-realpath.d.ts","sourceRoot":"","sources":["../../eslint-rules/temp-root-realpath.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;;;;;;;;;;;;;;4BAgF/B,WAAW;qCAEF,cAAc,GAAG;oBAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,CAAA;iBAAE;;;;;AApB1E,wBA+BE"}
@@ -0,0 +1,139 @@
1
+ /**
2
+ * `local/temp-root-realpath` — a temp root taken from `tmpdir()` that is not resolved to its
3
+ * realpath.
4
+ *
5
+ * ── THE DEFECT THIS RULE WAS WRITTEN FOR (measured from issue #9) ──────────────────
6
+ * On macOS `os.tmpdir()` returns a path under `/var/folders/…`, and `/var` is itself a symlink
7
+ * to `/private/var`. Node resolves a module's `import.meta.url` to the REAL path, while
8
+ * `process.argv[1]` and any path a test composes from the root "as typed" keep the old
9
+ * spelling. The same directory gets two names, and every path comparison starts lying:
10
+ *
11
+ * meta: "file:///private/var/folders/…/probe.mjs"
12
+ * argv: "/var/folders/…/probe.mjs"
13
+ *
14
+ * On `ubuntu-latest` `/tmp` is a real directory, the two spellings coincide, and the same
15
+ * asserts pass. Three harnesses failed on macOS on a CLEAN checkout, while CI only ran Linux,
16
+ * so green there was a statement about Linux and nothing else.
17
+ *
18
+ * 🔴 WHY A RULE, NOT THREE FIXES. The issue's author fixed the three files they named and
19
+ * honestly wrote that they had not audited the rest. FIFTEEN files in this repository set up a
20
+ * root; every next harness sets up a sixteenth. A fix patches today's list, a rule fixes the
21
+ * shape — and, crucially, it is RED ON LINUX, where the defect itself cannot be reproduced.
22
+ * Without it the only guard is a Mac in someone's hands.
23
+ *
24
+ * ── THE PREDICATE, AND WHY IT IS EXACTLY THIS ──────────────────────────────────────
25
+ * Finding: a call to `mkdtempSync` whose ARGUMENTS contain a call to `tmpdir()`, and which is
26
+ * not itself the sole argument of `realpathSync(…)`.
27
+ *
28
+ * ⚠️ `tmpdir()` in the arguments is the load-bearing part, not decoration. A nested root of the
29
+ * shape `mkdtempSync(join(TMP, "repo-"))` is NOT flagged: it inherits its spelling from `TMP`,
30
+ * and `TMP` is caught at its own point of creation. This turns the issue's "resolve each temp
31
+ * root ONCE at creation" into something checkable character by character rather than by
32
+ * judgment: the rule does not try to guess whether a variable is resolved — it demands that
33
+ * exactly the spot where a platform path enters the program is resolved.
34
+ *
35
+ * ⚠️ WHAT THE RULE DOES NOT CATCH, named out loud so it is not mistaken for more. A root
36
+ * assembled around it (`const t = tmpdir(); const d = mkdtempSync(t + "/x")`) will pass: there
37
+ * is no call to `tmpdir()` in `mkdtempSync`'s arguments. On this corpus the defect has exactly
38
+ * one shape — `mkdtempSync(join(tmpdir(), …))` — and a wider predicate would have to be built
39
+ * on interprocedural analysis, whose cost outweighs the benefit across fifteen sites.
40
+ *
41
+ * 🔴 AND WHAT THE RULE DELIBERATELY LEAVES ALONE: `symlinkSync`. A symlink created BY A TEST
42
+ * inside an already-resolved root is exactly the subject three harnesses test, and resolving
43
+ * the root does not cancel that, it makes it the only symlink in the frame. Before the fix
44
+ * there were two: the one the test placed and the one the platform planted — and the test's
45
+ * assertion was about their sum.
46
+ */
47
+ /**
48
+ * Whether `value` is an ESTree node: an object whose `type` is a string. The walk below reaches
49
+ * every property of a node, and most of them (`regex`, `value`, arrays of flags) are not nodes.
50
+ */
51
+ const isNode = (value) => "type" in value && typeof value.type === "string";
52
+ /** The called function's name: `f()` and `mod.f()` count as the same name for our purposes. */
53
+ const calleeName = (node) => {
54
+ if (node?.type !== "CallExpression")
55
+ return "";
56
+ const c = node.callee;
57
+ if (c.type === "Identifier")
58
+ return c.name;
59
+ if (c.type === "MemberExpression" &&
60
+ !c.computed &&
61
+ c.property.type === "Identifier")
62
+ return c.property.name;
63
+ return "";
64
+ };
65
+ /**
66
+ * Whether the subtree contains a call to `tmpdir()`. A walk over nodes, not a substring
67
+ * search: the word `tmpdir` in a comment or a string literal nearby is not a call, and a
68
+ * text-based guard would be forced to exclude itself — exactly the class of check string-based
69
+ * checks are banned for in this repository.
70
+ */
71
+ const mentionsTmpdir = (node) => {
72
+ if (node === null || typeof node !== "object")
73
+ return false;
74
+ if (Array.isArray(node))
75
+ return node.some(mentionsTmpdir);
76
+ if (!isNode(node))
77
+ return false;
78
+ if (calleeName(node) === "tmpdir")
79
+ return true;
80
+ for (const [key, value] of Object.entries(node)) {
81
+ if (key === "parent" || key === "loc" || key === "range")
82
+ continue;
83
+ if (value !== null && typeof value === "object" && mentionsTmpdir(value))
84
+ return true;
85
+ }
86
+ return false;
87
+ };
88
+ /** `realpathSync(x)` and `realpathSync.native(x)` — both resolve the path, both count. */
89
+ const isRealpathCall = (node) => {
90
+ if (node?.type !== "CallExpression")
91
+ return false;
92
+ const name = calleeName(node);
93
+ if (name === "realpathSync")
94
+ return true;
95
+ // `realpathSync.native(…)`: the call's name is `native`, and the object is `realpathSync` itself.
96
+ if (name !== "native")
97
+ return false;
98
+ const obj = node.callee.type === "MemberExpression" ? node.callee.object : null;
99
+ if (obj?.type === "Identifier")
100
+ return obj.name === "realpathSync";
101
+ return (obj?.type === "MemberExpression" &&
102
+ !obj.computed &&
103
+ "name" in obj.property &&
104
+ obj.property.name === "realpathSync");
105
+ };
106
+ export default {
107
+ rules: {
108
+ "temp-root-realpath": {
109
+ meta: {
110
+ type: "problem",
111
+ docs: {
112
+ description: "a temp root taken from tmpdir() is resolved to its realpath at the moment it is created",
113
+ },
114
+ schema: [],
115
+ messages: {
116
+ unresolved: "temp root from tmpdir() is not resolved: wrap it as `realpathSync(mkdtempSync(join(tmpdir(), …)))`. " +
117
+ "On macOS /var is a symlink to /private/var, so Node resolves import.meta.url to the realpath " +
118
+ "while process.argv[1] and any path this file composes keep the spelling as typed — the same " +
119
+ "directory under two names, and every path comparison built on it silently compares the two.",
120
+ },
121
+ },
122
+ create(context) {
123
+ return {
124
+ CallExpression(node) {
125
+ if (calleeName(node) !== "mkdtempSync")
126
+ return;
127
+ if (!mentionsTmpdir(node.arguments))
128
+ return;
129
+ const parent = node.parent;
130
+ if (isRealpathCall(parent) && parent.arguments[0] === node)
131
+ return;
132
+ context.report({ node, messageId: "unresolved" });
133
+ },
134
+ };
135
+ },
136
+ },
137
+ },
138
+ };
139
+ //# sourceMappingURL=temp-root-realpath.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"temp-root-realpath.js","sourceRoot":"","sources":["../../eslint-rules/temp-root-realpath.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import type { RuleContext } from "./rule-context.ts";
2
+ declare const _default: {
3
+ "future-promise": {
4
+ meta: {
5
+ type: string;
6
+ docs: {
7
+ description: string;
8
+ };
9
+ schema: never[];
10
+ messages: {
11
+ promise: string;
12
+ };
13
+ };
14
+ create(context: RuleContext): {
15
+ root(): void;
16
+ };
17
+ };
18
+ "acm-frontmatter-override": {
19
+ meta: {
20
+ type: string;
21
+ docs: {
22
+ description: string;
23
+ };
24
+ schema: never[];
25
+ messages: {
26
+ override: string;
27
+ };
28
+ };
29
+ create(context: RuleContext): {
30
+ root(): void;
31
+ };
32
+ };
33
+ };
34
+ export default _default;
35
+ //# sourceMappingURL=tex-build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tex-build.d.ts","sourceRoot":"","sources":["../../eslint-rules/tex-build.ts"],"names":[],"mappings":"AAoDA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;;;;;;;;;;;;;wBAuGnC,WAAW;;;;;;;;;;;;;;;wBA2GX,WAAW;;;;;AAyD7B,wBAGE"}
@@ -0,0 +1,313 @@
1
+ /**
2
+ * tex-build — rules about the BUILT LaTeX paper: about what the preamble declares, not about
3
+ * how the prose is written.
4
+ *
5
+ * ── WHY ITS OWN NAMESPACE, AND NOT `paper/*` ────────────────────────────────
6
+ * In the source corpus the `paper/*` family is declared on BOTH `paper.md` and `paper.tex`,
7
+ * and that parity is pinned by an assertion: "a rule added for markdown must not silently
8
+ * fail to reach LaTeX (or the other way round)". The subject here is the BUILD MODE
9
+ * (`\documentclass[review]`, `printacmref=false`), and in markdown it does not exist: a draft
10
+ * has no mechanical sign saying "this build is the final one". Writing
11
+ * `paper/future-promise: "off"` on markdown would be an exemption justified by "there is no
12
+ * subject", and that same parity assertion requires the rule to be ALIVE on the other side —
13
+ * i.e. the machinery exists but is built for the opposite direction (off on `.tex`, alive on
14
+ * `.md`).
15
+ *
16
+ * Hence a namespace of its own, the same way an external word-counter gets one: it has a word
17
+ * count markdown does not have, we have a preamble markdown does not have. Where the config
18
+ * enumerates non-`paper/*` rules on `.tex`, the list is CLOSED and `tex/future-promise` is
19
+ * named explicitly, so that a new namespace arrives by decision rather than as a side effect
20
+ * of editing a config.
21
+ *
22
+ * ⚠️ THE MEASUREMENT THIS DECISION RESTS ON (source corpus, 2026-09-06 → 2026-09-07): had the
23
+ * rule been declared on `paper.md` it would produce ZERO there TODAY — not one match of
24
+ * `FUTURE_PROMISE_RE` in the markdown draft of the one paper that has one. So the choice is
25
+ * about semantics, not noise: markdown has no input distinguishing a draft from a shipped
26
+ * build, and the rule would fire on a draft's honest promise.
27
+ *
28
+ * ── THE RULE READS THE FILE FROM DISK, NOT THE PROJECTION. This is load-bearing ──
29
+ * The LaTeX language in `latex-language.mjs` hands rules a PROJECTION of `.tex` into a
30
+ * markdown-like text, and the first thing that projection does is blank the whole preamble
31
+ * (`blank(0, preEnd)`) plus `\documentclass` and `\settopmatter` as OPAQUE macros. Both signs
32
+ * of the build mode are therefore ERASED in the projection, and this check is inexpressible
33
+ * over `sourceCode.getText()`. `readFileSync(context.filename)` is the same thing sibling
34
+ * rules in the source corpus do when they read a registry file next to the paper; the only
35
+ * difference is that here the file being read is the linted file itself. The one side effect
36
+ * is named: in an editor with an unsaved buffer the rule judges the saved version. On a CLI
37
+ * run (and in CI) there is no such difference.
38
+ *
39
+ * ── WHAT THE MOVE TO A LINT RULE FIXED BESIDES THE FORM (measured 2026-09-07) ──
40
+ * 🔴 The previous implementation, a script, picked its input like this: look for `paper.md` /
41
+ * `draft.md`, and failing that take `readdirSync(dir).find((f) => f.endsWith(".tex"))` — the
42
+ * FIRST `.tex` in directory order. Measured across the three papers of the source corpus:
43
+ * paper A → a 536-BYTE file holding nine `\def`s with venue numbers. The check, added
44
+ * specifically BECAUSE of paper A, was not reading paper A;
45
+ * paper B → a four-month-old draft, one of SEVEN `.tex` files in that directory (the real
46
+ * `paper.tex` happened to get the same verdict — "review mode" — so the set of
47
+ * findings matched by accident, not by construction);
48
+ * paper C → `paper.tex`, the only `.tex` there; it matched.
49
+ * An ESLint rule runs over the glob declared in the config, so the address stopped depending
50
+ * on `readdirSync` order and on how many drafts happen to lie next to the paper.
51
+ */
52
+ import { readFileSync } from "node:fs";
53
+ /**
54
+ * A copy rather than an import, for the same reason the prose lexicons are copies: a rule may
55
+ * not depend on the script it is replacing, or removing the check from that script becomes
56
+ * impossible.
57
+ */
58
+ const FUTURE_PROMISE_RE = /\b(?:at (?:the )?camera[-\s]?ready|upon acceptance|on acceptance|will be (?:released|published|made (?:publicly )?available|public|open[-\s]?sourced))\b/i;
59
+ /** The same two signs a render script uses to tell a review build from a camera-ready one. */
60
+ const REVIEW_MODE_RE = /^[^%\n]*(?:\\documentclass\[[^\]]*\breview\b|printacmref=false)/m;
61
+ /** Is this an `acmart` build at all? Outside acmart none of the macros below mean anything. */
62
+ const ACMART_RE = /\\documentclass(?:\s*\[[^\]]*\])?\s*\{acmart\}/;
63
+ /**
64
+ * acmart's OWN escape hatch for "this is not going to an ACM venue": in `nonacm` mode the
65
+ * class disables the ACM Reference Format itself (`acmart.cls:110-121`, "in 'nonacm' mode we
66
+ * disable the \"ACM Reference Format\""). A build that declares it is not an ACM submission
67
+ * is exempt — that is the supported way to say so, and it is a class option rather than a
68
+ * hand-rolled blanking of the front matter.
69
+ */
70
+ const NONACM_RE = /\\documentclass\s*\[[^\]]*\bnonacm\b/;
71
+ /**
72
+ * The overrides whose ENTIRE effect is to remove furniture the template puts on page 1.
73
+ * Each entry names what disappears, because a message that only says "do not do this" gets
74
+ * read as style advice.
75
+ */
76
+ const FRONTMATTER_OVERRIDES = [
77
+ {
78
+ re: /\\setcopyright\s*\{\s*none\s*\}/g,
79
+ removes: "the copyright statement (acmart.cls:1857, copyright mode 0)",
80
+ },
81
+ {
82
+ re: /\\renewcommand\s*\*?\s*\{?\s*\\footnotetextcopyrightpermission/g,
83
+ removes: "the permission/copyright footnote block on page 1 (acmart.cls:2180)",
84
+ },
85
+ {
86
+ re: /\\(?:this)?pagestyle\s*\{\s*(?:plain|empty)\s*\}/g,
87
+ removes: "acmart's own page style, i.e. the running heads and folios it restores at " +
88
+ "\\begin{document} (acmart.cls:2876-2881)",
89
+ },
90
+ ];
91
+ // ═════════════════════════════════════════════════════════════════════════════
92
+ // future-promise — a shipped build promises what it has already handed over
93
+ // ═════════════════════════════════════════════════════════════════════════════
94
+ // 🔴 Added 2026-08-24, because it would have caught a real blocker hours earlier. An accepted
95
+ // paper printed "(full harness at camera-ready)" in BOTH of its most-read positions — the end
96
+ // of the abstract and the end of the conclusion — while its own Availability paragraph three
97
+ // lines below said the harness was already public. The document contradicted itself two
98
+ // against one, and the losing side was the abstract: exactly the part a digital library
99
+ // indexes and the only part most people read.
100
+ //
101
+ // Worse than a typo: a reviewer had listed it as a weakness — "The experimental harness is not
102
+ // yet available (it is expected for the camera-ready version)" — so the printed camera-ready
103
+ // kept asserting the very thing the reviewer complained about.
104
+ //
105
+ // ⚠️ Here is why it survived, and it generalises. The promise WAS swept out the same day — out
106
+ // of the ARTIFACT (`README:34`, `reproduce.py:166`). Nobody made the same pass over
107
+ // `paper.tex`. One class of defect, two carriers, one of them cleaned. A human pass goes where
108
+ // the human is looking; this check goes to both places, because it does not know where anyone
109
+ // is looking.
110
+ //
111
+ // The scope is DELIBERATELY NARROW, because a noisy gate is a silenced gate:
112
+ // - `.tex` only (see the file header: markdown has no sign of "this build is final");
113
+ // - a review-mode build is EXEMPT, and that is the whole point: during peer review
114
+ // "at camera-ready" is a true and ordinary promise. The defect is only a promise that
115
+ // outlived its own delivery;
116
+ // - LaTeX comments are blanked FIRST: author notes of the form `% camera-ready blocker`
117
+ // legitimately live in the source, and a finding on those would be precisely the false
118
+ // positive that kills a check.
119
+ //
120
+ // SEVERITY is `warn`, and that is an analysis rather than caution. The finding is NOT binary:
121
+ // a promise in a camera-ready is sometimes honest (something genuinely not released yet), and
122
+ // the verdict "does it contradict the Availability paragraph" is made by a human — which is
123
+ // what the message text says. There is also a demonstrable class of false positives: "their
124
+ // replication will be published in 2027" is a sentence about SOMEONE ELSE's work. An `error`
125
+ // that fails on a correct input gets switched off the same day, and after that the binary
126
+ // checks stop being read too.
127
+ const futurePromise = {
128
+ meta: {
129
+ type: "problem",
130
+ docs: {
131
+ description: "the build is not in review mode, yet the text promises a future release of something already handed over",
132
+ },
133
+ schema: [],
134
+ messages: {
135
+ promise: "this build is NOT in review mode, but the text promises a future release: " +
136
+ "«{{text}}». Check it against Availability: if the thing is already published, the " +
137
+ "paper contradicts itself, and the abstract is the side that loses — the abstract is " +
138
+ "what the digital library indexes. That is exactly how «(full harness at " +
139
+ "camera-ready)» survived a sweep that went through the artifact and never went " +
140
+ "through paper.tex",
141
+ },
142
+ },
143
+ create(context) {
144
+ return {
145
+ root() {
146
+ // 🔴 ONE GUARD, NOT THREE. The first draft had `if (!file || !existsSync(file))`
147
+ // BEFORE the read — removing that line on 2026-09-07 showed it is caught by NOTHING:
148
+ // the `try/catch` around the read covers exactly the same cases (no path, a `<text>` run
149
+ // through stdin, no permission). Two guards doing one job is a guard the harness can
150
+ // never fail on, i.e. a dead assertion dressed as robustness.
151
+ // What is left is the read inside `try`: stay quiet rather than judge content we never
152
+ // saw.
153
+ let text;
154
+ try {
155
+ text = readFileSync(context.filename, "utf8");
156
+ }
157
+ catch {
158
+ return;
159
+ }
160
+ if (REVIEW_MODE_RE.test(text))
161
+ return;
162
+ // Comments are blanked by CUTTING THE TAIL of the line, exactly as the previous
163
+ // implementation did. Columns do not shift: a match is only possible to the LEFT of
164
+ // `%`, and the left part of the line stays in place character for character. An
165
+ // escaped `\%` is not a comment.
166
+ const lines = text
167
+ .split("\n")
168
+ .map((l) => l.replace(/(^|[^\\])%.*$/, "$1"));
169
+ lines.forEach((line, i) => {
170
+ const m = FUTURE_PROMISE_RE.exec(line);
171
+ if (!m)
172
+ return;
173
+ const column = m.index + 1;
174
+ context.report({
175
+ loc: {
176
+ start: { line: i + 1, column },
177
+ end: { line: i + 1, column: column + m[0].length },
178
+ },
179
+ messageId: "promise",
180
+ data: { text: m[0] },
181
+ });
182
+ });
183
+ },
184
+ };
185
+ },
186
+ };
187
+ // ══════════════════════════════════════════════════════════════════════════════
188
+ // acm-frontmatter-override — the build blanks template furniture off page 1
189
+ // ══════════════════════════════════════════════════════════════════════════════
190
+ // 🔴 Added 2026-09-11, from a DESK REJECT. A submission was rejected before any content
191
+ // review with the reason "substantially deviating from the specified template… front page
192
+ // lacks elements included in the template". Its preamble carried three lines whose entire
193
+ // effect is to take elements off the front page:
194
+ //
195
+ // \setcopyright{none}
196
+ // \renewcommand\footnotetextcopyrightpermission[1]{}
197
+ // \pagestyle{plain}
198
+ //
199
+ // The same three lines sit COMMENTED OUT in a sibling paper of the same corpus — the one that
200
+ // was accepted. The safer pattern was already known in the same directory and was not carried
201
+ // over. That is the shape this check exists for: a defect that is invisible in the source,
202
+ // visible only on the rendered first page, and fatal before anyone reads a word.
203
+ //
204
+ // ⚠️ THE RULE IS NOT CONDITIONED ON REVIEW MODE, AND THAT IS A MEASUREMENT, NOT A CHOICE.
205
+ // The obvious framing — "flag these when `\documentclass[review]`, because review mode already
206
+ // handles the front matter" — does not survive reading `acmart.cls`. The `review` option does
207
+ // exactly two things (`acmart.cls:93-99`): it turns on line numbers and sets
208
+ // `\@ACM@printfoliostrue`. It does not touch the copyright statement, the permission footnote
209
+ // or the page style. So the overrides are not fighting review mode; they strip the same
210
+ // furniture in EVERY mode — and stripping it in the camera-ready is worse, because that is
211
+ // the version the digital library keeps.
212
+ //
213
+ // The guards are therefore about the CLASS, not the stage:
214
+ // - `acmart` only. Outside it `\pagestyle{plain}` is an ordinary, correct line;
215
+ // - `nonacm` exempts, because that is acmart's own supported way to say "not an ACM venue",
216
+ // and in that mode the class disables the ACM Reference Format itself (`acmart.cls:110`).
217
+ //
218
+ // 🔴 `printacmref=false` IS DELIBERATELY NOT IN THE SET, though it was in the rejected preamble
219
+ // and it too removes a block from page 1. Two reasons, and the first is structural: the
220
+ // sibling rule in this very file reads `printacmref=false` as a SIGN OF REVIEW MODE
221
+ // (`REVIEW_MODE_RE`). A module where one rule treats a token as a legitimate marker and
222
+ // another calls the same token a defect is a module that contradicts itself, and the reader
223
+ // cannot tell which half to believe. The second is that ACM sanctions it for preprints. If it
224
+ // ever needs catching, it needs catching together with a decision about `REVIEW_MODE_RE` —
225
+ // not as a fourth line in this array.
226
+ //
227
+ // SEVERITY is for the CONSUMER to set, and the two configs differ on purpose: this repository
228
+ // lints fixtures that are broken by construction, a consumer lints a real paper. The finding
229
+ // is binary — the macro is present or it is not — it has a named exemption, and the cost of a
230
+ // miss is a rejection with no content review. In a consumer that is `error`.
231
+ const acmFrontmatterOverride = {
232
+ meta: {
233
+ type: "problem",
234
+ docs: {
235
+ description: "an acmart build overrides ACM's front-matter commands, removing template elements from page 1",
236
+ },
237
+ schema: [],
238
+ messages: {
239
+ override: "«{{text}}» removes {{removes}} from the front page of an `acmart` build. A submission " +
240
+ "was desk-rejected for exactly this — «substantially deviating from the specified " +
241
+ "template… front page lacks elements included in the template» — before any content " +
242
+ "review happened. The `review` option does not suppress this furniture itself " +
243
+ "(acmart.cls:93-99 turns on line numbers and folios, nothing else), so the line is not " +
244
+ "compensating for the class: it is fighting the template. Delete it. If this paper " +
245
+ "genuinely is not going to an ACM venue, say so the supported way — " +
246
+ "`\\documentclass[…,nonacm]{acmart}` — and the class drops the ACM Reference Format " +
247
+ "on its own",
248
+ },
249
+ },
250
+ create(context) {
251
+ return {
252
+ root() {
253
+ // Same single guard as the sibling rule, and for the same measured reason: a separate
254
+ // `existsSync` before the read is covered by this `catch` in every case it could
255
+ // fire, i.e. it is an assertion the harness can never fail on.
256
+ let text;
257
+ try {
258
+ text = readFileSync(context.filename, "utf8");
259
+ }
260
+ catch {
261
+ return;
262
+ }
263
+ // Comments are blanked BEFORE the guards, not only before the search — unlike the
264
+ // sibling rule, which carries the `^[^%\n]*` prefix inside each pattern instead.
265
+ // It matters here: the accepted sibling paper of the source corpus keeps this exact
266
+ // block commented out as a record of what it used to do, and a `\documentclass` line
267
+ // quoted inside a comment must not arm the rule any more than a commented
268
+ // `\setcopyright{none}` must trip it. Columns do not shift: a match is only possible
269
+ // to the LEFT of `%`, and the left part of the line survives character for character.
270
+ // An escaped `\%` is not a comment.
271
+ const lines = text
272
+ .split("\n")
273
+ .map((l) => l.replace(/(^|[^\\])%.*$/, "$1"));
274
+ const code = lines.join("\n");
275
+ if (!ACMART_RE.test(code))
276
+ return;
277
+ if (NONACM_RE.test(code))
278
+ return;
279
+ lines.forEach((line, i) => {
280
+ for (const { re, removes } of FRONTMATTER_OVERRIDES) {
281
+ // 🔴 NO `re.lastIndex = 0` HERE, AND THAT IS A MEASUREMENT. These regexes are
282
+ // module-level and `/g`, so they carry `lastIndex` across files, and the obvious
283
+ // defensive line is to reset it. Removing that line on 2026-09-11 left the harness
284
+ // GREEN — because the loop below always drains the regex, and a `/g` `exec` that
285
+ // returns `null` resets `lastIndex` to 0 by itself. The reset was a guard the
286
+ // harness could never fail on, i.e. the same dead assertion dressed as robustness
287
+ // that the sibling rule's single-`try` comment describes. What keeps the property
288
+ // honest instead is an ASSERTION, not a line of code: the harness lints the same
289
+ // input twice in one process and requires the two runs to agree. Add a `break` to
290
+ // this loop and that assertion is what will fail.
291
+ let m;
292
+ while ((m = re.exec(line)) !== null) {
293
+ const column = m.index + 1;
294
+ context.report({
295
+ loc: {
296
+ start: { line: i + 1, column },
297
+ end: { line: i + 1, column: column + m[0].length },
298
+ },
299
+ messageId: "override",
300
+ data: { text: m[0], removes },
301
+ });
302
+ }
303
+ }
304
+ });
305
+ },
306
+ };
307
+ },
308
+ };
309
+ export default {
310
+ "future-promise": futurePromise,
311
+ "acm-frontmatter-override": acmFrontmatterOverride,
312
+ };
313
+ //# sourceMappingURL=tex-build.js.map