paperlint 4.0.0 → 4.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 (236) hide show
  1. package/.github/workflows/ci.yml +16 -23
  2. package/CLAUDE.md +55 -11
  3. package/CONTRIBUTING.md +30 -7
  4. package/README.md +11 -8
  5. package/dist/adapters/banal/invocation.d.ts +1 -1
  6. package/dist/adapters/latex/document-class.d.ts +23 -0
  7. package/dist/adapters/latex/document-class.d.ts.map +1 -0
  8. package/dist/adapters/latex/document-class.js +110 -0
  9. package/dist/adapters/latex/document-class.js.map +1 -0
  10. package/dist/adapters/latex/hidden.d.ts +16 -0
  11. package/dist/adapters/latex/hidden.d.ts.map +1 -0
  12. package/dist/adapters/latex/hidden.js +49 -0
  13. package/dist/adapters/latex/hidden.js.map +1 -0
  14. package/dist/adapters/latex/includes.d.ts +12 -0
  15. package/dist/adapters/latex/includes.d.ts.map +1 -0
  16. package/dist/adapters/latex/includes.js +42 -0
  17. package/dist/adapters/latex/includes.js.map +1 -0
  18. package/dist/adapters/latex/index.d.ts +33 -0
  19. package/dist/adapters/latex/index.d.ts.map +1 -0
  20. package/dist/adapters/latex/index.js +25 -0
  21. package/dist/adapters/latex/index.js.map +1 -0
  22. package/dist/adapters/latex/nodes.d.ts +58 -0
  23. package/dist/adapters/latex/nodes.d.ts.map +1 -0
  24. package/dist/adapters/latex/nodes.js +97 -0
  25. package/dist/adapters/latex/nodes.js.map +1 -0
  26. package/dist/adapters/latex/outline.d.ts +10 -0
  27. package/dist/adapters/latex/outline.d.ts.map +1 -0
  28. package/dist/adapters/latex/outline.js +39 -0
  29. package/dist/adapters/latex/outline.js.map +1 -0
  30. package/dist/adapters/latex/parse.d.ts +42 -0
  31. package/dist/adapters/latex/parse.d.ts.map +1 -0
  32. package/dist/adapters/latex/parse.js +95 -0
  33. package/dist/adapters/latex/parse.js.map +1 -0
  34. package/dist/adapters/latex/prose.d.ts +8 -0
  35. package/dist/adapters/latex/prose.d.ts.map +1 -0
  36. package/dist/adapters/latex/prose.js +353 -0
  37. package/dist/adapters/latex/prose.js.map +1 -0
  38. package/dist/adapters/latex/rendered.d.ts +10 -0
  39. package/dist/adapters/latex/rendered.d.ts.map +1 -0
  40. package/dist/adapters/latex/rendered.js +39 -0
  41. package/dist/adapters/latex/rendered.js.map +1 -0
  42. package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
  43. package/dist/adapters/pdfjs/fill.js +12 -12
  44. package/dist/adapters/pdfjs/fill.js.map +1 -1
  45. package/dist/build.js +4 -4
  46. package/dist/build.js.map +1 -1
  47. package/dist/claim-provenance.d.ts +34 -0
  48. package/dist/claim-provenance.d.ts.map +1 -0
  49. package/dist/claim-provenance.js +146 -0
  50. package/dist/claim-provenance.js.map +1 -0
  51. package/dist/cli.d.ts.map +1 -1
  52. package/dist/cli.js +49 -5
  53. package/dist/cli.js.map +1 -1
  54. package/dist/domain/paper-source.d.ts +58 -0
  55. package/dist/domain/paper-source.d.ts.map +1 -0
  56. package/dist/domain/paper-source.js +109 -0
  57. package/dist/domain/paper-source.js.map +1 -0
  58. package/dist/domain/sentences.d.ts +26 -0
  59. package/dist/domain/sentences.d.ts.map +1 -0
  60. package/dist/domain/sentences.js +48 -0
  61. package/dist/domain/sentences.js.map +1 -0
  62. package/dist/domain/tex-document.d.ts +119 -0
  63. package/dist/domain/tex-document.d.ts.map +1 -0
  64. package/dist/domain/tex-document.js +43 -0
  65. package/dist/domain/tex-document.js.map +1 -0
  66. package/dist/eslint-rules/paper-typography.d.ts +7 -0
  67. package/dist/eslint-rules/paper-typography.d.ts.map +1 -1
  68. package/dist/eslint-rules/paper-typography.js +53 -1
  69. package/dist/eslint-rules/paper-typography.js.map +1 -1
  70. package/dist/facts-file.d.ts.map +1 -1
  71. package/dist/facts-file.js +2 -2
  72. package/dist/facts-file.js.map +1 -1
  73. package/dist/new-paper.d.ts +5 -0
  74. package/dist/new-paper.d.ts.map +1 -1
  75. package/dist/new-paper.js +3 -0
  76. package/dist/new-paper.js.map +1 -1
  77. package/dist/package-dirs.d.ts +5 -0
  78. package/dist/package-dirs.d.ts.map +1 -0
  79. package/dist/package-dirs.js +14 -0
  80. package/dist/package-dirs.js.map +1 -0
  81. package/dist/ports/latex.d.ts +35 -0
  82. package/dist/ports/latex.d.ts.map +1 -0
  83. package/dist/ports/latex.js +2 -0
  84. package/dist/ports/latex.js.map +1 -0
  85. package/dist/presets.d.ts +15 -2
  86. package/dist/presets.d.ts.map +1 -1
  87. package/dist/presets.js +19 -6
  88. package/dist/presets.js.map +1 -1
  89. package/dist/register.d.ts +40 -0
  90. package/dist/register.d.ts.map +1 -0
  91. package/dist/register.js +134 -0
  92. package/dist/register.js.map +1 -0
  93. package/dist/tex-paper.d.ts +19 -0
  94. package/dist/tex-paper.d.ts.map +1 -0
  95. package/dist/tex-paper.js +105 -0
  96. package/dist/tex-paper.js.map +1 -0
  97. package/dist/tex-requirements.d.ts +11 -0
  98. package/dist/tex-requirements.d.ts.map +1 -1
  99. package/dist/tex-requirements.js +14 -8
  100. package/dist/tex-requirements.js.map +1 -1
  101. package/dist/tex-venue-rules.d.ts +88 -0
  102. package/dist/tex-venue-rules.d.ts.map +1 -0
  103. package/dist/tex-venue-rules.js +268 -0
  104. package/dist/tex-venue-rules.js.map +1 -0
  105. package/dist/venue-rules.d.ts +1 -1
  106. package/dist/venue-rules.d.ts.map +1 -1
  107. package/docs/configuration.md +3 -2
  108. package/docs/e2e.md +69 -40
  109. package/docs/install.md +1 -1
  110. package/docs/prior-art/README.md +1 -1
  111. package/docs/prior-art/package-location.md +5 -5
  112. package/docs/prior-art/repro/README.md +1 -1
  113. package/docs/prior-art/test-tooling.md +21 -10
  114. package/docs/rules/tex/claim-provenance.md +83 -0
  115. package/docs/rules/tex/missing-input.md +58 -0
  116. package/docs/rules/tex/register.md +77 -0
  117. package/docs/rules/tex/required-section.md +75 -0
  118. package/docs/rules/tex/template.md +74 -0
  119. package/docs/rules/tex/venue-leftover.md +73 -0
  120. package/docs/rules.md +73 -48
  121. package/docs/testing.md +17 -4
  122. package/docs/texlive-install-decision.md +1 -1
  123. package/docs/toolchain.md +2 -2
  124. package/eslint-rules/paper-typography.test.mjs +48 -1
  125. package/eslint-rules/paper-typography.ts +72 -1
  126. package/eslint.config.mjs +156 -15
  127. package/fixtures/build-e2e/acmart/expect.json +26 -0
  128. package/fixtures/build-e2e/aidc/PIPELINE-STATUS.md +3 -0
  129. package/fixtures/build-e2e/aidc/expect.json +14 -0
  130. package/fixtures/build-e2e/aidc/paper.tex +46 -0
  131. package/fixtures/build-e2e/aidc/paperlint.json +1 -0
  132. package/fixtures/build-e2e/broken/expect.json +11 -0
  133. package/fixtures/build-e2e/cite/expect.json +27 -0
  134. package/fixtures/build-e2e/empty/expect.json +12 -0
  135. package/fixtures/build-e2e/fallback/expect.json +53 -0
  136. package/fixtures/build-e2e/guards/expect.json +6 -0
  137. package/fixtures/build-e2e/no-source/expect.json +10 -0
  138. package/fixtures/build-e2e/unbalanced/expect.json +60 -0
  139. package/fixtures/pdf-facts/README.md +1 -1
  140. package/fixtures/real-markdown-paper/baseline.ts +8 -7
  141. package/fixtures/render-paper/build-clean.sh +3 -5
  142. package/package.json +14 -2
  143. package/{skills/submit-paper/references/venues → presets}/acm-sigconf.jsonc +4 -1
  144. package/{skills/submit-paper/references/venues → presets}/agenticdev.jsonc +4 -0
  145. package/presets/aidc.jsonc +73 -0
  146. package/{skills/submit-paper/references/venues → presets}/aisec.jsonc +4 -0
  147. package/presets/ieee-conference.jsonc +66 -0
  148. package/{skills/submit-paper/references/venues → presets}/realm.jsonc +8 -1
  149. package/{skills/submit-paper/references/venues → presets/tex}/paper-guards.tex +1 -1
  150. package/{skills/submit-paper/references/venues → presets}/venue-profile.schema.json +30 -2
  151. package/scripts/check.harness.ts +64 -10
  152. package/scripts/check.test.ts +60 -13
  153. package/scripts/check.ts +83 -30
  154. package/scripts/test.test.ts +1 -1
  155. package/scripts/test.ts +1 -1
  156. package/skills/cold-read-diff/SKILL.md +31 -10
  157. package/skills/cold-read-diff/SKILL.md.spec.ts +30 -9
  158. package/skills/draft-paper/SKILL.md +2 -2
  159. package/skills/draft-paper/SKILL.md.spec.ts +1 -1
  160. package/skills/grade-paper-writing/SKILL.md +109 -44
  161. package/skills/grade-paper-writing/SKILL.md.spec.ts +108 -43
  162. package/skills/harden-paper/SKILL.md +35 -36
  163. package/skills/harden-paper/SKILL.md.spec.ts +34 -35
  164. package/skills/paper-adversarial-review/SKILL.md +2 -2
  165. package/skills/paper-adversarial-review/SKILL.md.spec.ts +1 -1
  166. package/skills/paper-pipeline/PIPELINE-MAP.md +2 -2
  167. package/skills/paper-pipeline/SKILL.md +4 -4
  168. package/skills/paper-pipeline/SKILL.md.spec.ts +3 -3
  169. package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +6 -6
  170. package/skills/paper-pipeline/references/writing-craft.md +53 -18
  171. package/skills/paper-pipeline/scripts/consumer.d.mts +0 -2
  172. package/skills/paper-pipeline/scripts/consumer.mjs +1 -15
  173. package/skills/pc-panel-review/SKILL.md +36 -11
  174. package/skills/pc-panel-review/SKILL.md.spec.ts +35 -10
  175. package/skills/render-paper/SKILL.md +2 -2
  176. package/skills/render-paper/SKILL.md.spec.ts +1 -1
  177. package/skills/render-paper/check-render.sh +16 -63
  178. package/skills/render-paper/render-paper.harness.mjs +17 -59
  179. package/src/adapters/banal/index.test.ts +1 -1
  180. package/src/adapters/banal/invocation.ts +1 -1
  181. package/src/adapters/banal/output.test.ts +1 -1
  182. package/src/adapters/banal/run.test.ts +1 -1
  183. package/src/adapters/banal/xml.test.ts +1 -1
  184. package/src/adapters/latex/document-class.ts +155 -0
  185. package/src/adapters/latex/hidden.ts +70 -0
  186. package/src/adapters/latex/includes.ts +66 -0
  187. package/src/adapters/latex/index.test.ts +535 -0
  188. package/src/adapters/latex/index.ts +65 -0
  189. package/src/adapters/latex/nodes.ts +150 -0
  190. package/src/adapters/latex/outline.ts +64 -0
  191. package/src/adapters/latex/parse.ts +137 -0
  192. package/src/adapters/latex/prose.ts +484 -0
  193. package/src/adapters/latex/rendered.ts +59 -0
  194. package/src/adapters/pdfjs/fill.test.ts +1 -1
  195. package/src/adapters/pdfjs/fill.ts +18 -10
  196. package/src/build.harness.mjs +5 -7
  197. package/src/build.ts +4 -4
  198. package/src/claim-provenance.test.ts +352 -0
  199. package/src/claim-provenance.ts +217 -0
  200. package/src/cli.harness.mjs +1 -1
  201. package/src/cli.test.ts +2 -8
  202. package/src/cli.ts +53 -8
  203. package/src/domain/paper-source.test.ts +156 -0
  204. package/src/domain/paper-source.ts +233 -0
  205. package/src/domain/sentences.ts +67 -0
  206. package/src/domain/tex-document.test.ts +80 -0
  207. package/src/domain/tex-document.ts +143 -0
  208. package/src/engine.harness.mjs +1 -1
  209. package/src/facts-file.test.ts +4 -7
  210. package/src/facts-file.ts +2 -2
  211. package/src/latex-loop.harness.ts +1 -1
  212. package/src/new-paper.test.ts +28 -3
  213. package/src/new-paper.ts +8 -0
  214. package/src/package-dirs.ts +17 -0
  215. package/src/paper-settings-commands.test.ts +6 -1
  216. package/src/ports/latex.ts +42 -0
  217. package/src/presets.test.ts +65 -4
  218. package/src/presets.ts +38 -15
  219. package/src/register.test.ts +165 -0
  220. package/src/register.ts +177 -0
  221. package/src/rule-cases.test.ts +524 -0
  222. package/src/rule-docs.test.ts +159 -0
  223. package/src/tex-paper.test.ts +131 -0
  224. package/src/tex-paper.ts +152 -0
  225. package/src/tex-requirements.harness.ts +13 -23
  226. package/src/tex-requirements.ts +35 -8
  227. package/src/tex-venue-rules.test.ts +741 -0
  228. package/src/tex-venue-rules.ts +461 -0
  229. package/src/toolchain.harness.mjs +1 -1
  230. package/src/venue-rules.test.ts +96 -4
  231. package/src/venue-rules.ts +1 -1
  232. package/templates/paper/paper.tex +2 -1
  233. package/skills/harden-paper/fixtures/uncited-assertions-sample.md +0 -43
  234. package/skills/harden-paper/fixtures/uncited-assertions-sample.tex +0 -77
  235. /package/{skills/submit-paper/references/venues → presets/tex}/agenticdev.tex +0 -0
  236. /package/{skills/submit-paper/references/venues → presets}/tex-base.jsonc +0 -0
