paperlint 4.2.0 → 6.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 (251) hide show
  1. package/CLAUDE.md +15 -2
  2. package/CONTRIBUTING.md +3 -0
  3. package/README.md +23 -12
  4. package/action.harness.mjs +54 -36
  5. package/action.yml +12 -27
  6. package/bin/paperlint.mjs +5 -1
  7. package/dist/adapters/latex/includes.d.ts +5 -1
  8. package/dist/adapters/latex/includes.d.ts.map +1 -1
  9. package/dist/adapters/latex/includes.js +8 -2
  10. package/dist/adapters/latex/includes.js.map +1 -1
  11. package/dist/adapters/latex/index.d.ts +1 -0
  12. package/dist/adapters/latex/index.d.ts.map +1 -1
  13. package/dist/adapters/latex/index.js +3 -0
  14. package/dist/adapters/latex/index.js.map +1 -1
  15. package/dist/adapters/latex/layout.d.ts +27 -0
  16. package/dist/adapters/latex/layout.d.ts.map +1 -0
  17. package/dist/adapters/latex/layout.js +62 -0
  18. package/dist/adapters/latex/layout.js.map +1 -0
  19. package/dist/adapters/latex/nodes.d.ts.map +1 -1
  20. package/dist/adapters/latex/nodes.js +11 -1
  21. package/dist/adapters/latex/nodes.js.map +1 -1
  22. package/dist/adapters/latex/parse.d.ts.map +1 -1
  23. package/dist/adapters/latex/parse.js +5 -1
  24. package/dist/adapters/latex/parse.js.map +1 -1
  25. package/dist/build.d.ts.map +1 -1
  26. package/dist/build.js +7 -3
  27. package/dist/build.js.map +1 -1
  28. package/dist/cli.d.ts +2 -8
  29. package/dist/cli.d.ts.map +1 -1
  30. package/dist/cli.js +92 -42
  31. package/dist/cli.js.map +1 -1
  32. package/dist/domain/anonymity.d.ts +68 -0
  33. package/dist/domain/anonymity.d.ts.map +1 -0
  34. package/dist/domain/anonymity.js +94 -0
  35. package/dist/domain/anonymity.js.map +1 -0
  36. package/dist/domain/body-pages.d.ts +69 -0
  37. package/dist/domain/body-pages.d.ts.map +1 -0
  38. package/dist/domain/body-pages.js +141 -0
  39. package/dist/domain/body-pages.js.map +1 -0
  40. package/dist/domain/paper-source.js +2 -2
  41. package/dist/domain/tex-document.d.ts +9 -0
  42. package/dist/domain/tex-document.d.ts.map +1 -1
  43. package/dist/eslint-rules/latex-language.d.ts +5 -1
  44. package/dist/eslint-rules/latex-language.d.ts.map +1 -1
  45. package/dist/eslint-rules/latex-language.js +6 -3
  46. package/dist/eslint-rules/latex-language.js.map +1 -1
  47. package/dist/eslint-rules/paper-context.d.ts +20 -0
  48. package/dist/eslint-rules/paper-context.d.ts.map +1 -0
  49. package/dist/eslint-rules/paper-context.js +44 -0
  50. package/dist/eslint-rules/paper-context.js.map +1 -0
  51. package/dist/eslint-rules/paper-typography.d.ts +1 -1
  52. package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
  53. package/dist/eslint-rules/paper-typography.js +32 -18
  54. package/dist/eslint-rules/paper-typography.js.map +1 -1
  55. package/dist/eslint-rules/pdf-last-page-balance.d.ts +1 -1
  56. package/dist/eslint-rules/pdf-last-page-balance.js +2 -2
  57. package/dist/eslint-rules/tex-build.d.ts.map +1 -1
  58. package/dist/eslint-rules/tex-build.js +7 -1
  59. package/dist/eslint-rules/tex-build.js.map +1 -1
  60. package/dist/facts-file.d.ts +28 -2
  61. package/dist/facts-file.d.ts.map +1 -1
  62. package/dist/facts-file.js +54 -9
  63. package/dist/facts-file.js.map +1 -1
  64. package/dist/init.d.ts +4 -19
  65. package/dist/init.d.ts.map +1 -1
  66. package/dist/init.js +24 -29
  67. package/dist/init.js.map +1 -1
  68. package/dist/lib/paper-config.d.ts.map +1 -1
  69. package/dist/lib/paper-config.js +2 -1
  70. package/dist/lib/paper-config.js.map +1 -1
  71. package/dist/package-dirs.d.ts +2 -0
  72. package/dist/package-dirs.d.ts.map +1 -1
  73. package/dist/package-dirs.js +8 -0
  74. package/dist/package-dirs.js.map +1 -1
  75. package/dist/paper-files.d.ts +2 -2
  76. package/dist/paper-includes.d.ts +17 -0
  77. package/dist/paper-includes.d.ts.map +1 -0
  78. package/dist/paper-includes.js +51 -0
  79. package/dist/paper-includes.js.map +1 -0
  80. package/dist/paper-settings.d.ts +11 -0
  81. package/dist/paper-settings.d.ts.map +1 -1
  82. package/dist/paper-settings.js +38 -4
  83. package/dist/paper-settings.js.map +1 -1
  84. package/dist/pdf-content.d.ts +40 -0
  85. package/dist/pdf-content.d.ts.map +1 -0
  86. package/dist/pdf-content.js +97 -0
  87. package/dist/pdf-content.js.map +1 -0
  88. package/dist/pdf-facts.d.ts +30 -0
  89. package/dist/pdf-facts.d.ts.map +1 -1
  90. package/dist/pdf-facts.js +37 -1
  91. package/dist/pdf-facts.js.map +1 -1
  92. package/dist/ports/latex.d.ts +3 -1
  93. package/dist/ports/latex.d.ts.map +1 -1
  94. package/dist/presets.d.ts +2 -0
  95. package/dist/presets.d.ts.map +1 -1
  96. package/dist/presets.js +4 -0
  97. package/dist/presets.js.map +1 -1
  98. package/dist/rules-config.d.ts +1 -0
  99. package/dist/rules-config.d.ts.map +1 -1
  100. package/dist/rules-config.js.map +1 -1
  101. package/dist/structure.js +1 -1
  102. package/dist/structure.js.map +1 -1
  103. package/dist/tex-paper.d.ts +17 -3
  104. package/dist/tex-paper.d.ts.map +1 -1
  105. package/dist/tex-paper.js +36 -38
  106. package/dist/tex-paper.js.map +1 -1
  107. package/dist/tex-requirements.d.ts +4 -0
  108. package/dist/tex-requirements.d.ts.map +1 -1
  109. package/dist/tex-requirements.js +3 -0
  110. package/dist/tex-requirements.js.map +1 -1
  111. package/dist/tex-venue-rules.d.ts +7 -1
  112. package/dist/tex-venue-rules.d.ts.map +1 -1
  113. package/dist/tex-venue-rules.js +40 -0
  114. package/dist/tex-venue-rules.js.map +1 -1
  115. package/dist/types.d.ts +2 -0
  116. package/dist/types.d.ts.map +1 -1
  117. package/dist/venue-rules.d.ts +38 -8
  118. package/dist/venue-rules.d.ts.map +1 -1
  119. package/dist/venue-rules.js +179 -41
  120. package/dist/venue-rules.js.map +1 -1
  121. package/docs/configuration.md +23 -20
  122. package/docs/e2e.md +32 -11
  123. package/docs/install.md +8 -8
  124. package/docs/prior-art/paper-folder-scaffolding.md +4 -7
  125. package/docs/rules/anonymity/identity.md +111 -0
  126. package/docs/rules/format/layout-override.md +61 -0
  127. package/docs/rules/format/page-limit.md +108 -0
  128. package/docs/rules/pdf/body-size.md +87 -0
  129. package/docs/rules.md +55 -46
  130. package/eslint-rules/latex-language.ts +10 -4
  131. package/eslint-rules/paper-context.test.ts +47 -0
  132. package/eslint-rules/paper-context.ts +57 -0
  133. package/eslint-rules/paper-typography.ts +67 -23
  134. package/eslint-rules/pdf-last-page-balance.harness.mjs +6 -1
  135. package/eslint-rules/pdf-last-page-balance.ts +2 -2
  136. package/eslint-rules/tex-build.ts +8 -1
  137. package/fixtures/build-e2e/aidc/expect.json +1 -1
  138. package/fixtures/build-e2e/aidc/paperlint.json +1 -1
  139. package/fixtures/build-e2e/anon-ack/PIPELINE-STATUS.md +3 -0
  140. package/fixtures/build-e2e/anon-ack/expect.json +19 -0
  141. package/fixtures/build-e2e/anon-ack/paper.tex +31 -0
  142. package/fixtures/build-e2e/anon-ack/paperlint.json +1 -0
  143. package/fixtures/build-e2e/anon-author/PIPELINE-STATUS.md +3 -0
  144. package/fixtures/build-e2e/anon-author/expect.json +19 -0
  145. package/fixtures/build-e2e/anon-author/paper.tex +29 -0
  146. package/fixtures/build-e2e/anon-author/paperlint.json +1 -0
  147. package/fixtures/build-e2e/anon-bib/PIPELINE-STATUS.md +3 -0
  148. package/fixtures/build-e2e/anon-bib/expect.json +20 -0
  149. package/fixtures/build-e2e/anon-bib/paper.tex +35 -0
  150. package/fixtures/build-e2e/anon-bib/paperlint.json +1 -0
  151. package/fixtures/build-e2e/anon-href/PIPELINE-STATUS.md +3 -0
  152. package/fixtures/build-e2e/anon-href/expect.json +19 -0
  153. package/fixtures/build-e2e/anon-href/paper.tex +30 -0
  154. package/fixtures/build-e2e/anon-href/paperlint.json +1 -0
  155. package/fixtures/build-e2e/anon-macro/PIPELINE-STATUS.md +3 -0
  156. package/fixtures/build-e2e/anon-macro/expect.json +19 -0
  157. package/fixtures/build-e2e/anon-macro/paper.tex +30 -0
  158. package/fixtures/build-e2e/anon-macro/paperlint.json +1 -0
  159. package/fixtures/build-e2e/anon-pdfauthor/PIPELINE-STATUS.md +3 -0
  160. package/fixtures/build-e2e/anon-pdfauthor/expect.json +19 -0
  161. package/fixtures/build-e2e/anon-pdfauthor/paper.tex +31 -0
  162. package/fixtures/build-e2e/anon-pdfauthor/paperlint.json +1 -0
  163. package/fixtures/build-e2e/unbalanced/expect.json +2 -2
  164. package/fixtures/real-markdown-paper/baseline.json +1 -2
  165. package/lib/paper-config.ts +3 -1
  166. package/package.json +1 -1
  167. package/presets/acm-sigconf.jsonc +3 -1
  168. package/presets/aidc.jsonc +16 -17
  169. package/presets/ieee-conference.jsonc +8 -0
  170. package/presets/venue-profile.schema.json +9 -0
  171. package/scripts/check.ts +2 -2
  172. package/scripts/eslint-report-guard.mjs +5 -2
  173. package/scripts/harness-api.frozen.json +0 -1
  174. package/skills/paper-pipeline/references/pipeline-status-template.md +2 -16
  175. package/skills/render-paper/extract-pdf-facts.harness.ts +3 -3
  176. package/src/adapters/latex/includes.ts +10 -2
  177. package/src/adapters/latex/index.test.ts +8 -0
  178. package/src/adapters/latex/index.ts +3 -0
  179. package/src/adapters/latex/layout.ts +106 -0
  180. package/src/adapters/latex/nodes.ts +11 -3
  181. package/src/adapters/latex/parse.ts +5 -1
  182. package/src/build.harness.mjs +6 -4
  183. package/src/build.test.ts +23 -1
  184. package/src/build.ts +7 -3
  185. package/src/cli.harness.mjs +22 -54
  186. package/src/cli.test.ts +15 -18
  187. package/src/cli.ts +114 -46
  188. package/src/domain/anonymity.test.ts +163 -0
  189. package/src/domain/anonymity.ts +129 -0
  190. package/src/domain/body-pages.test.ts +300 -0
  191. package/src/domain/body-pages.ts +223 -0
  192. package/src/domain/paper-source.ts +2 -2
  193. package/src/domain/tex-document.ts +10 -0
  194. package/src/facts-file.test.ts +100 -4
  195. package/src/facts-file.ts +91 -9
  196. package/src/init.test.ts +33 -13
  197. package/src/init.ts +25 -48
  198. package/src/linted-set.test.ts +11 -1
  199. package/src/new-paper.harness.mjs +1 -5
  200. package/src/new-paper.test.ts +7 -1
  201. package/src/package-dirs.ts +9 -0
  202. package/src/paper-files.ts +2 -2
  203. package/src/paper-includes.test.ts +161 -0
  204. package/src/paper-includes.ts +77 -0
  205. package/src/paper-settings.test.ts +63 -6
  206. package/src/paper-settings.ts +53 -5
  207. package/src/pdf-content.test.ts +71 -0
  208. package/src/pdf-content.ts +125 -0
  209. package/src/pdf-facts.test.ts +110 -2
  210. package/src/pdf-facts.ts +106 -8
  211. package/src/ports/latex.ts +3 -0
  212. package/src/presets.ts +8 -8
  213. package/src/rule-cases.test.ts +29 -33
  214. package/src/rule-docs.test.ts +0 -3
  215. package/src/rules-config.ts +1 -0
  216. package/src/structure.harness.ts +1 -3
  217. package/src/structure.ts +1 -1
  218. package/src/tex-paper.test.ts +78 -61
  219. package/src/tex-paper.ts +57 -48
  220. package/src/tex-requirements.ts +9 -0
  221. package/src/tex-venue-rules.test.ts +111 -6
  222. package/src/tex-venue-rules.ts +57 -2
  223. package/src/types.ts +2 -0
  224. package/src/venue-rules.test.ts +351 -39
  225. package/src/venue-rules.ts +247 -47
  226. package/templates/paper/PIPELINE-STATUS.md +0 -4
  227. package/dist/action-ref.d.ts +0 -12
  228. package/dist/action-ref.d.ts.map +0 -1
  229. package/dist/action-ref.js +0 -16
  230. package/dist/action-ref.js.map +0 -1
  231. package/dist/eslint-rules/paper-research-question.d.ts +0 -31
  232. package/dist/eslint-rules/paper-research-question.d.ts.map +0 -1
  233. package/dist/eslint-rules/paper-research-question.js +0 -188
  234. package/dist/eslint-rules/paper-research-question.js.map +0 -1
  235. package/docs/rules/tex/missing-input.md +0 -58
  236. package/eslint-rules/paper-research-question.harness.mjs +0 -212
  237. package/eslint-rules/paper-research-question.ts +0 -199
  238. package/fixtures/paper-research-question/comment-only/PIPELINE-STATUS.md +0 -9
  239. package/fixtures/paper-research-question/comment-only/paper.tex +0 -7
  240. package/fixtures/paper-research-question/declared-not-in-paper/PIPELINE-STATUS.md +0 -10
  241. package/fixtures/paper-research-question/declared-not-in-paper/paper.tex +0 -6
  242. package/fixtures/paper-research-question/draft/PIPELINE-STATUS.md +0 -6
  243. package/fixtures/paper-research-question/draft/paper.tex +0 -2
  244. package/fixtures/paper-research-question/markdown-no-rq/PIPELINE-STATUS.md +0 -9
  245. package/fixtures/paper-research-question/markdown-no-rq/paper.md +0 -4
  246. package/fixtures/paper-research-question/shipped-no-rq/PIPELINE-STATUS.md +0 -12
  247. package/fixtures/paper-research-question/shipped-no-rq/paper.tex +0 -3
  248. package/fixtures/paper-research-question/shipped-with-rq/PIPELINE-STATUS.md +0 -10
  249. package/fixtures/paper-research-question/shipped-with-rq/paper.tex +0 -2
  250. package/src/action-ref.test.ts +0 -26
  251. package/src/action-ref.ts +0 -15
