paperlint 6.3.0 → 7.1.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 (185) hide show
  1. package/CONTRIBUTING.md +4 -2
  2. package/README.md +20 -13
  3. package/dist/adapters/latex/conditionals.d.ts +21 -0
  4. package/dist/adapters/latex/conditionals.d.ts.map +1 -0
  5. package/dist/adapters/latex/conditionals.js +75 -0
  6. package/dist/adapters/latex/conditionals.js.map +1 -0
  7. package/dist/adapters/latex/headings.d.ts +10 -0
  8. package/dist/adapters/latex/headings.d.ts.map +1 -0
  9. package/dist/adapters/latex/headings.js +129 -0
  10. package/dist/adapters/latex/headings.js.map +1 -0
  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/parse.d.ts +1 -1
  16. package/dist/adapters/latex/parse.d.ts.map +1 -1
  17. package/dist/adapters/latex/parse.js +3 -2
  18. package/dist/adapters/latex/parse.js.map +1 -1
  19. package/dist/build.d.ts +1 -1
  20. package/dist/build.d.ts.map +1 -1
  21. package/dist/build.js +1 -1
  22. package/dist/build.js.map +1 -1
  23. package/dist/cli.d.ts +1 -1
  24. package/dist/cli.d.ts.map +1 -1
  25. package/dist/cli.js +19 -5
  26. package/dist/cli.js.map +1 -1
  27. package/dist/domain/body-pages.d.ts +29 -0
  28. package/dist/domain/body-pages.d.ts.map +1 -1
  29. package/dist/domain/body-pages.js +25 -0
  30. package/dist/domain/body-pages.js.map +1 -1
  31. package/dist/domain/heading-case.d.ts +100 -0
  32. package/dist/domain/heading-case.d.ts.map +1 -0
  33. package/dist/domain/heading-case.js +365 -0
  34. package/dist/domain/heading-case.js.map +1 -0
  35. package/dist/domain/tex-document.d.ts +32 -0
  36. package/dist/domain/tex-document.d.ts.map +1 -1
  37. package/dist/domain/tex-document.js +11 -0
  38. package/dist/domain/tex-document.js.map +1 -1
  39. package/dist/eslint-rules/latex-language.d.ts.map +1 -1
  40. package/dist/eslint-rules/latex-language.js +16 -3
  41. package/dist/eslint-rules/latex-language.js.map +1 -1
  42. package/dist/facts-file.d.ts +3 -3
  43. package/dist/facts-file.d.ts.map +1 -1
  44. package/dist/facts-file.js +3 -3
  45. package/dist/facts-file.js.map +1 -1
  46. package/dist/folder-venue-rule.js +6 -6
  47. package/dist/folder-venue-rule.js.map +1 -1
  48. package/dist/heading-case.d.ts +48 -0
  49. package/dist/heading-case.d.ts.map +1 -0
  50. package/dist/heading-case.js +191 -0
  51. package/dist/heading-case.js.map +1 -0
  52. package/dist/paper-includes.d.ts +3 -0
  53. package/dist/paper-includes.d.ts.map +1 -1
  54. package/dist/paper-includes.js +17 -2
  55. package/dist/paper-includes.js.map +1 -1
  56. package/dist/ports/latex.d.ts +7 -1
  57. package/dist/ports/latex.d.ts.map +1 -1
  58. package/dist/preset-card-rule.d.ts +100 -0
  59. package/dist/preset-card-rule.d.ts.map +1 -0
  60. package/dist/preset-card-rule.js +221 -0
  61. package/dist/preset-card-rule.js.map +1 -0
  62. package/dist/presets.d.ts +29 -5
  63. package/dist/presets.d.ts.map +1 -1
  64. package/dist/presets.js +104 -68
  65. package/dist/presets.js.map +1 -1
  66. package/dist/submission.d.ts +8 -0
  67. package/dist/submission.d.ts.map +1 -1
  68. package/dist/submission.js +19 -7
  69. package/dist/submission.js.map +1 -1
  70. package/dist/tex-requirements.d.ts +42 -3
  71. package/dist/tex-requirements.d.ts.map +1 -1
  72. package/dist/tex-requirements.js +34 -5
  73. package/dist/tex-requirements.js.map +1 -1
  74. package/dist/tex-venue-rules.d.ts +6 -3
  75. package/dist/tex-venue-rules.d.ts.map +1 -1
  76. package/dist/tex-venue-rules.js +29 -10
  77. package/dist/tex-venue-rules.js.map +1 -1
  78. package/dist/venue-rules.d.ts +2 -1
  79. package/dist/venue-rules.d.ts.map +1 -1
  80. package/dist/venue-rules.js +55 -7
  81. package/dist/venue-rules.js.map +1 -1
  82. package/docs/configuration.md +13 -1
  83. package/docs/e2e.md +14 -12
  84. package/docs/optional-rules.md +4 -3
  85. package/docs/rules/format/page-limit.md +19 -2
  86. package/docs/rules/paper/folder-venue-leftover.md +1 -1
  87. package/docs/rules/talk/profile.md +1 -1
  88. package/docs/rules/tex/heading-case.md +424 -0
  89. package/docs/rules/tex/template.md +25 -5
  90. package/docs/rules/tex/venue-leftover.md +13 -0
  91. package/docs/rules.md +74 -57
  92. package/docs/submission.md +5 -1
  93. package/eslint-rules/latex-language.test.mjs +12 -0
  94. package/eslint-rules/latex-language.ts +17 -4
  95. package/eslint-rules/paper-typography.test.mjs +12 -0
  96. package/eslint.config.mjs +59 -1
  97. package/fixtures/build-e2e/acmart/paper.tex +1 -1
  98. package/fixtures/build-e2e/fallback/expect.json +3 -3
  99. package/fixtures/build-e2e/msr/PIPELINE-STATUS.md +3 -0
  100. package/fixtures/build-e2e/msr/expect.json +18 -0
  101. package/fixtures/build-e2e/msr/paper.tex +49 -0
  102. package/fixtures/build-e2e/msr/paperlint.json +1 -0
  103. package/fixtures/build-e2e/msr-appendix-after/PIPELINE-STATUS.md +3 -0
  104. package/fixtures/build-e2e/msr-appendix-after/expect.json +20 -0
  105. package/fixtures/build-e2e/msr-appendix-after/paper.tex +48 -0
  106. package/fixtures/build-e2e/msr-appendix-after/paperlint.json +1 -0
  107. package/lib/skill-checks.mjs +31 -0
  108. package/package.json +7 -1
  109. package/presets/acm-sigconf.jsonc +5 -0
  110. package/presets/acm-sigconf.md +43 -0
  111. package/presets/agenticdev.jsonc +27 -6
  112. package/presets/agenticdev.md +151 -0
  113. package/presets/aidc.jsonc +6 -0
  114. package/presets/aidc.md +75 -0
  115. package/presets/aisec.jsonc +5 -1
  116. package/presets/aisec.md +117 -0
  117. package/presets/ieee-conference.jsonc +5 -0
  118. package/presets/ieee-conference.md +38 -0
  119. package/presets/msr.jsonc +71 -0
  120. package/presets/msr.md +94 -0
  121. package/presets/realm.jsonc +7 -2
  122. package/presets/realm.md +166 -0
  123. package/presets/tex/agenticdev.tex +1 -1
  124. package/presets/tex-base.jsonc +6 -0
  125. package/presets/tex-base.md +30 -0
  126. package/presets/venue-profile.schema.json +65 -1
  127. package/skills/camera-ready/SKILL.md +8 -1
  128. package/skills/camera-ready/SKILL.md.spec.ts +7 -0
  129. package/skills/find-venue/SKILL.md +6 -5
  130. package/skills/find-venue/SKILL.md.spec.ts +5 -4
  131. package/skills/paper-pipeline/SKILL.md +5 -6
  132. package/skills/paper-pipeline/SKILL.md.spec.ts +4 -5
  133. package/skills/paper-pipeline/references/acl-venue-rules.md +1 -1
  134. package/skills/study-accepted-papers/SKILL.md +3 -3
  135. package/skills/study-accepted-papers/SKILL.md.spec.ts +2 -2
  136. package/skills/submission-portal/SKILL.md +2 -2
  137. package/skills/submission-portal/SKILL.md.spec.ts +1 -1
  138. package/skills/submit-paper/SKILL.md +34 -22
  139. package/skills/submit-paper/SKILL.md.spec.ts +33 -21
  140. package/skills/submit-paper/references/publishers/acm.md +1 -1
  141. package/src/adapters/latex/conditionals.ts +130 -0
  142. package/src/adapters/latex/headings.ts +163 -0
  143. package/src/adapters/latex/index.test.ts +65 -0
  144. package/src/adapters/latex/index.ts +3 -0
  145. package/src/adapters/latex/parse.ts +3 -2
  146. package/src/build.ts +2 -2
  147. package/src/cli.test.ts +50 -7
  148. package/src/cli.ts +28 -6
  149. package/src/domain/body-pages.test.ts +144 -0
  150. package/src/domain/body-pages.ts +61 -1
  151. package/src/domain/heading-case.test.ts +660 -0
  152. package/src/domain/heading-case.ts +538 -0
  153. package/src/domain/tex-document.ts +36 -0
  154. package/src/domain/venue-name.test.ts +13 -7
  155. package/src/facts-file.test.ts +4 -4
  156. package/src/facts-file.ts +6 -6
  157. package/src/folder-venue-rule.test.ts +10 -10
  158. package/src/folder-venue-rule.ts +6 -6
  159. package/src/heading-case.test.ts +500 -0
  160. package/src/heading-case.ts +275 -0
  161. package/src/init.test.ts +1 -1
  162. package/src/new-paper.test.ts +37 -6
  163. package/src/paper-includes.test.ts +26 -0
  164. package/src/paper-includes.ts +20 -2
  165. package/src/paper-settings-commands.test.ts +26 -11
  166. package/src/ports/latex.ts +7 -0
  167. package/src/preset-card-rule.test.ts +267 -0
  168. package/src/preset-card-rule.ts +384 -0
  169. package/src/presets.test.ts +259 -41
  170. package/src/presets.ts +161 -75
  171. package/src/rule-cases.test.ts +136 -50
  172. package/src/submission.test.ts +44 -4
  173. package/src/submission.ts +28 -7
  174. package/src/talk-rules.test.ts +6 -3
  175. package/src/tex-requirements.harness.ts +25 -11
  176. package/src/tex-requirements.test.ts +127 -1
  177. package/src/tex-requirements.ts +91 -12
  178. package/src/tex-venue-rules.test.ts +196 -21
  179. package/src/tex-venue-rules.ts +47 -16
  180. package/src/venue-rules.test.ts +180 -8
  181. package/src/venue-rules.ts +71 -10
  182. package/templates/paper/paper.tex +1 -1
  183. package/skills/submit-paper/references/venues/agenticdev.md +0 -171
  184. package/skills/submit-paper/references/venues/aisec.md +0 -105
  185. package/skills/submit-paper/references/venues/realm.md +0 -155