package/docs/e2e.md CHANGED
@@ -1,37 +1,50 @@
1
1
  # End-to-end tests
2
2
 
3
- They are `test/e2e/install.ts` and `test/e2e/build.ts`. Both are
4
- part of `npm run check`, so nobody has to remember to call them. `npm run test:e2e` runs just
5
- these, install first; if the install e2e fails or is skipped, the build e2e does not run.
3
+ They are vitest tests named `*.e2e.ts` under `test/e2e/`, one vitest project per environment they
4
+ need, found by the project's glob — no list of files exists in `package.json`, `scripts/check.ts`
5
+ or CI:
6
+
7
+ | script | project | what it needs | files |
8
+ | -------------------------- | ------------- | -------------------------------------------- | --------------------------- |
9
+ | `npm run test:e2e:install` | `e2e-install` | npm and pnpm, and the network for `npm pack` | `test/e2e/install/*.e2e.ts` |
10
+ | `npm run test:e2e:tex` | `e2e-tex` | TeX Live from `paperlint toolchain`, banal | `test/e2e/tex/*.e2e.ts` |
11
+ | `npm run test:e2e` | both | | |
12
+
13
+ Both areas are part of `npm run check`, so nobody has to remember to call them. CI runs each in the
14
+ job that has its environment: `test:e2e:install` in `gates`, `test:e2e:tex` in `build-e2e`.
6
15
 
