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
package/src/pdf-facts.ts CHANGED
@@ -34,6 +34,15 @@ import {
34
34
  import type { PageLayout, TextBox } from "./domain/page-layout.ts";
35
35
  // eslint-disable-next-line boundaries/dependencies -- legacy layer, moves behind a port in #76
36
36
  import { fillsFor, isUpright } from "./adapters/pdfjs/fill.ts";
37
+ import {
38
+ appendixOutlineDests,
39
+ linkTargetsOf,
40
+ metadataOf,
41
+ namedRefs,
42
+ pageTextOf,
43
+ refOf,
44
+ xmpOf,
45
+ } from "./pdf-content.ts";
37
46
  import { messageOf } from "./domain/text.ts";
38
47
  import { fieldOf, isRecord, numbersOf } from "./domain/record.ts";
39
48
 
@@ -83,12 +92,27 @@ interface Page {
83
92
  [Symbol.iterator](): Iterator<readonly unknown[]>;
84
93
  };
85
94
  getViewport(params: { scale: number }): Viewport;
95
+ /** pdf.js's annotations; the link ones carry `url`. */
96
+ getAnnotations(): Promise<readonly unknown[]>;
86
97
  }
87
98
 
88
99
  /** A document as this module reads it; pdf.js's `PDFDocumentProxy` is one. */
89
100
  interface Doc {
90
101
  readonly numPages: number;
91
102
  getPage(pageNumber: number): Promise<Page>;
103
+ /** The Info dictionary, and the XMP packet when there is one. */
104
+ getMetadata(): Promise<{
105
+ readonly info: unknown;
106
+ readonly metadata: Iterable<unknown> | null;
107
+ }>;
108
+ /** Every named destination: name → explicit destination. */
109
+ getDestinations(): Promise<unknown>;
110
+ /** The 0-based index of the page a destination's reference names. */
111
+ getPageIndex(ref: object): Promise<number>;
112
+ /** One named destination, or null. */
113
+ getDestination(id: string): Promise<unknown>;
114
+ /** The bookmarks: `{ title, dest, items }`, or null. */
115
+ getOutline(): Promise<unknown>;
92
116
  }
93
117
 
94
118
  /** A viewport point, which pdf.js types as `any[]`: the two numbers it holds. */
@@ -106,6 +130,22 @@ export interface PdfFacts {
106
130
  readonly fonts: Fonts;
107
131
  readonly last: PageText;
108
132
  readonly layout: readonly PageLayout[];
133
+ /** Each page's text, in content order, a line break after each line (`pageTextOf`). */
134
+ readonly pageTexts: readonly string[];
135
+ /** The target of every link, with its 1-based page. */
136
+ readonly links: readonly PdfLink[];
137
+ /** The 1-based page of the first bibliography entry hyperref anchored, or null without one. */
138
+ readonly bibAnchorPage: number | null;
139
+ /** The 1-based page the appendix starts on, from hyperref's destinations and bookmarks, or null. */
140
+ readonly appendixAnchorPage: number | null;
141
+ /** The Info dictionary and XMP packet, flat (`metadataOf`). */
142
+ readonly metadata: Readonly<Record<string, string>>;
143
+ }
144
+
145
+ /** A link annotation's target, and the page it is on. */
146
+ export interface PdfLink {
147
+ readonly page: number;
148
+ readonly uri: string;
109
149
  }
110
150
 
111
151
  export type PdfReadFailure =