@@ -3,6 +3,8 @@ import {
3
3
  bodyEnd,
4
4
  isAppendixHeading,
5
5
  isReferencesHeading,
6
+ pageSplit,
7
+ type Anchors,
6
8
  } from "./body-pages.ts";
7
9
 
8
10
  describe("isReferencesHeading", () => {
@@ -298,3 +300,145 @@ describe("bodyEnd: where the appendix is not taken, or not found", () => {
298
300
  });
299
301
  });
300
302
  });
303
+
304
+ describe("pageSplit: the appendix counts as body, the reference pages are the ones holding only references", () => {
305
+ it("no appendix: the body up to the references, the rest references", () => {
306
+ expect(
307
+ pageSplit(["body", "end\nReferences\n[1] A.", "[2] B.", "[3] C."], {
308
+ bib: 2,
309
+ appendix: null,
310
+ }),
311
+ ).toEqual({
312
+ kind: "found",
313
+ page: 2,
314
+ bodyPages: 2,
315
+ refPages: 2,
316
+ appendixFrom: null,
317
+ });
318
+ });
319
+
320
+ it("🔴 an appendix before the references is body: it does not end the body", () => {
321
+ expect(
322
+ pageSplit(
323
+ [
324
+ "body",
325
+ "Appendix A.\nDetails",
326
+ "more details\nReferences\n[1] A.",
327
+ "[2] B.",
328
+ ],
329
+ { bib: 3, appendix: 2 },
330
+ ),
331
+ ).toEqual({
332
+ kind: "found",
333
+ page: 3,
334
+ bodyPages: 3,
335
+ refPages: 1,
336
+ appendixFrom: null,
337
+ });
338
+ });
339
+
340
+ it("🔴 an appendix after the references is body: the pages from it to the end", () => {
341
+ expect(
342
+ pageSplit(
343
+ [
344
+ "body",
345
+ "end\nReferences\n[1] A.",
346
+ "[2] B.",
347
+ "[3] C.\nAppendix A.\nx",
348
+ "y",
349
+ ],
350
+ { bib: 2, appendix: 4 },
351
+ ),
352
+ ).toEqual({
353
+ kind: "found",
354
+ page: 2,
355
+ bodyPages: 4,
356
+ refPages: 1,
357
+ appendixFrom: 4,
358
+ });
359
+ });
360
+ });
361
+
362
+ describe("pageSplit: the appendix and the references on one page", () => {
363
+ it("an appendix after the references on their own page: that page is counted once", () => {
364
+ expect(
365
+ pageSplit(["body", "end\nReferences\n[1] A.\nAppendix A.\nx"], {
366
+ bib: 2,
367
+ appendix: 2,
368
+ }),
369
+ ).toEqual({
370
+ kind: "found",
371
+ page: 2,
372
+ bodyPages: 2,
373
+ refPages: 0,
374
+ appendixFrom: 2,
375
+ });
376
+ });
377
+
378
+ it("an appendix above the references on their page is body before them, like any text", () => {
379
+ expect(
380
+ pageSplit(["body", "end\nAppendix A.\nx\nReferences\n[1] A.", "[2] B."], {
381
+ bib: 2,
382
+ appendix: 2,
383
+ }),
384
+ ).toEqual({
385
+ kind: "found",
386
+ page: 2,
387
+ bodyPages: 2,
388
+ refPages: 1,
389
+ appendixFrom: null,
390
+ });
391
+ });
392
+
393
+ it("the references at the top of their page: that page holds only references", () => {
394
+ expect(
395
+ pageSplit(["body", "References\n[1] A.", "Appendix A.\nx"], {
396
+ bib: 2,
397
+ appendix: 3,
398
+ }),
399
+ ).toEqual({
400
+ kind: "found",
401
+ page: 2,
402
+ bodyPages: 2,
403
+ refPages: 1,
404
+ appendixFrom: 3,
405
+ });
406
+ });
407
+ });
408
+
409
+ describe("pageSplit: what is said rather than counted, and what is not looked for", () => {
410
+ it.each<[string, readonly string[], Anchors, string]>([
411
+ [
412
+ "no references heading",
413
+ ["body", "Appendix\nx"],
414
+ { bib: null, appendix: 2 },
415
+ "missing",
416
+ ],
417
+ [
418
+ "an appendix anchor with no heading",
419
+ ["body", "References\n[1] A.", "x"],
420
+ { bib: 2, appendix: 3 },
421
+ "disagree",
422
+ ],
423
+ [
424
+ "a references anchor with no heading",
425
+ ["body", "x"],
426
+ { bib: 2, appendix: null },
427
+ "disagree",
428
+ ],
429
+ ])("%s: said, not counted", (_, pages, anchors, kind) => {
430
+ expect(pageSplit(pages, anchors).kind).toBe(kind);
431
+ });
432
+
433
+ it("without anchors the references heading is found in the text, and no appendix is looked for", () => {
434
+ expect(
435
+ pageSplit(["body", "end\nReferences\n[1] A.", "Appendix A.\nx"]),
436
+ ).toEqual({
437
+ kind: "found",
438
+ page: 2,
439
+ bodyPages: 2,
440
+ refPages: 1,
441
+ appendixFrom: null,
442
+ });
443
+ });
444
+ });
@@ -186,8 +186,11 @@ function appendixAt(
186
186
  };