7
16
  This page says what they prove, what they deliberately do not, and when a change owes a new one.
8
17
 
9
- ## The line between a harness and an end-to-end run
18
+ ## The line between the other tests and an end-to-end run
10
19
 
11
- Most of this package is tested by harnesses, described in [`CONTRIBUTING.md`](../CONTRIBUTING.md):
12
- a rule is handed a file with a defect and must find it, then a clean file and must stay silent.
13
- That is the right shape for a rule, and it cannot answer a different kind of question.
20
+ The rest of the package is tested by unit and integration tests and, for what an agent sees, by
21
+ harnesses — [`testing.md`](testing.md) says which is which. A rule is tested twice before any e2e:
22
+ unit tests beside it hand its logic a defect and a clean input, and `src/rule-cases.test.ts` runs
23
+ every registered rule through real ESLint on a small paper tree, the way `paperlint lint` does.
24
+ That is still an integration test: no TeX, no install, the working tree as it is.
14
25
 
15
- A harness runs **in this repository's process, against this repository's working tree**. It
16
- imports the module directly. Every path resolves, every dependency is already installed, and the
17
- file it reads is the file in `git`. An end-to-end run exists because all three of those are
26
+ Those tests run **in this repository's process, against this repository's working tree**. They
27
+ import the modules directly. Every path resolves, every dependency is already installed, and the
28
+ file they read is the file in `git`. An end-to-end run exists because all three of those are
18
29
  assumptions that stop holding the moment somebody else installs the package.
19
30
 
20
- | question | answered by |
21
- | ------------------------------------------------------------------------------------ | --------------------------------------------------------- |
22
- | does this rule find this defect, and stay quiet otherwise | harness |
23
- | does the CLI pick the right script, with the right interpreter | harness (`src/build.harness.mjs` substitutes `spawnSync`) |
24
- | does the tarball, installed into a tree that is not this one, contain what it claims | **e2e** |
25
- | do the paths written inside a skill resolve where the package actually lands | **e2e** |
26
- | did a PDF come out, and is it typeset in the fonts the venue requires | **e2e** |
31
+ | question | answered by |
32
+ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------- |
33
+ | does this rule find this defect, and stay quiet otherwise | unit, beside the rule |
34
+ | does the rule run through ESLint, wired, on the file it is for | integration (`src/rule-cases.test.ts`) |
35
+ | does the CLI pick the right script, with the right interpreter | integration (`src/build.harness.mjs` substitutes `spawnSync`) |
36
+ | does the tarball, installed into a tree that is not this one, contain what it claims | **e2e** |
37
+ | do the paths written inside a skill resolve where the package actually lands | **e2e** |
38
+ | did a PDF come out, and is it typeset in the fonts the venue requires | **e2e** |
27
39
 
28
- The third and fourth rows are the ones a harness cannot reach even in principle: the defect only
29
- exists once the code is somewhere else.
40
+ The last three rows are the ones no other test can reach even in principle: the defect only exists
41
+ once the code is somewhere else, or once a real TeX has typeset it.
30
42
 