@@ -198,10 +238,17 @@ async function readPage(
198
238
  page: Page,
199
239
  seen: ReadonlySet<string>,
200
240
  lib: PdfJs,
201
- ): Promise<{ raw: RawPage; items: TextItem[]; layout: PageLayout }> {
241
+ ): Promise<{
242
+ raw: RawPage;
243
+ items: TextItem[];
244
+ layout: PageLayout;
245
+ text: string;
246
+ links: readonly string[];
247
+ }> {
202
248
  // The operator list is what makes pdf.js load a page's fonts into `commonObjs`.
203
249
  const ops = await page.getOperatorList();
204
- const items = (await page.getTextContent()).items.filter(isText);
250
+ const all = (await page.getTextContent()).items;
251
+ const items = all.filter(isText);
205
252
  const loaded = loadedFonts(page);
206
253
  const named = new Set(items.map((it) => it.fontName));
207
254
  const fonts = [...loaded.values()].filter(
@@ -211,6 +258,8 @@ async function readPage(
211
258
  raw: { hasText: items.length > 0, fonts },
212
259
  items,
213
260
  layout: layoutOf(page, items, ops, lib),
261
+ text: pageTextOf(all),
262
+ links: linkTargetsOf(await page.getAnnotations()),
214
263
  };
215
264
  }
216
265
 
@@ -310,12 +359,7 @@ function layoutOf(
310
359
  * transform, no pages) — `readPdf` below is the one production caller.
311
360
  */
312
361
  export async function factsOf(doc: Doc, lib: PdfJs): Promise<PdfRead> {
313
- const read: {
314
- page: Page;
315
- raw: RawPage;
316
- items: TextItem[];
317
- layout: PageLayout;
318
- }[] = [];
362
+ const read: (Awaited<ReturnType<typeof readPage>> & { page: Page })[] = [];
319
363
  const seen = new Set<string>();
320
364
  for (let i = 1; i <= doc.numPages; i++) {
321
365
  const page = await doc.getPage(i);
@@ -340,10 +384,64 @@ export async function factsOf(doc: Doc, lib: PdfJs): Promise<PdfRead> {
340
384
  fonts,
341
385
  last: pageText(last.page, last.items),
342
386
  layout: read.map((r) => r.layout),
387
+ ...saidOf(read, await doc.getMetadata()),
388
+ ...(await anchorsOf(doc)),
343
389
  },
344
390
  };
345
391
  }
346
392
 
393
+ /**
394
+ * Where hyperref anchored the bibliography's first entry (the lowest page of a `cite.<key>`
395
+ * destination) and the appendix (the lowest page of an `appendix.<letter>` destination or of a
396
+ * bookmark titled «Appendix…»). A reference pdf.js cannot place is skipped.
397
+ */
398
+ async function anchorsOf(
399
+ doc: Doc,
400
+ ): Promise<Pick<PdfFacts, "bibAnchorPage" | "appendixAnchorPage">> {
401
+ const destinations = await doc.getDestinations();
402
+ const bookmarked = await Promise.all(
403
+ appendixOutlineDests(await doc.getOutline()).map(async (d) =>
404
+ refOf(typeof d === "string" ? await doc.getDestination(d) : d),
405
+ ),
406
+ );
407
+ return {
408
+ bibAnchorPage: await lowestPage(doc, namedRefs(destinations, "cite.")),
409
+ appendixAnchorPage: await lowestPage(doc, [
410
+ ...namedRefs(destinations, "appendix."),
411
+ ...bookmarked.filter((r): r is object => r !== null),
412
+ ]),
413
+ };
414
+ }
415
+
416
+ /** The lowest 1-based page any of `refs` names, or null when none can be placed. */
417
+ async function lowestPage(
418
+ doc: Doc,
419
+ refs: readonly object[],
420
+ ): Promise<number | null> {
421
+ const pages = await Promise.all(
422
+ refs.map((r) =>
423
+ doc.getPageIndex(r).then(
424
+ (i) => [i + 1],
425
+ () => [],
426
+ ),
427
+ ),
428
+ );
429
+ const found = pages.flat();
430
+ return found.length === 0 ? null : Math.min(...found);
431
+ }
432
+
433
+ /** What the pages say, from each page's read, and the document's metadata. */
434
+ function saidOf(
435
+ read: readonly Pick<Awaited<ReturnType<typeof readPage>>, "text" | "links">[],
436
+ meta: Awaited<ReturnType<Doc["getMetadata"]>>,
437
+ ): Pick<PdfFacts, "pageTexts" | "links" | "metadata"> {
438
+ return {
439
+ pageTexts: read.map((r) => r.text),
440
+ links: read.flatMap((r, i) => r.links.map((uri) => ({ page: i + 1, uri }))),
441
+ metadata: metadataOf(meta.info, xmpOf(meta.metadata)),
442
+ };
443
+ }
444
+
347
445
  /** pdf.js's own exception names for the two conditions a caller can act on. */
348
446
  export function failureOf(e: unknown): PdfRead {
349
447
  const name = fieldOf(e, "name");
@@ -6,6 +6,7 @@ import type { Include } from "../domain/paper-source.ts";
6
6
  import type {
7
7
  ClassLine,
8
8
  DocumentClass,
9
+ LayoutOverride,
9
10
  Outline,
10
11
  Passage,
11
12
  Span,
@@ -39,4 +40,6 @@ export interface LatexReader {
39
40
  * option `want` names (the author's own options stay), or it has none.
40
41
  */
41
42
  readonly withDocumentClass: (src: string, want: DocumentClass) => string;
43
+ /** Every command that changes the page layout the class sets: margins, text block, line spacing. */
44
+ readonly layoutOverrides: (src: string) => readonly LayoutOverride[];
42
45
  }
package/src/presets.ts CHANGED
@@ -83,6 +83,8 @@ export interface Preset {
83
83
  readonly template: PresetTemplate | null;
84
84
  /** Every `name` and `aliases` along the chain: what this venue is called in a paper's text. */
85
85
  readonly aliases: readonly string[];
86
+ /** Whether the venue reviews double-blind (`anonymity/identity`); false when no file of the chain says. */
87
+ readonly blind: boolean;
86
88
  readonly requiredSections: readonly RequiredSection[];
87
89
  readonly tex: TexRequirements;
88
90
  readonly format: VenueFormat;
@@ -196,6 +198,7 @@ export function mergeFormat(
196
198
  refPtMax: pick("refPtMax"),
197
199
  fontsText: pick("fontsText"),
198
200
  fontsTitle: pick("fontsTitle"),
201
+ bodyEndsAt: pick("bodyEndsAt"),
199
202
  kinds: new Map([...parent.kinds, ...child.kinds]),
200
203
  };
201
204
  }
@@ -205,18 +208,13 @@ function merged(
205
208
  spec: string,
206
209
  rootFirst: readonly { readonly preset: PresetFile; readonly file: string }[],
207
210
  ): Preset {
208
- const base: {
209
- name: string | null;
210
- template: PresetTemplate | null;
211
- aliases: readonly string[];
212
- requiredSections: readonly RequiredSection[];
213
- tex: TexRequirements;
214
- format: VenueFormat;
215
- rules: Readonly<Record<string, unknown>>;
211
+ const base: Omit<Preset, "label" | "chain"> & {
212
+ readonly name: string | null;
216
213
  } = {
217
214
  name: null,
218
215
  template: null,
219
216
  aliases: [],
217
+ blind: false,
220
218
  requiredSections: [],
221
219
  tex: NO_REQUIREMENTS,
222
220
  format: NO_FORMAT,
@@ -233,6 +231,7 @@ function merged(
233
231
  ...p.aliases,
234
232
  ]),
235
233
  ],
234
+ blind: p.blind ?? acc.blind,
236
235
  requiredSections: p.requiredSections ?? acc.requiredSections,
237
236
  tex: p.tex ? mergeRequirements(acc.tex, p.tex) : acc.tex,
238
237
  format: mergeFormat(acc.format, p.format),
@@ -245,6 +244,7 @@ function merged(
245
244
  chain: rootFirst.map((x) => x.file),
246
245
  template: m.template,
247
246
  aliases: m.aliases,
247
+ blind: m.blind,
248
248
  requiredSections: m.requiredSections,
249
249
  tex: m.tex,
250
250
  format: m.format,
@@ -43,11 +43,22 @@ const paper = (
43
43
 
44
44
  const PDF = "%PDF-1.5 a stand-in for the built PDF";
45
45
  const AGENTICDEV = { extends: "paperlint:agenticdev", kind: "short" };
46
+ /** A double-blind venue's paper that declares who wrote it (`anonymity/*`). */
47
+ const BLIND = {
48
+ extends: "paperlint:aidc",
49
+ kind: "regular",
50
+ identity: ["Ada Example", "adaexample"],
51
+ };
46
52
  const ACM_TEX = tex("Text.", "\\documentclass[sigconf]{acmart}");
47
53
 
48
54
  /** The facts of a short agenticdev paper that meets every number in its preset. */
49
55
  const goodFacts = (): Record<string, unknown> => ({
50
- schema: 2,
56
+ schema: 3,
57
+ pages_text: [],
58
+ metadata: {},
59
+ links: [],
60
+ bib_anchor_page: null,
61
+ appendix_anchor_page: null,
51
62
  pdf: "paper.pdf",
52
63
  pdf_sha256: sha256Hex(new TextEncoder().encode(PDF)),
53
64
  venue: "agenticdev",
@@ -186,25 +197,6 @@ const CASES: Readonly<Record<string, RuleCases>> = {
186
197
  },
187
198
  silent: paper(tex("x")),
188
199
  },
189
- "paper/research-question": {
190
- reports: {
191
- tree: {
192
- [STATUS_FILE]: fixture(
193
- "paper-research-question/shipped-no-rq/PIPELINE-STATUS.md",
194
- ),
195
- [TEX_FILE]: fixture("paper-research-question/shipped-no-rq/paper.tex"),
196
- },
197
- file: TEX_FILE,
198
- severity: 1,
199
- line: 1,
200
- },
201
- silent: {
202
- [STATUS_FILE]: fixture(
203
- "paper-research-question/shipped-with-rq/PIPELINE-STATUS.md",
204
- ),
205
- [TEX_FILE]: fixture("paper-research-question/shipped-with-rq/paper.tex"),
206
- },
207
- },
208
200
  "paper/section-word": {
209
201
  reports: {
210
202
  tree: paper(tex("As in §2.")),
@@ -365,17 +357,6 @@ const CASES: Readonly<Record<string, RuleCases>> = {
365
357
  },
366
358
  silent: paper(tex(FORMAL)),
367
359
  },
368
- "tex/missing-input": {
369
- reports: {
370
- tree: paper(tex("Text.\n\\input{sections/gone}")),
371
- file: TEX_FILE,
372
- severity: 1,
373
- line: 4,
374
- },
375
- silent: paper(tex("Text.\n\\input{sections/here}"), {
376
- [`${P}/sections/here.tex`]: "More text.\n",
377
- }),
378
- },
379
360
  "bib/reachable-entry": {
380
361
  reports: {
381
362
  tree: paper(inlineBib("@book{a,\n title = {A},\n year = {2026}\n}")),
@@ -411,16 +392,31 @@ const CASES: Readonly<Record<string, RuleCases>> = {
411
392
  reports: onPdf(built({ page_w_in: 8.264 })),
412
393
  silent: built(),
413
394
  },
414
- "pdf/limits": {
395
+ "format/page-limit": {
415
396
  reports: onPdf(
416
397
  built({ body_pages: 9, pages_by_type: { body: 9, bib: 2 } }),
417
398
  ),
418
399
  silent: built(),
419
400
  },
420
401
  "pdf/body-size": {
421
- reports: { ...onPdf(built({ body_pt: 12 })), severity: 1 },
402
+ reports: onPdf(built({ body_pt: 12 })),
422
403
  silent: built(),
423
404
  },
405
+ "anonymity/identity": {
406
+ reports: onPdf(
407
+ built({ pages_text: ["Ada Example\n", "References\n"] }, BLIND),
408
+ ),
409
+ silent: built({ pages_text: ["Anonymous\n", "References\n"] }, BLIND),
410
+ },
411
+ "format/layout-override": {
412
+ reports: {
413
+ tree: aidc("\\linespread{0.9}Text."),
414
+ file: TEX_FILE,
415
+ severity: 2,
416
+ line: 4,
417
+ },
418
+ silent: aidc("\\vspace{-2mm}Text."),
419
+ },
424
420
  "pdf/last-page-balance": {
425
421
  reports: onPdf(
426
422
  built(
@@ -26,16 +26,13 @@ const AWAITING_PAGE: ReadonlySet<string> = new Set([
26
26
  "paper/leading-zero",
27
27
  "paper/refs-checked",
28
28
  "paper/refs-fresh",
29
- "paper/research-question",
30
29
  "paper/section-word",
31
30
  "paper/source",
32
31
  "paper/stages",
33
- "pdf/body-size",
34
32
  "pdf/fonts",
35
33
  "pdf/fresh",
36
34
  "pdf/geometry",
37
35
  "pdf/last-page-balance",
38
- "pdf/limits",
39
36
  "pdf/measured",
40
37
  "pdf/profile",
41
38
  "review/frontmatter",
@@ -64,6 +64,7 @@ export interface ConfigBlock {
64
64
  readonly languageOptions?: Readonly<Record<string, unknown>>;
65
65
  readonly linterOptions?: Readonly<Record<string, unknown>>;
66
66
  readonly rules?: Readonly<Record<string, unknown>>;
67
+ readonly settings?: Readonly<Record<string, unknown>>;
67
68
  }
68
69
 
69
70
  export type Parsed<T> =
@@ -79,9 +79,7 @@ try {
79
79
  check(
80
80
  "and the message names the CONSEQUENCE, not a restatement of the condition",
81
81
  /are skipped/.test(at("no-scorecard")[0]?.message ?? "") &&
82
- /stage, source and research-question checks/.test(
83
- at("no-scorecard")[0]?.message ?? "",
84
- ),
82
+ /stage and source checks/.test(at("no-scorecard")[0]?.message ?? ""),
85
83
  );
86
84
  check(
87
85
  "and the consequence names THIS directory by name",
package/src/structure.ts CHANGED
@@ -122,7 +122,7 @@ export function checkStructure(
122
122
  */
123
123
  function whyMissingMatters(file: string, dirName: string): string {
124
124
  if (file === "PIPELINE-STATUS.md")
125
- return `the stage, source and research-question checks are skipped for \`${dirName}\``;
125
+ return `the stage and source checks are skipped for \`${dirName}\``;
126
126
  return `declared as required by your \`structure\` configuration`;
127
127
  }
128
128
 
@@ -1,81 +1,98 @@
1
1
  /**
2
- * `tex/missing-input` and the reading every parse-tree rule shares (`readPaper`): an include that
3
- * names no file is reported where it stands in `paper.tex`, once, whether `paper.tex` wrote it or a
4
- * file it includes did; an include that resolves says nothing.
2
+ * The reading every parse-tree rule shares (`readPaper`), the files of the paper's body that lint
3
+ * reads on their own (`bodyFiles`), and where a finding of the assembled text is reported
4
+ * (`reportInPaper`).
5
5
  */
6
6
  import { describe, expect, it } from "vitest";
7
7
  import { latexReader } from "./adapters/latex/index.ts";
8
8
  import { memoryFiles } from "./adapters/memory/index.ts";
9
- import { missingInputRule, readPaper, reportInPaper } from "./tex-paper.ts";
9
+ import { join } from "node:path";
10
+ import { texInputsDir } from "./package-dirs.ts";
11
+ import { bodyFiles, readPaper, reportInPaper } from "./tex-paper.ts";
10
12
 
11
- const doc = (body: string): string =>
12
- `\\documentclass{article}\n\\begin{document}\n${body}\n\\end{document}\n`;
13
+ describe("readPaper — the paper as the rules read it", () => {
14
+ it("reads includes from the paper's own directory", () => {
15
+ const p = readPaper("/p/paper.tex", "x\\input{s/a}y", {
16
+ files: memoryFiles({ "/p/s/a.tex": "AAA" }),
17
+ latex: latexReader,
18
+ });
19
+ expect(p.text).toBe("xAAAy");
20
+ expect(p.main).toBe("paper.tex");
21
+ });
13
22
 
14
- /** What the rule reports over `/p/paper.tex` holding `src`, with `files` on disk. */
15
- function run(src: string, files: Record<string, string> = {}) {
16
- const out: { line: number; messageId: string; data: unknown }[] = [];
17
- const rule = missingInputRule({
18
- files: memoryFiles(files),
19
- latex: latexReader,
23
+ it("an include the paper's directory lacks is found in paperlint's own inputs, as the build finds it", () => {
24
+ const p = readPaper("/p/paper.tex", "x\\input{guards}y", {
25
+ files: memoryFiles({ [join(texInputsDir(), "guards.tex")]: "G" }),
26
+ latex: latexReader,
27
+ });
28
+ expect({ text: p.text, missing: p.missing }).toEqual({
29
+ text: "xGy",
30
+ missing: [],
31
+ });
20
32
  });
21
- rule
22
- .create({
23
- filename: "/p/paper.tex",
24
- sourceCode: {
25
- text: src,
26
- getLocFromIndex: (i) => ({
27
- line: src.slice(0, i).split("\n").length,
28
- column: 0,
29
- }),
30
- },
31
- report: (d) => {
32
- if ("messageId" in d)
33
- out.push({
34
- line: d.loc.start.line,
35
- messageId: d.messageId,
36
- data: d.data,
37
- });
38
- },
39
- })
40
- .root?.();
41
- return out;
42
- }
43
33
 
44
- describe("tex/missing-input", () => {
45
- it("an \\input naming no file is reported at its line, naming the target", () => {
46
- expect(run(doc("Text.\n\\input{sections/gone}"))).toEqual([
47
- {
48
- line: 4,
49
- messageId: "missing",
50
- data: { target: "sections/gone", file: "paper.tex" },
51
- },
52
- ]);
34
+ it("the paper's directory comes first: its file wins over paperlint's of the same name", () => {
35
+ const p = readPaper("/p/paper.tex", "\\input{guards}", {
36
+ files: memoryFiles({
37
+ "/p/guards.tex": "mine",
38
+ [join(texInputsDir(), "guards.tex")]: "package",
39
+ }),
40
+ latex: latexReader,
41
+ });
42
+ expect(p.text).toBe("mine");
53
43
  });
44
+ });
54
45
 
55
- it("one missing in an included file is reported at the include that brought that file in", () => {
56
- expect(run(doc("\\input{a}"), { "/p/a.tex": "A\n\\input{b}\n" })).toEqual([
57
- { line: 3, messageId: "missingIn", data: { target: "b", file: "a.tex" } },
58
- ]);
46
+ describe("bodyFiles — the files of the body lint reads on their own", () => {
47
+ const doc = (preamble: string, body: string): string =>
48
+ `\\documentclass{article}\n${preamble}\n\\begin{document}\n${body}\n\\end{document}\n`;
49
+ const files = (extra: Record<string, string> = {}) =>
50
+ memoryFiles({
51
+ "/p/macros.tex": "\\newcommand{\\x}{y}",
52
+ "/p/sections/a.tex": "A\n\\input{sections/b}\n",
53
+ "/p/sections/b.tex": "B",
54
+ [join(texInputsDir(), "guards.tex")]: "G",
55
+ ...extra,
56
+ });
57
+
58
+ it("every file an include in the document body brings in, nested ones too, by absolute path", () => {
59
+ expect(
60
+ bodyFiles("/p/paper.tex", doc("", "\\input{sections/a}"), {
61
+ files: files(),
62
+ latex: latexReader,
63
+ }),
64
+ ).toEqual({
65
+ files: ["/p/sections/a.tex", "/p/sections/b.tex"],
66
+ missing: [],
67
+ });
59
68
  });
60
69
 
61
- it("silent when every include resolves, with `.tex` added or not", () => {
70
+ it("not a preamble include (macros), and not a file found only in paperlint's inputs", () => {
62
71
  expect(
63
- run(doc("\\input{a}\\include{b.tex}"), {
64
- "/p/a.tex": "A",
65
- "/p/b.tex": "B",
72
+ bodyFiles(
73
+ "/p/paper.tex",
74
+ doc("\\input{macros}", "\\input{guards}\nText."),
75
+ { files: files(), latex: latexReader },
76
+ ),
77
+ ).toEqual({ files: [], missing: [] });
78
+ });
79
+
80
+ it("an include that resolves nowhere is named, with the file that wrote it", () => {
81
+ expect(
82
+ bodyFiles("/p/paper.tex", doc("", "\\input{gone}"), {
83
+ files: files(),
84
+ latex: latexReader,
66
85
  }),
67
- ).toEqual([]);
86
+ ).toEqual({ files: [], missing: [{ file: "paper.tex", target: "gone" }] });
68
87
  });
69
- });
70
88
 
71
- describe("readPaper — the paper as the rules read it", () => {
72
- it("reads includes from the paper's own directory", () => {
73
- const p = readPaper("/p/paper.tex", "x\\input{s/a}y", {
74
- files: memoryFiles({ "/p/s/a.tex": "AAA" }),
75
- latex: latexReader,
76
- });
77
- expect(p.text).toBe("xAAAy");
78
- expect(p.main).toBe("paper.tex");
89
+ it("a main file with no document environment: every include is the body", () => {
90
+ expect(
91
+ bodyFiles("/p/paper.tex", "\\input{sections/b}", {
92
+ files: files(),
93
+ latex: latexReader,
94
+ }),
95
+ ).toEqual({ files: ["/p/sections/b.tex"], missing: [] });
79
96
  });
80
97
  });
81
98