@@ -0,0 +1 @@
1
+ {"extends": "paperlint:aidc", "kind": "short", "identity": ["Ada Example", "adaexample", "ada@example.org", "Example University", "exampletool"]}
@@ -0,0 +1,3 @@
1
+ # Fixture
2
+
3
+ Stage: draft
@@ -0,0 +1,19 @@
1
+ {
2
+ "$comment": "anonymity/identity on a real IEEEtran build: A: the name in the author block. Exactly one finding, naming where.",
3
+ "build": {
4
+ "outcome": "pdf"
5
+ },
6
+ "lint": {
7
+ "rules": {
8
+ "anonymity/identity": [
9
+ {
10
+ "severity": 2,
11
+ "says": [
12
+ "«Ada Example»",
13
+ "PDF, page 1"
14
+ ]
15
+ }
16
+ ]
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,29 @@
1
+ % A: the name in the author block.
2
+ % The declared identity is invented: "Ada Example", handle "adaexample" (paperlint.json).
3
+ \documentclass[conference,compsoc]{IEEEtran}
4
+ \begin{filecontents*}{refs.bib}
5
+ @article{knuth84,
6
+ title = {Literate Programming},
7
+ author = {Knuth, Donald E.},
8
+ journal = {The Computer Journal},
9
+ volume = {27},
10
+ number = {2},
11
+ pages = {97--111},
12
+ year = {1984},
13
+ doi = {10.1093/comjnl/27.2.97}
14
+ }
15
+ \end{filecontents*}
16
+ \begin{document}
17
+ \title{A Fixture for Double-Blind Review}
18
+ \author{\IEEEauthorblockN{Ada Example}\IEEEauthorblockA{Anonymous Institution}}
19
+ \maketitle
20
+ \section{Introduction}
21
+ This paper builds on earlier work~\cite{knuth84}.
22
+
23
+ \section{Conclusion}
24
+ The conclusion restates that the fixture builds.
25
+ \bibliographystyle{IEEEtran}
26
+ \bibliography{refs}
27
+ \section*{LLM Usage Statement}
28
+ No LLMs were used in writing this fixture.
29
+ \end{document}
@@ -0,0 +1 @@
1
+ {"extends": "paperlint:aidc", "kind": "short", "identity": ["Ada Example", "adaexample", "ada@example.org", "Example University", "exampletool"]}
@@ -0,0 +1,3 @@
1
+ # Fixture
2
+
3
+ Stage: draft
@@ -0,0 +1,20 @@
1
+ {
2
+ "$comment": "anonymity/identity on a real IEEEtran build: C: a self-citation: the name only in a .bib author field, printed by IEEEtran.bst as \"A. Example\". Exactly one finding, naming where.",
3
+ "build": {
4
+ "outcome": "pdf"
5
+ },
6
+ "lint": {
7
+ "rules": {
8
+ "anonymity/identity": [
9
+ {
10
+ "severity": 2,
11
+ "says": [
12
+ "«A. Example»",
13
+ "matches «Ada Example»",
14
+ "PDF, page"
15
+ ]
16
+ }
17
+ ]
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,35 @@
1
+ % C: a self-citation: the name only in a .bib author field, printed by IEEEtran.bst as "A. Example".
2
+ % The declared identity is invented: "Ada Example", handle "adaexample" (paperlint.json).
3
+ \documentclass[conference,compsoc]{IEEEtran}
4
+ \begin{filecontents*}{refs.bib}
5
+ @article{knuth84,
6
+ title = {Literate Programming},
7
+ author = {Knuth, Donald E.},
8
+ journal = {The Computer Journal},
9
+ volume = {27},
10
+ number = {2},
11
+ pages = {97--111},
12
+ year = {1984},
13
+ doi = {10.1093/comjnl/27.2.97}
14
+ }
15
+ @misc{example25,
16
+ title = {A Tool for Measuring Things},
17
+ author = {Example, Ada and Other, Bob},
18
+ year = {2025},
19
+ note = {Technical report}
20
+ }
21
+ \end{filecontents*}
22
+ \begin{document}
23
+ \title{A Fixture for Double-Blind Review}
24
+ \author{\IEEEauthorblockN{Anonymous Authors}}
25
+ \maketitle
26
+ \section{Introduction}
27
+ This paper builds on earlier work~\cite{knuth84,example25}.
28
+
29
+ \section{Conclusion}
30
+ The conclusion restates that the fixture builds.
31
+ \bibliographystyle{IEEEtran}
32
+ \bibliography{refs}
33
+ \section*{LLM Usage Statement}
34
+ No LLMs were used in writing this fixture.
35
+ \end{document}
@@ -0,0 +1 @@
1
+ {"extends": "paperlint:aidc", "kind": "short", "identity": ["Ada Example", "adaexample", "ada@example.org", "Example University", "exampletool"]}
@@ -0,0 +1,3 @@
1
+ # Fixture
2
+
3
+ Stage: draft
@@ -0,0 +1,19 @@
1
+ {
2
+ "$comment": "anonymity/identity on a real IEEEtran build: E: a link whose visible text names nobody and whose target is the author's account. The anonymous mirror beside it is not reported. Exactly one finding, naming where.",
3
+ "build": {
4
+ "outcome": "pdf"
5
+ },
6
+ "lint": {
7
+ "rules": {
8
+ "anonymity/identity": [
9
+ {
10
+ "severity": 2,
11
+ "says": [
12
+ "link target on page 1 (https://github.com/adaexample/tool)",
13
+ "«adaexample»"
14
+ ]
15
+ }
16
+ ]
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,30 @@
1
+ % E: a link whose visible text names nobody and whose target is the author's account. The anonymous mirror beside it is not reported.
2
+ % The declared identity is invented: "Ada Example", handle "adaexample" (paperlint.json).
3
+ \documentclass[conference,compsoc]{IEEEtran}
4
+ \usepackage{hyperref}
5
+ \begin{filecontents*}{refs.bib}
6
+ @article{knuth84,
7
+ title = {Literate Programming},
8
+ author = {Knuth, Donald E.},
9
+ journal = {The Computer Journal},
10
+ volume = {27},
11
+ number = {2},
12
+ pages = {97--111},
13
+ year = {1984},
14
+ doi = {10.1093/comjnl/27.2.97}
15
+ }
16
+ \end{filecontents*}
17
+ \begin{document}
18
+ \title{A Fixture for Double-Blind Review}
19
+ \author{\IEEEauthorblockN{Anonymous Authors}}
20
+ \maketitle
21
+ \section{Introduction}
22
+ This paper builds on earlier work~\cite{knuth84}.
23
+ The code is at \href{https://github.com/adaexample/tool}{the tool's repository}, mirrored anonymously at \href{https://anonymous.4open.science/r/tool-1A2B}{an anonymous mirror}.
24
+ \section{Conclusion}
25
+ The conclusion restates that the fixture builds.
26
+ \bibliographystyle{IEEEtran}
27
+ \bibliography{refs}
28
+ \section*{LLM Usage Statement}
29
+ No LLMs were used in writing this fixture.
30
+ \end{document}
@@ -0,0 +1 @@
1
+ {"extends": "paperlint:aidc", "kind": "short", "identity": ["Ada Example", "adaexample", "ada@example.org", "Example University", "exampletool"]}
@@ -0,0 +1,3 @@
1
+ # Fixture
2
+
3
+ Stage: draft
@@ -0,0 +1,19 @@
1
+ {
2
+ "$comment": "anonymity/identity on a real IEEEtran build: F: the name hidden behind a macro; only the typeset text shows it. Exactly one finding, naming where.",
3
+ "build": {
4
+ "outcome": "pdf"
5
+ },
6
+ "lint": {
7
+ "rules": {
8
+ "anonymity/identity": [
9
+ {
10
+ "severity": 2,
11
+ "says": [
12
+ "«Ada Example»",
13
+ "PDF, page 1"
14
+ ]
15
+ }
16
+ ]
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,30 @@
1
+ % F: the name hidden behind a macro; only the typeset text shows it.
2
+ % The declared identity is invented: "Ada Example", handle "adaexample" (paperlint.json).
3
+ \documentclass[conference,compsoc]{IEEEtran}
4
+ \newcommand{\me}{Ada Example}
5
+ \begin{filecontents*}{refs.bib}
6
+ @article{knuth84,
7
+ title = {Literate Programming},
8
+ author = {Knuth, Donald E.},
9
+ journal = {The Computer Journal},
10
+ volume = {27},
11
+ number = {2},
12
+ pages = {97--111},
13
+ year = {1984},
14
+ doi = {10.1093/comjnl/27.2.97}
15
+ }
16
+ \end{filecontents*}
17
+ \begin{document}
18
+ \title{A Fixture for Double-Blind Review}
19
+ \author{\IEEEauthorblockN{Anonymous Authors}}
20
+ \maketitle
21
+ \section{Introduction}
22
+ This paper builds on earlier work~\cite{knuth84}.
23
+ This extends a tool first described by \me.
24
+ \section{Conclusion}
25
+ The conclusion restates that the fixture builds.
26
+ \bibliographystyle{IEEEtran}
27
+ \bibliography{refs}
28
+ \section*{LLM Usage Statement}
29
+ No LLMs were used in writing this fixture.
30
+ \end{document}
@@ -0,0 +1 @@
1
+ {"extends": "paperlint:aidc", "kind": "short", "identity": ["Ada Example", "adaexample", "ada@example.org", "Example University", "exampletool"]}
@@ -0,0 +1,3 @@
1
+ # Fixture
2
+
3
+ Stage: draft
@@ -0,0 +1,19 @@
1
+ {
2
+ "$comment": "anonymity/identity on a real IEEEtran build: D: the name only in the PDF metadata, set by hyperref; no page shows it. Exactly one finding, naming where.",
3
+ "build": {
4
+ "outcome": "pdf"
5
+ },
6
+ "lint": {
7
+ "rules": {
8
+ "anonymity/identity": [
9
+ {
10
+ "severity": 2,
11
+ "says": [
12
+ "PDF metadata: Author",
13
+ "«Ada Example»"
14
+ ]
15
+ }
16
+ ]
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,31 @@
1
+ % D: the name only in the PDF metadata, set by hyperref; no page shows it.
2
+ % The declared identity is invented: "Ada Example", handle "adaexample" (paperlint.json).
3
+ \documentclass[conference,compsoc]{IEEEtran}
4
+ \usepackage{hyperref}
5
+ \hypersetup{pdfauthor={Ada Example}}
6
+ \begin{filecontents*}{refs.bib}
7
+ @article{knuth84,
8
+ title = {Literate Programming},
9
+ author = {Knuth, Donald E.},
10
+ journal = {The Computer Journal},
11
+ volume = {27},
12
+ number = {2},
13
+ pages = {97--111},
14
+ year = {1984},
15
+ doi = {10.1093/comjnl/27.2.97}
16
+ }
17
+ \end{filecontents*}
18
+ \begin{document}
19
+ \title{A Fixture for Double-Blind Review}
20
+ \author{\IEEEauthorblockN{Anonymous Authors}}
21
+ \maketitle
22
+ \section{Introduction}
23
+ This paper builds on earlier work~\cite{knuth84}.
24
+
25
+ \section{Conclusion}
26
+ The conclusion restates that the fixture builds.
27
+ \bibliographystyle{IEEEtran}
28
+ \bibliography{refs}
29
+ \section*{LLM Usage Statement}
30
+ No LLMs were used in writing this fixture.
31
+ \end{document}
@@ -0,0 +1 @@
1
+ {"extends": "paperlint:aidc", "kind": "short", "identity": ["Ada Example", "adaexample", "ada@example.org", "Example University", "exampletool"]}
@@ -18,7 +18,7 @@
18
18
  }
19
19
  },
20
20
  "facts": {
21
- "schema": 2,
21
+ "schema": 3,
22
22
  "lastPageColumnsPt": [
23
23
  621.5,
24
24
  264.8
@@ -45,7 +45,7 @@
45
45
  "pdf/profile": [],
46
46
  "pdf/fonts": [],
47
47
  "pdf/geometry": [],
48
- "pdf/limits": [],
48
+ "format/page-limit": [],
49
49
  "pdf/body-size": [],
50
50
  "pdf/measured": [
51
51
  {
@@ -7,7 +7,7 @@
7
7
  "this file names the rule. That is not expressible on an eleven-line stub, where every line was",
8
8
  "written by someone who already knew which rule would read it.",
9
9
  "",
10
- "Recorded 2026-09-19, re-recorded 2026-09-26 for 3.0.0. Three of the four rules are about the",
10
+ "Recorded 2026-09-19, re-recorded 2026-09-26 for 3.0.0. Two of the three rules are about the",
11
11
  "scorecard being deliberately thin (no pdf, no frozen source) rather than about the prose.",
12
12
  "paper/leading-zero reports each of the 18 p-values of paperlint#44 where it is (one finding per",
13
13
  "occurrence since 3.0.0; before, paper/typography reported them as one count). They are real",
@@ -17,7 +17,6 @@
17
17
  "findings": {
18
18
  "paper/stages": 2,
19
19
  "paper/source": 1,
20
- "paper/research-question": 1,
21
20
  "paper/leading-zero": 18
22
21
  }
23
22
  }
@@ -136,8 +136,10 @@ export const SETTINGS_KEYS: Readonly<Record<string, string>> = Object.freeze({
136
136
  "rule overrides: { id: severity } for every paper in scope, or ESLint blocks (files, ignores, rules) — paperlint lint",
137
137
  extends:
138
138
  "the venue preset: paperlint:<name> or ./path.jsonc — the pdf/ venue rules, paperlint build",
139
- kind: "the kind of paper, whose page limit applies — pdf/limits",
139
+ kind: "the kind of paper, whose page limit applies — format/page-limit",
140
140
  pdf: "where the built PDF is, relative to the paper, when it is not paper.pdf — the facts",
141
+ identity:
142
+ "what identifies the authors — names, handles, emails, affiliations, own project names; a blind venue's PDF must not say any of them — anonymity/identity. The root's list and a paper's are joined",
141
143
  scripts: "how skill prose names the scripts — consumer.mjs",
142
144
  timezone: "the zone deadline anchors are written in — consumer.mjs",
143
145
  contactEmail: "the polite-pool address for citation lookups — consumer.mjs",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperlint",
3
- "version": "4.2.0",
3
+ "version": "6.0.0",
4
4
  "description": "A linter for scientific papers written in LaTeX: catches mistakes before you submit to a conference or journal.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -74,7 +74,9 @@
74
74
  // «font size is 9 pt for all body text»
75
75
  "body_pt": 9,
76
76
  // banal measures the MODE of the rendered text, not the declared size (measured 08-26: 9.30 at
77
- // acmart's declared 9). Hence the tolerance — and hence why body size stays a nudge, not a gate.
77
+ // acmart's declared 9). Hence the tolerance: banal reads 0.3 pt high on every template measured
78
+ // (fixtures/accepted-papers/body-size.json), and 0.5 absorbs that while a paper one size off is
79
+ // still reported.
78
80
  "body_pt_tol": 0.5,
79
81
 
80
82
  // «font size of the references can be from 7 pt to 9 pt»
@@ -22,6 +22,10 @@
22
22
  {
23
23
  "extends": "paperlint:ieee-conference",
24
24
 
25
+ // «Submissions must be properly anonymized for double-blind review.» (the call above), and
26
+ // `"blind":true` in the portal's data. anonymity/identity then requires the paper's `identity`.
27
+ "blind": true,
28
+
25
29
  // What the venue is called in a paper's text (tex/venue-leftover): the workshop, and the
26
30
  // conference it is held at.
27
31
  "aliases": ["AIDC", "ACSAC"],
@@ -44,30 +48,25 @@
44
48
  "textblock_w_in": 7.0,
45
49
  "textblock_h_in": 9.02,
46
50
 
47
- // 🔴 PAGE LIMITS ARE NOT GATED HERE, AND THAT IS A MEASUREMENT. banal splits pages into body
48
- // and references by what is on them, and a page that holds ANY non-bibliography text counts as
49
- // body. Measured 2026-09-27 on a [conference,compsoc] paper whose body ends at the foot of
50
- // page 12:
51
+ // 🔴 THE PAGE LIMIT IS COUNTED BEFORE THE REFERENCES, NOT BY banal. banal splits pages into
52
+ // body and references by what is on them, and a page that holds ANY non-bibliography text
53
+ // counts as body. Measured 2026-09-27 on a [conference,compsoc] paper whose body ends at the
54
+ // foot of page 12:
51
55
  // LLM Usage Statement before the references → body 13, references 0
52
56
  // an appendix before the references → body 13, references 2
53
57
  // both placed after the references → body right, the tail counted as references
54
58
  // AIDC counts neither in the limit («excluding references and appendices»; the ACSAC AI policy:
55
- // the LLM Usage Statement «does not count towards the page limit»), and tex/required-section
56
- // accepts the statement before or after the bibliography. A gate on banal's body count would
57
- // therefore fail a correct 12-page paper — the realm.jsonc case again. The numbers below are
58
- // data for humans, not an input to pdf/limits.
59
- "page_limits_not_gated": {
60
- "_": "Body pages, excluding references, appendices and the LLM Usage Statement. banal counts a page holding an appendix or the statement before the bibliography as body, so its body count is not this number.",
61
- "regular": 12,
62
- "short": 6,
63
- "references_and_appendices": "unlimited",
64
- "llm_usage_statement": "not counted (ACSAC AI policy)",
65
- },
59
+ // the LLM Usage Statement «does not count towards the page limit»). So format/page-limit counts
60
+ // the body up to where the references start (`body_ends_at`): the page they start on counts
61
+ // when body text stands above the heading there, and the limit below is gated. An appendix
62
+ // ends the body too, wherever it sits, when hyperref anchors it (docs/rules/format/page-limit.md).
63
+ "body_ends_at": "references",
66
64
 
67
65
  // «Short position papers or work-in-progress (WIP) papers» share one limit, so one kind.
66
+ // References, appendices and the LLM Usage Statement: unlimited.
68
67
  "kinds": {
69
- "regular": {},
70
- "short": {},
68
+ "regular": { "body_pages_max": 12 },
69
+ "short": { "body_pages_max": 6 },
71
70
  },
72
71
  },
73
72
  }
@@ -59,6 +59,14 @@
59
59
  "body_pt": 10,
60
60
  "body_pt_tol": 0.5,
61
61
 
62
+ // The class sets the bibliography in \footnotesize, 8 pt at 10pt and at 9pt (IEEEtran.cls,
63
+ // \thebibliography). Measured 2026-09-29 on the four built ACSAC papers in
64
+ // fixtures/accepted-papers (body-size.json): pdf.js reads the reference glyphs at 8.00 pt
65
+ // (7.97 without compsoc), banal gives 8.3 on all four. body_pt_tol widens it to 7.5-8.5, so a
66
+ // bibliography shrunk to \scriptsize (7.3) or grown to \small (9.3) is reported.
67
+ "ref_pt_min": 8,
68
+ "ref_pt_max": 8,
69
+
62
70
  // The Times clone. Headings and the title are set in the same family, so a `fonts_title` would
63
71
  // repeat this check and report a Computer Modern build twice.
64
72
  "fonts_text": "NimbusRomNo9L",
@@ -24,6 +24,10 @@
24
24
  "type": "string",
25
25
  "minLength": 1
26
26
  },
27
+ "blind": {
28
+ "description": "The venue reviews double-blind: its call says submissions must be anonymized. Read by `anonymity/identity`, which then requires the paper's `identity` and finds none of it in the PDF. Set only where the venue says so, with the quote. A child's value replaces its parent's.",
29
+ "type": "boolean"
30
+ },
27
31
  "aliases": {
28
32
  "description": "Other names the venue goes by in a paper's text — its acronym, its parent conference (`[\"AISec\", \"ACM CCS\"]`). Read by `tex/venue-leftover`: a paper that extends another venue and says one of these is flagged. Matched as whole words, case-sensitive. Collected along the `extends` chain, with `name`.",
29
33
  "type": "array",
@@ -105,6 +109,11 @@
105
109
  "type": "string",
106
110
  "minLength": 1
107
111
  },
112
+ "body_ends_at": {
113
+ "description": "How `format/page-limit` counts body pages. Absent: banal's body count (every page holding non-bibliography text). `references`: the pages up to where the body ends — at the references or the appendix, whichever comes first, found by hyperref's anchors and then the heading line — with that page counted when body text stands above the heading on it; for a venue whose limit excludes references and appendices (and what follows them, such as statements).",
114
+ "type": "string",
115
+ "enum": ["references"]
116
+ },
108
117
  "page_limits_not_gated": {
109
118
  "type": "object"
110
119
  },
package/scripts/check.ts CHANGED
@@ -3,9 +3,9 @@
3
3
  * `npm run check` — ONE command you run on yourself before pushing.
4
4
  *
5
5
  * ── WHY IT EXISTS, AND IT IS A MEASURED FAILURE, NOT A TIDINESS IDEA ───────────────────────
6
- * On 2026-09-19 a change to `paper/research-question` was pushed that broke the test suite.
6
+ * On 2026-09-19 a change to a lint rule was pushed that broke the test suite.
7
7
  * The gates were run afterwards — `lint`, `check:readme`, `check:globs`, `check:content-only`,
8
- * and the rule's own (since-removed) mutation battery, all green — but `npm test` and `test:install` were not,
8
+ * and the rule's own mutation battery, all green — but `npm test` and `test:install` were not,
9
9
  * because there were eleven separate scripts and no way to run them except from memory. Five
10
10
  * callers across two files had been left behind by the change, and the suite said so; nobody
11
11
  * asked it.
@@ -17,6 +17,7 @@
17
17
  * proves the BEHAVIOUR rather than the presence of a string in YAML.
18
18
  *
19
19
  * Usage: node scripts/eslint-report-guard.mjs <report.json> <eslint-rc> [paths] [config]
20
+ * An empty or absent `paths` means the CLI was given none and linted the papersDir of the config.
20
21
  * Exit: 1 when nothing was measured (unparsable report, or zero files linted)
21
22
  * otherwise ESLint's own return code, passed through untouched
22
23
  */
@@ -26,8 +27,10 @@ import { readFileSync } from "node:fs";
26
27
  export function guard(
27
28
  reportPath,
28
29
  eslintRc,
29
- { paths = ".", config = "paperlint.json" } = {},
30
+ { paths = "", config = "paperlint.json" } = {},
30
31
  ) {
32
+ const scope =
33
+ paths.trim() === "" ? "(none: the papersDir of paperlint.json)" : paths;
31
34
  const lines = [];
32
35
  let res;
33
36
  try {
@@ -48,7 +51,7 @@ export function guard(
48
51
  if (res.length === 0) {
49
52
  lines.push(
50
53
  "::error::ESLint linted ZERO files. In the exit code this is indistinguishable from a clean " +
51
- `run, and it means the paths or the config matched nothing. paths=${paths} config=${config}`,
54
+ `run, and it means the paths or the config matched nothing. paths=${scope} config=${config}`,
52
55
  );
53
56
  return { code: 1, lines };
54
57
  }
@@ -5,7 +5,6 @@
5
5
  "action.harness.mjs",
6
6
  "eslint-rules/install-path-literals.harness.mjs",
7
7
  "eslint-rules/latex-language.harness.mjs",
8
- "eslint-rules/paper-research-question.harness.mjs",
9
8
  "eslint-rules/paper-stages.harness.mjs",
10
9
  "eslint-rules/papers.harness.mjs",
11
10
  "eslint-rules/pdf-last-page-balance.harness.mjs",
@@ -11,7 +11,7 @@ the verdict line's wording and the `☐`/`☑` marks as they are, or the hook go
11
11
 
12
12
  ## The front matter
13
13
 
14
- Above the sections, the file carries YAML. Two fields are read by rules:
14
+ Above the sections, the file carries YAML. The rules read its `stages` field:
15
15
 
16
16
  ```yaml
17
17
  ---
@@ -22,20 +22,9 @@ stages:
22
22
  bytes: 305412
23
23
  source: versions/2026-07-22-submitted.tex
24
24
  sourceBytes: 57210
25
- researchQuestion: "Does pruning the state space reduce review cost?"
26
25
  ---
27
26
  ```
28
27
 
29
- `researchQuestion` is **your sentence, written once** — not a label the checker hunts for in the
30
- prose. `paper/research-question` then asks two things it can actually answer: is it written down,
31
- and does the paper contain it (whitespace collapsed). Leave the field out and a shipped paper gets
32
- an advisory finding, which is the right outcome for a position paper with no question: the absence
33
- becomes a decision on the record rather than an omission.
34
-
35
- ⚠️ Write the sentence the way it appears in the paper. If the paper breaks it across lines that is
36
- fine — only whitespace is normalised — but LaTeX markup inside the sentence (`\emph{cost}`) will not
37
- match, and the finding will quote exactly what it looked for.
38
-
39
28
  ## The row ids, and what each one runs
40
29
 
41
30
  The `id` cell is the **join key** — the same string appears in this template, in every paper's
@@ -116,10 +105,7 @@ existed. Neither is expressible now. Keep new ids lowercase, one word, and say w
116
105
  The template itself is a FILE, not this page: `templates/paper/PIPELINE-STATUS.md` at the package
117
106
  root (beside `templates/paper/paper.tex` and `paper.md`). `paperlint new <name>` copies it with the paper's
118
107
  name filled in, and a project that keeps its own richer scorecard puts one at
119
- `<papers>/.template/PIPELINE-STATUS.md`, which `paperlint new` prefers. It used to live in a fenced block
120
- here, which meant a command would have had to parse markdown to extract it, and a human had to copy
121
- it by hand. The copy carries `researchQuestion` (empty) and no `stages` — a new paper has shipped
122
- nothing.
108
+ `<papers>/.template/PIPELINE-STATUS.md`, which `paperlint new` prefers. The copy carries no `stages` — a new paper has shipped nothing.
123
109
 
124
110
  ---
125
111
 
@@ -119,8 +119,8 @@ try {
119
119
  const fonts = Fonts.safeParse(facts.fonts);
120
120
  const fontList = fonts.success ? fonts.data : [];
121
121
  check(
122
- "the facts are schema 2, about paper.pdf, for the venue paperlint.json declares",
123
- facts.schema === 2 &&
122
+ "the facts are schema 3, about paper.pdf, for the venue paperlint.json declares",
123
+ facts.schema === 3 &&
124
124
  facts.pdf === "paper.pdf" &&
125
125
  facts.venue === "agenticdev" &&
126
126
  facts.kind === "short",
@@ -236,5 +236,5 @@ try {
236
236
  }
237
237
 
238
238
  console.log(
239
- `✓ ${String(check.count)} assertions passed — extract-pdf-facts: the exit-code contract, schema 2, and the no-no-yes font`,
239
+ `✓ ${String(check.count)} assertions passed — extract-pdf-facts: the exit-code contract, schema 3, and the no-no-yes font`,
240
240
  );
@@ -55,10 +55,18 @@ export const includesOf = (t: ParsedTex): readonly Include[] =>
55
55
  const isDocument = (n: Node): n is Readonly<Ast.Environment> =>
56
56
  n.type === "environment" && n.env === "document";
57
57
 
58
- /** The span of the `document` environment's body, or null when the source has none. */
58
+ /**
59
+ * The span of the `document` environment's body, or null when the source has none. A macro's own
60
+ * position ends at its name, so its place is taken with its arguments: a body ending in
61
+ * `\input{sections/a}` ends at the closing brace.
62
+ */
59
63
  export const documentBodyOf = (t: ParsedTex): Span | null =>
60
64
  (t.root.content.find(isDocument)?.content ?? [])
61
- .flatMap((n) => inPlace(placeOf(n), (span) => [span]))
65
+ .flatMap((n) =>
66
+ inPlace(n.type === "macro" ? macroPlace(n) : placeOf(n), (span) => [
67
+ span,
68
+ ]),
69
+ )
62
70
  .reduce<Span | null>(
63
71
  (body, span) =>
64
72
  body === null ? span : { start: body.start, end: span.end },
@@ -526,6 +526,14 @@ describe("documentBodyOf — the body of a source that is a document of its own"
526
526
  expect(latexReader.documentBody(src)).toEqual(body);
527
527
  });
528
528
 
529
+ it("a body ending in a macro ends after that macro's arguments, not after its name", () => {
530
+ const src = "\\begin{document}\nText.\n\\input{sections/a}\\end{document}";
531
+ const body = documentBodyOf(parseLatex(src));
532
+ expect(body && src.slice(body.start, body.end)).toBe(
533
+ "Text.\n\\input{sections/a}",
534
+ );
535
+ });
536
+
529
537
  it("none without a document environment, or with an empty one", () => {
530
538
  expect(documentBodyOf(parseLatex("just text"))).toBe(null);
531
539
  expect(documentBodyOf(parseLatex("\\begin{document}\\end{document}"))).toBe(