31
- ## `test/e2e/install.ts` — the package, installed
43
+ ## `test/e2e/install/install.e2e.ts` — the package, installed
32
44
 
33
- `test/e2e/install.ts`. It runs `npm pack`, then installs the resulting tarball into a fresh
34
- temporary tree, **under npm and under pnpm separately**, and drives the installed binary:
45
+ It runs `npm pack`, then installs the resulting tarball into a fresh temporary tree, **under npm
46
+ and under pnpm separately** — one test per manager, each the whole scenario, because its steps
47
+ depend on each other — and drives the installed binary:
35
48
 
36
49
  - the install itself finishes
37
50
  - `paperlint --help` answers with zero
@@ -55,13 +68,21 @@ that works under npm can be dead under pnpm with no error anywhere. The same run
55
68
  binary **directly** rather than through `node <path>`: under npm `.bin` holds a symlink, under
56
69
  pnpm a shell wrapper, and calling `node bin` measures the caller's habit instead of the package.
57
70
 
58
- ## `test/e2e/build.ts` — a real `pdflatex`
71
+ ## `test/e2e/tex/build.e2e.ts` — a real `pdflatex`
59
72
 
60
- `test/e2e/build.ts`. It copies `fixtures/build-e2e/` — eight papers, none with a build script paperlint
61
- would run — into a temporary tree, points a config at it, and runs `paperlint build --all`. paperlint compiles
62
- each paper itself with the real `pdflatex` and `bibtex`; the artifacts are then measured with
73
+ It copies `fixtures/build-e2e/` — nine papers, none with a build script paperlint would run — into
74
+ a temporary tree, points a config at it, and runs `paperlint build --all`. paperlint compiles each
75
+ paper itself with the real `pdflatex` and `bibtex`; the artifacts are then measured with
63
76
  paperlint's own pdf.js reader from `dist/`, and the fonts are cross-checked against the list of
64
- programs pdfTeX writes into `paper.log`:
77
+ programs pdfTeX writes into `paper.log`.
78
+
79
+ **Each fixture declares what it must produce** in `fixtures/build-e2e/<name>/expect.json`, parsed
80
+ with the schema in `test/e2e/tex/build-expect.ts`: whether it builds (`build.outcome`), what its
81
+ lines of the build output say, its fonts, its last page's text, files in its directory, the facts
82
+ the build measured, and — for `paperlint lint` after the build — the exit code and **exactly** the
83
+ findings for each rule it names (`[]` is "none"). One `describe` per fixture directory turns each
84
+ declared field into a test. A folder without `expect.json`, or with one the schema rejects, fails.
85
+ A new venue fixture is a new folder: its paper, its `paperlint.json`, its `expect.json`.
65
86
 
66
87
  | fixture | what it is there to prove |
67
88
  | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
@@ -72,6 +93,8 @@ programs pdfTeX writes into `paper.log`:
72
93
  | `unbalanced` | a two-column acmart paper whose last page comes out 621.5 / 264.8 pt builds **green** — the build does not judge the layout and rewrites nothing — while `_build/paper.facts.json` records both heights, and `paperlint lint` with `pdf/last-page-balance` turned on in `rules` reports the page |
73
94
  | `broken` | a failing build names pdflatex and its exit code, quotes the error line and its `l.NNN` context, and deletes the stale `paper.pdf` planted before the run |
74
95
  | `no-source` | a paper with no `paper.tex` is named separately, and the run as a whole is a failure |
96
+ | `empty` | a document with no pages: pdflatex exits 0 and writes no PDF, and that is a failure, not a green `paper.pdf` |
97
+ | `aidc` | a real `[conference,compsoc]{IEEEtran}` paper builds on paperlint's TeX Live in NimbusRomNo9L, `tex/template`, `tex/required-section` and the `pdf/*` rules find nothing, and `tex/venue-leftover` warns once, on its planted line |
75
98
 
76
99
  The `fallback` row is the point of the whole file. `acmart.cls` checks for `libertine.sty`,
77
100
  `zi4.sty` and `newtxmath.sty`, and failing to find any of them sets `\@ACM@newfontsfalse` and
@@ -79,28 +102,30 @@ typesets the paper in Computer Modern. The build is green, the PDF looks fine, t
79
102
  and therefore so does the pagination. A submitted paper went out that way. **An exit code cannot
80
103
  see it, so the content of the artifact is what gets measured.**
81
104
 
82
- Before the fixtures, the run proves the refusal with no TeX at all: PATH holds `node` alone, the
105
+ Beside the fixtures, one test proves the refusal with no TeX at all: PATH holds `node` alone, the
83
106
  cache directory is empty and `CI` is set, and `paperlint build` must exit 1 with one line naming
84
- `npx paperlint toolchain` and the venue's packages, print no plan and create no PDF and no cache. It then
85
- asks `paperlint build --dry-run` which TeX Live the real run will use; under `--strict` (CI) that must be
86
- paperlint's own cache, because the runner has no other.
107
+ `npx paperlint toolchain` and the venue's packages, print no plan and create no PDF and no cache.
108
+ The rest asks `paperlint build --dry-run` which TeX Live the real run will use; under CI that must
109
+ be paperlint's own cache, because the runner has no other.
87
110
 
88
- ## `test/e2e/toolchain.ts` — real TeX Live, and only it
111
+ ## `test/e2e/tex/toolchain.e2e.ts` — real TeX Live, and only it
89
112
 
90
113
  `paperlint toolchain` into `$PAPERLINT_TEXLIVE_DIR` against real CTAN; a second run must say "nothing to do"
91
114
  within seconds; `--check` must exit 0; then the `acmart` fixture is built with PATH holding `node`
92
115
  only, so no other TeX Live and no PDF tool can stand in, and the PDF must carry Libertine and Biolinum
93
- and no Computer Modern face. Without `PAPERLINT_TEXLIVE_DIR` it is a declared skip: installing ~270 MB
116
+ and no Computer Modern face. Without `PAPERLINT_TEXLIVE_DIR` its tests are skipped: installing ~270 MB
94
117
  into a home directory as a side effect of `npm run check` is the unasked install rule 11 forbids.
95
118
  `src/toolchain.harness.mjs` covers the installer's logic (mirror fallback, archive check, time
96
119
  limit, verification, idempotence) against a fake mirror on disk, without the network.
97
120
 
98
- ## Skips are declared, never silent
121
+ ## Skips are reported, never passed
99
122
 
100
123
  A clone without TeX Live genuinely cannot run the build e2e; a machine without pnpm cannot run