187
187
  }
188
188
 
189
+ /** A body end that was found. */
190
+ type Found = Extract<BodyEnd, { readonly kind: "found" }>;
191
+
189
192
  /** The body's end at a heading: the heading's page counts when body text stands above it. */
190
- function endAt(pages: readonly PageReading[], at: At): BodyEnd {
193
+ function endAt(pages: readonly PageReading[], at: At): Found {
191
194
  const running = runningLines(pages);
192
195
  const body = at.above.some((l) => /\p{L}/u.test(l) && !running.has(shape(l)));
193
196
  return {
@@ -221,3 +224,60 @@ export function bodyEnd(
221
224
  .toSorted((a, b) => a.page - b.page || a.line - b.line)[0];
222
225
  return first === undefined ? { kind: "missing" } : endAt(pages, first);
223
226
  }
227
+
228
+ /**
229
+ * The pages of a paper split as a venue counts them when its body limit includes the appendices
230
+ * and only the pages holding nothing but references are reference pages (MSR: «10 pages for the
231
+ * main text, inclusive of all figures, tables, appendices, etc. Two more pages containing only
232
+ * references are permitted.»):
233
+ *
234
+ * body the pages up to the references — their page too when body text stands above the
235
+ * heading — and, when the appendix starts after the references, every page from the
236
+ * appendix's to the last, which holds appendix text
237
+ * references the pages between, which hold only references
238
+ *
239
+ * An appendix before the references is body like any other text, and ends nothing. The appendix is
240
+ * found by hyperref's anchor only (`appendixAt`): without hyperref, an appendix after the references
241
+ * is not told from them and is counted with them. Pure.
242
+ */
243
+ export type PageSplit =
244
+ | {
245
+ readonly kind: "found";
246
+ /** 1-based page holding the references heading. */
247
+ readonly page: number;
248
+ readonly bodyPages: number;
249
+ readonly refPages: number;
250
+ /** 1-based page the appendix starts on, when it follows the references; else null. */
251
+ readonly appendixFrom: number | null;
252
+ }
253
+ | { readonly kind: "missing" }
254
+ | Extract<BodyEnd, { readonly kind: "disagree" }>;
255
+
256
+ /** Whether heading `a` stands after heading `b` in the document. */
257
+ const after = (a: At, b: At): boolean =>
258
+ a.page > b.page || (a.page === b.page && a.line > b.line);
259
+
260
+ export function pageSplit(
261
+ texts: readonly string[],
262
+ anchors: Anchors = { bib: null, appendix: null },
263
+ ): PageSplit {
264
+ const pages = texts.map(readPage);
265
+ const refs = referencesAt(pages, anchors.bib);
266
+ const appendix = appendixAt(pages, anchors.appendix);
267
+ if (refs.kind === "disagree") return refs;
268
+ if (appendix.kind === "disagree") return appendix;
269
+ if (refs.kind === "missing") return { kind: "missing" };
270
+ const upTo = endAt(pages, refs).bodyPages;
271
+ const tail =
272
+ appendix.kind === "at" && after(appendix, refs) ? appendix.page : null;
273
+ // The pages from the appendix to the end, less the ones already counted up to the references.
274
+ const appendixPages = tail === null ? 0 : pages.length - Math.max(tail, upTo);
275
+ const bodyPages = upTo + appendixPages;
276
+ return {
277
+ kind: "found",
278
+ page: refs.page + 1,
279
+ bodyPages,
280
+ refPages: pages.length - bodyPages,
281
+ appendixFrom: tail === null ? null : tail + 1,
282
+ };
283
+ }