101
- half of the install e2e. Each says so and exits **77** — the skip code vigiles' own runner uses —
102
- not zero, and `npm run check` reports it as `SKIPPED — not run, not passed`. In CI the same absence
103
- means a broken environment, so `--strict` turns the skip into a failure.
124
+ half of the install e2e. Their tests are then `skipIf`'d, and vitest reports them as skipped.
125
+ `npm run check` reads that count from vitest's JSON report and prints the gate as
126
+ `SKIPPED — not run, not passed`. In CI the same absence means a broken environment: with `CI` set
127
+ (or `PAPERLINT_E2E_STRICT=1`) each such file registers one more test that fails and names what is
128
+ missing (`test/e2e/need.ts`).
104
129
 
105
130
  A skipped step and a passed one look identical in a CI interface. That is the class this whole
106
131
  package is written against, so it is not allowed to happen inside it either.
@@ -115,9 +140,13 @@ Not every change does. The trigger is countable — ask whether the change touch
115
140
  - anything read through `package.json` fields — `bin`, `files`, `exports`
116
141
  - a new external program the pipeline shells out to
117
142
  - a new artifact the pipeline produces whose **content** can be wrong while the exit code is zero
143
+ - a new template-family preset: the TeX packages it declares, and the page size and fonts measured
144
+ from a real build of its class → one fixture folder in `fixtures/build-e2e/`, with its
145
+ `expect.json`
118
146
 
119
- If none of those apply, a harness beside the changed module is the right test, and an e2e run
120
- would only make the suite slower without making it stricter.
147
+ If none of those apply, a unit or integration test beside the changed module is the right test,
148
+ and an e2e run would only make the suite slower without making it stricter. A new rule is not a
149
+ trigger by itself: its cases in `src/rule-cases.test.ts` already run it through real ESLint.
121
150
 
122
151
  ## What these two do NOT cover
123
152
 
@@ -142,7 +171,7 @@ everything.
142
171
 
143
172
  ## The corpus
144
173
 
145
- `stageCorpus()` in `test/e2e/install.ts` writes a small paper by hand — a declared stage, its
174
+ `stageCorpus()` in `test/e2e/install/install.e2e.ts` writes a small paper by hand — a declared stage, its
146
175
  PDF, its byte counts, the cross-check between them — and copies `fixtures/build-e2e/acmart` beside
147
176
  it so the LaTeX rules see LaTeX rather than a placeholder.
148
177
 
package/docs/install.md CHANGED
@@ -121,7 +121,7 @@ a CI workflow and a heavy toolchain), asks two questions and guesses the rest.
121
121
 
122
122
  **npm and pnpm are covered; Yarn Plug'n'Play is not supported.**
123
123
 
124
- `test/e2e/install.ts` packs the tarball, installs it into a clean project with each manager that
124
+ `test/e2e/install/install.e2e.ts` packs the tarball, installs it into a clean project with each manager that
125
125
  launches on the machine, and runs the hook command to see whether it resolves. A grep over
126
126
  `hooks.json` would not do: the string is right under any manager, and whether it resolves depends
127
127
  on the tree the manager laid out. It also checks that every shipped skill is reachable as
@@ -1,7 +1,7 @@
1
1
  # Prior art — how comparable tools are shaped
2
2
 
3
3
  **Why this folder lives in the repository.** These are technical records that code and other docs
4
- cite by path: `test/e2e/install.ts` cites `package-location.md`, `scripts/marketplace-shape.mjs`
4
+ cite by path: `test/e2e/install/install.e2e.ts` cites `package-location.md`, `scripts/marketplace-shape.mjs`
5
5
  and `docs/install.md` point at the probes in `repro/`, and `eslint.config.mjs` excludes `repro/`
6
6
  by name. Moving the folder out would leave those references pointing nowhere. The notes are for
7
7
  contributors and maintainers, not for someone checking a paper, which is why the README does not
@@ -1,6 +1,6 @@
1
1
  # How a package finds its own installed files, and its own bin
2
2
 
3
- **Question this file answers:** `test/e2e/install.ts` inspects the tree it just installed by
3
+ **Question this file answers:** `test/e2e/install/install.e2e.ts` inspects the tree it just installed by
4
4
  spelling the layout out — `node_modules/research-paper-pipeline` (line 172),
5
5
  `node_modules/.bin/rpp` (line 276), and `node_modules/.../plugin/hooks/hooks.json` (line 135).
6
6
  `contentDelivery()` separately tries THREE candidate base directories for a path written as prose
@@ -54,7 +54,7 @@ argument of `import.meta.resolve` is the non-standard part:
54
54
 
55
55
  ### 🔴 `import.meta.resolve` is the wrong tool here, and it fails SILENTLY
56
56
 
57
- `test/e2e/install.ts` runs from the repository and inspects a consumer somewhere in `os.tmpdir()`.
57
+ `test/e2e/install/install.e2e.ts` runs from the repository and inspects a consumer somewhere in `os.tmpdir()`.
58
58
  `import.meta.resolve` always resolves from **the file that calls it** — `cwd` is irrelevant and
59
59
  the `parent` argument is ignored without the flag rather than rejected:
60
60
 
@@ -165,7 +165,7 @@ literal node_modules/ : <WORK>/consumer-pnpm/node_modules/research-paper-pipelin
165
165
  Under pnpm the hardcode addresses the **symlink** and resolution addresses the **store realpath**
166
166
  (Node realpaths by default; `--preserve-symlinks` flips it, and `resolve-package-path` carries
167
167
  `lib/should-preserve-symlinks.js` for exactly that). `realpathSync` of one equals the other, so
168
- every `existsSync` / `readdirSync` / `readFileSync` in `test/e2e/install.ts` gives the same answer
168
+ every `existsSync` / `readdirSync` / `readFileSync` in `test/e2e/install/install.e2e.ts` gives the same answer
169
169
  either way. Two interesting asymmetries, both measured:
170
170
 
171
171
  - `module.findPackageJSON` returns the **symlink** path under pnpm, not the store path — the
@@ -348,7 +348,7 @@ node_modules present? NO
348
348
  today the npm channel is the one that delivers the runnable code — but it is not impossible.
349
349
  3. 🔴 **The blocker the note does NOT name is the immovable one, and no resolution API fixes it.**
350
350
  PnP resolves to a path **inside a zip**. Line 3: the outside process gets `existsSync: false`
351
- for the very path PnP just handed it; only PnP's patched `fs` can open it. `test/e2e/install.ts`
351
+ for the very path PnP just handed it; only PnP's patched `fs` can open it. `test/e2e/install/install.e2e.ts`
352
352
  inspects the installed tree with `existsSync`, `readdirSync` and `readFileSync` **from its own
353
353
  process**. Under PnP those read nothing, whatever API produced the path — and `yarn bin rpp`
354
354
  (line 4) hands back the same unreadable path, so the supported bin lookup does not help either.
@@ -359,7 +359,7 @@ and it should be recorded as such rather than as "PnP has no node_modules".
359
359
 
360
360
  ## 9. The second guess: the base directory for a path written in prose
361
361
 
362
- `contentDelivery()` (in `test/e2e/install.ts`) tries three bases per reference.
362
+ `contentDelivery()` (in `test/e2e/install/install.e2e.ts`) tries three bases per reference.
363
363
  Measured over the real corpus with `claim6-doc-path-candidates.mjs`:
364
364
 
365
365
  ```console
@@ -56,7 +56,7 @@ $ claude plugin validate /tmp/m --strict; echo "RC=$?"
56
56
  ## The CLAIM 4–6 probes — where an installed package is, and how to find it
57
57
 
58
58
  These back [`../package-location.md`](../package-location.md). They measure the two hardcodes in
59
- `test/e2e/install.ts` (`node_modules/<name>` and `node_modules/.bin/rpp`) against the
59
+ `test/e2e/install/install.e2e.ts` (`node_modules/<name>` and `node_modules/.bin/rpp`) against the
60
60
  resolution APIs Node actually offers, under npm, pnpm and Yarn Berry (PnP).
61
61
 
62
62
  Build the consumer trees once — it packs this repository and installs the SAME tarball twice:
@@ -27,16 +27,27 @@ So the genuinely agent-facing harnesses are three: `hooks/hooks.harness.ts` (`ru
27
27
  (`runHarnessTest`, which spawns the real `claude` binary against a scripted model), and the skill
28
28
  contract checks. Everything else is ordinary Node testing that happens to carry the suffix.
29
29
 
30
- ## Why the install e2e stays a script
31
-
32
- `test/e2e/install.ts` is one linear scenario per package manager with strictly dependent steps
33
- — install, bin, `init`, `lint`, hook commands, content delivery — and a summary. A runner adds
34
- named subtests and a reporter; the script already prints per-check `✓`/`✗` and a per-manager
35
- verdict. The pack-and-install work stays in our code under any host, so the host buys only the
36
- reporter, at the cost of a second exit-code path.
37
-
38
- It also stays OUT of `npm test`: it needs the network for the consumer's transitive dependencies
39
- and takes minutes. It is its own CI step with its own timeout.
30
+ ## The e2e runs are vitest projects
31
+
32
+ The end-to-end runs are vitest tests (`test/e2e/<area>/*.e2e.ts`), one project per environment
33
+ they need — `e2e-install` (npm and pnpm) and `e2e-tex` (TeX Live) — found by the project's glob, so
34
+ no list of files exists in `package.json`, `scripts/check.ts` or CI (#132). What the runner buys is
35
+ not the reporter alone:
36
+
37
+ - **the fixture table.** `fixtures/build-e2e/` is a table of independent cases, and each fixture's
38
+ `expect.json` becomes its own tests through one `describe.each` over the directory. A new venue
39
+ fixture is a folder, not a block of test code.
40
+ - **a skip the aggregate can read.** A missing tool is `skipIf`, and vitest's JSON report counts it,
41
+ which is what `npm run check` reads to print a gate as skipped rather than passed. Under CI the
42
+ same absence fails (`test/e2e/need.ts`).
43
+
44
+ `install.e2e.ts` stays ONE test per package manager, each the whole scenario — install, bin,
45
+ `init`, `lint`, hook commands, content delivery — because its steps depend on each other: a
46
+ failure in the install makes every later step meaningless. Inside it, each check is a soft
47
+ expectation, so a run reports every mismatch, not the first.
48
+
49
+ The e2e projects stay OUT of `npm test` (project `unit`): they need the network, TeX Live or
50
+ several minutes. Each runs in its own CI job, with its own timeout.
40
51
 
41
52
  ## Tarball-by-path versus a local registry
42
53
 
@@ -0,0 +1,83 @@
1
+ # tex/claim-provenance
2
+
3
+ **Level:** warn · **Reads:** `paper.tex`
4
+
5
+ ## What it catches
6
+
7
+ A sentence of the body that states a number and does not say whose the number is. The sentence is
8
+ reported once, however many numbers it holds. Any one of these owners silences it:
9
+
10
+ | owner | in the sentence |
11
+ | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
12
+ | a work outside the paper | a `\cite` (any of natbib's or biblatex's forms), a `\url`, an `\href`, or a footnote that cites or links |
13
+ | the authors | _we_, _our_, _ours_, _us_ (the capitalised country _US_ is not the authors) |
14
+ | a place in the paper | `\ref`, `\cref`, `\autoref` and their kin, or the words _Section 4_, _Table 2_, _Fig. 3_, _Appendix B_, _§4_ |
15
+ | a claimant named as one | _advertises_, _claims_: _"the skill advertises a 65% reduction"_, _"(claim 63%)"_ |
16
+ | the sample the number counts | _n = 134_ (in prose or in math), or a count followed by what it counts: _48 runs_, _48 independent runs_, _of 1,836 repositories_ |
17
+
18
+ It reads the body of the whole paper — `paper.tex` with the files it `\input`s, `\include`s or
19
+ `\subfile`s spliced in — from the start of the document, the abstract included, to `\appendix` or the
20
+ bibliography. A sentence in an included file is reported at that include, with the file and line at
21
+ the front of the message. Footnotes are read as sentences of their own. A citation or a footnote set right after
22
+ the full stop belongs to the sentence the full stop ends.
23
+
24
+ A number is digits a reader sees — `42\%`, `0.37`, `1,836`, `3x`, `3×`. Not a number: a year on its
25
+ own (1900–2099); a number right after a capitalised word inside the sentence (_Claude 3_, _Python
26
+ 3.12_, _Node 22_), which is a name; the level of an interval (the 95 of _95% CI_); digits glued to a letter, a hyphen or a point (`GPT-4`, `COVID-19`, `v1.2`, `10k`,
27
+ `0x1F`); anything inside math, a table, a figure, a caption, a heading (a run-in one too: `\textbf{Threats.}` opening a paragraph), code, a comment, a macro
28
+ definition, or the title block.
29
+
30
+ ## Why
31
+
32
+ A reviewer who cannot tell a property of the world from a cited finding from the authors' own
33
+ measurement says so, and scores the paper's soundness on it. Academic prose carries the owner by
34
+ default — _we found_, _[12] report_, _(Table 2)_ — and a sentence rewritten for punch loses it first:
35
+ _"Under test the rules held in 97% of cases"_ has no subject, and a reader takes it
36
+ for a result from the literature. This rule is the floor under the rules that shorten sentences.
37
+
38
+ ## Examples
39
+
40
+ ```latex
41
+ Agents ignore 42\% of the rules they are given. % warns
42
+ Under test the rules held in 97\% of cases. % warns: no one ran the test
43
+ We find that agents ignore 42\% of the rules. % silent: the authors
44
+ Agents ignore 42\% of the rules~\cite{smith2025}. % silent: a citation
45
+ Agents ignore 42\% of the rules (Table~\ref{t:rates}). % silent: a place in the paper
46
+ Of 1,836 repositories, 12\% ship hooks. % silent: the sample
47
+ In 2024 the first agents shipped with GPT-4. % silent: a year and a name
48
+ ```
49
+
50
+ The first line reports:
51
+
52
+ > «42%» — whose number is this? A reader cannot tell a cited finding from your own measurement from
53
+ > a general fact. Say it in the sentence: cite the work (\cite), claim it (we measured, our census),
54
+ > point to where it is shown (\ref, Section N, Table N), or name the sample it counts (n = 134, 48
55
+ > runs)
56
+
57
+ ## Options / preset fields
58
+
59
+ None. It is on, as a warning, for every `paper.tex`; turn it off or up in the `rules` setting like
60
+ any other rule.
61
+
62
+ ## What it does not check
63
+
64
+ - Whether the owner is the right one: a citation that does not report the number, or a _we_ in a
65
+ sentence about someone else's work, silences the sentence all the same.
66
+ - What a custom macro prints: `\rate{}` defined as `42\%` is not a number here.
67
+ - Which numbers are names, beyond the two shapes it knows: a number after a lowercase word (_version
68
+ 3 of the model_) is read as a quantity, and so is the number of a name that opens the sentence
69
+ (_Claude 3 fails …_).
70
+ - Whether a count followed by a plural noun is really the sample (_3 times faster_ passes as one).
71
+ - Markdown papers (`paper.md`): it reads the LaTeX parse tree only.
72
+
73
+ ## How to fix
74
+
75
+ Say whose the number is, in the sentence — usually by putting back the subject a punchy rewrite took
76
+ out: _"We ran the rules in 48 sessions and saw no violation"_, not _"No violation in 48 runs."_ A
77
+ number that is common knowledge, or quoted on purpose, keeps its sentence with ESLint's directive and
78
+ the reason:
79
+
80
+ ```latex
81
+ % eslint-disable-next-line tex/claim-provenance -- the venue's own page limit, quoted
82
+ The limit is 8.
83
+ ```
@@ -0,0 +1,58 @@
1
+ # tex/missing-input
2
+
3
+ **Level:** warn · **Reads:** `paper.tex`, and every file it includes
4
+
5
+ ## What it catches
6
+
7
+ An `\input`, `\include` or `\subfile` that names a file which is not there. The path is resolved the
8
+ way TeX resolves it, from the paper's directory: `name.tex` first, then `name` as written. An
9
+ include inside an included file is resolved from the paper's directory too.
10
+
11
+ ## Why
12
+
13
+ The rules that read the parse tree — `tex/template`, `tex/required-section`, `tex/venue-leftover`,
14
+ `tex/claim-provenance` — read the whole paper: `paper.tex` with every included file spliced in where
15
+ it is included, as TeX reads it. A file that is not there cannot be read, and without this rule the
16
+ others would report a clean body for text they never saw. TeX stops on it too.
17
+
18
+ ## Examples
19
+
20
+ ```latex
21
+ \input{sections/results} % warns when neither sections/results.tex nor sections/results exists
22
+ \input{sections/intro} % silent: sections/intro.tex is there
23
+ ```
24
+
25
+ The first line reports:
26
+
27
+ > `sections/results` is not there (tried `sections/results.tex` and `sections/results`, from the
28
+ > paper's directory): TeX stops on it, and no rule read what it should hold
29
+
30
+ A missing file included by another included file is reported at the include in `paper.tex` that
31
+ brought that file in, and the message names the file that holds it.
32
+
33
+ ## Options / preset fields
34
+
35
+ None.
36
+
37
+ ## What it does not check
38
+
39
+ - A path built from a macro (`\input{\dir/intro}`): macros are not expanded, so the path is looked
40
+ up as written.
41
+ - `\import{dir}{file}` and other packages' include macros.
42
+
43
+ ## How to fix
44
+
45
+ Correct the path, or add the file. A file that a build step writes (a table of generated numbers)
46
+ is absent before that step runs — keep the include with a disable directive and the reason:
47
+
48
+ ```latex
49
+ % eslint-disable-next-line tex/missing-input -- written by repro/numbers.sh before the build
50
+ \input{generated/numbers}
51
+ ```
52
+
53
+ ## Where findings in included files are reported
54
+
55
+ ESLint reports a finding in the file it lints, and paperlint lints `paper.tex`. A finding of any of
56
+ the rules above whose text came from an included file is reported at the include in `paper.tex`,
57
+ with the included file, line and column at the front of its message
58
+ (`sections/results.tex:12:3: …`). A disable directive for it goes above that include.
@@ -0,0 +1,77 @@
1
+ # tex/register
2
+
3
+ **Level:** warn · **Reads:** `paper.tex` and the files it includes
4
+
5
+ ## What it catches
6
+
7
+ A body in which more than 0.8 sentences per 1000 words open with _And_, _So_, _But_, _Nor_, _Or_ or
8
+ _Yet_. It is reported once, at the body's first sentence, with the count, the rate and the first
9
+ three such sentences.
10
+
11
+ It reads the body of the whole paper — `paper.tex` with the files it `\input`s, `\include`s or
12
+ `\subfile`s spliced in — from the start of the document, the abstract included, to `\appendix` or the
13
+ bibliography: paragraphs, list items and footnotes. Headings are not sentences, and neither is a
14
+ run-in heading — a bold or italic phrase ending in `.` or `:` that opens a paragraph or an item
15
+ (`\textbf{Threats.} …`). Captions, tables, figures, code and math environments are left out. A
16
+ citation, a cross-reference or inline math counts as one word, as it reads on the page. Sentences
17
+ are cut exactly as [`tex/claim-provenance`](claim-provenance.md) cuts them.
18
+
19
+ A conjunction counts only as the capitalised first word of a sentence; the _and_ that opens a list
20
+ item continuing the sentence before the list does not.
21
+
22
+ A body under 2 000 words is not judged: there one sentence decides the rate.
23
+
24
+ ## Why
25
+
26
+ Reviewers called our prose "too informal" and "a blogpost". Measured against eleven accepted
27
+ papers, sentences opening with a coordinating conjunction set our four drafts apart:
28
+
29
+ | measure | 11 accepted papers | our 4 drafts |
30
+ | ------------------------------------------------------------ | ---------------------------: | -----------: |
31
+ | sentences opening with And/So/But/Nor/Or/Yet, per 1000 words | 0–0.58 (the ACSAC papers: 0) | 0.29–2.98 |
32
+
33
+ The limit sits above the accepted range with room to spare: the rule flags a body that has drifted
34
+ to a post's register, not one a little above the median. Mean sentence length did not separate the
35
+ two groups. The share of sentences under eight words did not either, once measured on the parse
36
+ tree: 3.3–7.3% on the two accepted papers in this repository's corpus, 5.1–7.0% on three of ours.
37
+
38
+ On the corpus (`fixtures/accepted-papers/`) the rule measures:
39
+
40
+ | paper | per 1000 words |
41
+ | ----------------------------------- | -------------: |
42
+ | `secure-acsac24` (ACSAC 2024) | 0.00 |
43
+ | `llm-splained-acsac25` (ACSAC 2025) | 0.00 |
44
+ | `agenticdev-acm26` (ours, accepted) | 1.60 |
45
+
46
+ The vocabulary of machine-written text (_delve_, _crucial_) is a different question, and a different
47
+ check; a paper can avoid every such word and still read as a post.
48
+
49
+ ## Examples
50
+
51
+ ```latex
52
+ We measured the cost on four tools. But the bill barely moves. % a conjunction start
53
+ We measured the cost on four tools, and the bill barely moves. % none
54
+ We measured the cost on four tools. However, the bill barely moves. % none
55
+ ```
56
+
57
+ On a body where such sentences run above the limit, the rule reports:
58
+
59
+ > 7 sentences open with And, So, But, Nor, Or or Yet — 1.60 per 1000 words (first: «…», «…», «…»).
60
+ > Accepted papers measured 0–0.58; this rule warns above 0.8. Join the sentence to the one before,
61
+ > or open with the connective a paper uses (However, Therefore, Moreover)
62
+
63
+ ## Options / preset fields
64
+
65
+ In a paper's `paperlint.json`:
66
+
67
+ ```json
68
+ {
69
+ "rules": {
70
+ "tex/register": ["warn", { "conjunctionStartsPer1000": 0.8 }]
71
+ }
72
+ }
73
+ ```
74
+
75
+ | option | default | what it is |
76
+ | -------------------------- | ------: | ---------------------------------------------------------- |
77
+ | `conjunctionStartsPer1000` | 0.8 | the conjunction starts per 1000 words above which it warns |
@@ -0,0 +1,75 @@
1
+ # tex/required-section
2
+
3
+ **Level:** error · **Reads:** `paper.tex`, and the preset its `paperlint.json` extends
4
+
5
+ ## What it catches
6
+
7
+ A section the venue requires is missing, or is not where the venue requires it. The preset lists
8
+ the required sections by title in `required_sections`; the paper must have a `\section` or
9
+ `\section*` with exactly that title (whitespace collapsed), and, when the preset says
10
+ `position: "last"`, no section of the main text may come after it.
11
+
12
+ It reads the sections of the whole paper, the files `paper.tex` includes spliced in where they are
13
+ included, so a required section kept in `sections/llm-usage.tex` counts.
14
+
15
+ It only knows the sections a preset lists. A paper whose `paperlint.json` names no preset, or a
16
+ preset with no `required_sections`, gets no finding.
17
+
18
+ ## Why
19
+
20
+ Some venues make a section mandatory and desk-reject without it. ACSAC's AI Usage Policy, which
21
+ its AIDC workshop applies (https://www.acsac.org/2026/submissions/ai/):
22
+
23
+ > If LLMs are used, authors must include a separate, clearly marked section titled "LLM Usage
24
+ > Statement" at the end of the paper. This section does not count towards the page limit.
25
+
26
+ > Failure to comply with these requirements is grounds for desk rejection without further review.
27
+
28
+ A common near miss is a bold paragraph in another venue's wording («Use of Generative AI.»): not a
29
+ section, and not that title.
30
+
31
+ **"If LLMs are used" is not something a rule can know, so the rule always requires the section for
32
+ a preset that lists it.** An author who used none says so in it («No LLMs were used in this
33
+ work.»).
34
+
35
+ ## Examples
36
+
37
+ Under a preset with
38
+ `"required_sections": [{ "title": "LLM Usage Statement", "position": "last" }]`:
39
+
40
+ ```latex
41
+ \paragraph{Use of Generative AI.} We used an LLM. % fails: a paragraph, not a section
42
+ \section*{LLM usage statement} % fails: not the exact title
43
+ \section*{LLM Usage Statement} % passes, after the last section of the text
44
+ ```
45
+
46
+ A missing section reports:
47
+
48
+ > aidc requires a section titled «LLM Usage Statement» and there is none — add
49
+ > `\section*{LLM Usage Statement}` (the title exactly; a bold paragraph does not count)
50
+
51
+ The right title in the wrong place — before the introduction — reports:
52
+
53
+ > «LLM Usage Statement» must close the paper at aidc, and the section «Introduction» comes after it
54
+ > — move it after the last section of the body (after the bibliography is fine)
55
+
56
+ ## Preset field
57
+
58
+ No rule options. It reads `required_sections`; a child preset's list replaces its parent's:
59
+
60
+ ```jsonc
61
+ "required_sections": [{ "title": "LLM Usage Statement", "position": "last" }]
62
+ ```
63
+
64
+ `title` is compared exactly after collapsing whitespace — the venue's words are the contract.
65
+ `position: "last"` requires it after every section of the main text, which is every section before
66
+ `\appendix` and the bibliography (`\bibliography`, `\printbibliography`, `thebibliography`); after
67
+ the bibliography or an appendix is fine. Without `position`, anywhere will do.
68
+
69
+ ## What it does not check
70
+
71
+ - Whether LLMs were used, or what the section says.
72
+
73
+ ## How to fix
74
+
75
+ Add `\section*{<the title>}` where the message says, with the content the venue asks for.