paperlint 5.0.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 (147) hide show
  1. package/CLAUDE.md +13 -0
  2. package/CONTRIBUTING.md +3 -0
  3. package/README.md +8 -8
  4. package/bin/paperlint.mjs +5 -1
  5. package/dist/adapters/latex/index.d.ts +1 -0
  6. package/dist/adapters/latex/index.d.ts.map +1 -1
  7. package/dist/adapters/latex/index.js +3 -0
  8. package/dist/adapters/latex/index.js.map +1 -1
  9. package/dist/adapters/latex/layout.d.ts +27 -0
  10. package/dist/adapters/latex/layout.d.ts.map +1 -0
  11. package/dist/adapters/latex/layout.js +62 -0
  12. package/dist/adapters/latex/layout.js.map +1 -0
  13. package/dist/adapters/latex/nodes.d.ts.map +1 -1
  14. package/dist/adapters/latex/nodes.js +11 -1
  15. package/dist/adapters/latex/nodes.js.map +1 -1
  16. package/dist/adapters/latex/parse.d.ts.map +1 -1
  17. package/dist/adapters/latex/parse.js +5 -1
  18. package/dist/adapters/latex/parse.js.map +1 -1
  19. package/dist/cli.d.ts.map +1 -1
  20. package/dist/cli.js +25 -5
  21. package/dist/cli.js.map +1 -1
  22. package/dist/domain/anonymity.d.ts +68 -0
  23. package/dist/domain/anonymity.d.ts.map +1 -0
  24. package/dist/domain/anonymity.js +94 -0
  25. package/dist/domain/anonymity.js.map +1 -0
  26. package/dist/domain/body-pages.d.ts +69 -0
  27. package/dist/domain/body-pages.d.ts.map +1 -0
  28. package/dist/domain/body-pages.js +141 -0
  29. package/dist/domain/body-pages.js.map +1 -0
  30. package/dist/domain/tex-document.d.ts +9 -0
  31. package/dist/domain/tex-document.d.ts.map +1 -1
  32. package/dist/eslint-rules/pdf-last-page-balance.d.ts +1 -1
  33. package/dist/eslint-rules/pdf-last-page-balance.js +2 -2
  34. package/dist/facts-file.d.ts +28 -2
  35. package/dist/facts-file.d.ts.map +1 -1
  36. package/dist/facts-file.js +54 -9
  37. package/dist/facts-file.js.map +1 -1
  38. package/dist/lib/paper-config.d.ts.map +1 -1
  39. package/dist/lib/paper-config.js +2 -1
  40. package/dist/lib/paper-config.js.map +1 -1
  41. package/dist/paper-settings.d.ts +11 -0
  42. package/dist/paper-settings.d.ts.map +1 -1
  43. package/dist/paper-settings.js +38 -4
  44. package/dist/paper-settings.js.map +1 -1
  45. package/dist/pdf-content.d.ts +40 -0
  46. package/dist/pdf-content.d.ts.map +1 -0
  47. package/dist/pdf-content.js +97 -0
  48. package/dist/pdf-content.js.map +1 -0
  49. package/dist/pdf-facts.d.ts +30 -0
  50. package/dist/pdf-facts.d.ts.map +1 -1
  51. package/dist/pdf-facts.js +37 -1
  52. package/dist/pdf-facts.js.map +1 -1
  53. package/dist/ports/latex.d.ts +3 -1
  54. package/dist/ports/latex.d.ts.map +1 -1
  55. package/dist/presets.d.ts +2 -0
  56. package/dist/presets.d.ts.map +1 -1
  57. package/dist/presets.js +4 -0
  58. package/dist/presets.js.map +1 -1
  59. package/dist/tex-requirements.d.ts +4 -0
  60. package/dist/tex-requirements.d.ts.map +1 -1
  61. package/dist/tex-requirements.js +3 -0
  62. package/dist/tex-requirements.js.map +1 -1
  63. package/dist/tex-venue-rules.d.ts +7 -1
  64. package/dist/tex-venue-rules.d.ts.map +1 -1
  65. package/dist/tex-venue-rules.js +40 -0
  66. package/dist/tex-venue-rules.js.map +1 -1
  67. package/dist/types.d.ts +2 -0
  68. package/dist/types.d.ts.map +1 -1
  69. package/dist/venue-rules.d.ts +38 -8
  70. package/dist/venue-rules.d.ts.map +1 -1
  71. package/dist/venue-rules.js +179 -41
  72. package/dist/venue-rules.js.map +1 -1
  73. package/docs/configuration.md +18 -16
  74. package/docs/e2e.md +32 -11
  75. package/docs/rules/anonymity/identity.md +111 -0
  76. package/docs/rules/format/layout-override.md +61 -0
  77. package/docs/rules/format/page-limit.md +108 -0
  78. package/docs/rules/pdf/body-size.md +87 -0
  79. package/docs/rules.md +44 -39
  80. package/eslint-rules/pdf-last-page-balance.harness.mjs +6 -1
  81. package/eslint-rules/pdf-last-page-balance.ts +2 -2
  82. package/fixtures/build-e2e/aidc/expect.json +1 -1
  83. package/fixtures/build-e2e/aidc/paperlint.json +1 -1
  84. package/fixtures/build-e2e/anon-ack/PIPELINE-STATUS.md +3 -0
  85. package/fixtures/build-e2e/anon-ack/expect.json +19 -0
  86. package/fixtures/build-e2e/anon-ack/paper.tex +31 -0
  87. package/fixtures/build-e2e/anon-ack/paperlint.json +1 -0
  88. package/fixtures/build-e2e/anon-author/PIPELINE-STATUS.md +3 -0
  89. package/fixtures/build-e2e/anon-author/expect.json +19 -0
  90. package/fixtures/build-e2e/anon-author/paper.tex +29 -0
  91. package/fixtures/build-e2e/anon-author/paperlint.json +1 -0
  92. package/fixtures/build-e2e/anon-bib/PIPELINE-STATUS.md +3 -0
  93. package/fixtures/build-e2e/anon-bib/expect.json +20 -0
  94. package/fixtures/build-e2e/anon-bib/paper.tex +35 -0
  95. package/fixtures/build-e2e/anon-bib/paperlint.json +1 -0
  96. package/fixtures/build-e2e/anon-href/PIPELINE-STATUS.md +3 -0
  97. package/fixtures/build-e2e/anon-href/expect.json +19 -0
  98. package/fixtures/build-e2e/anon-href/paper.tex +30 -0
  99. package/fixtures/build-e2e/anon-href/paperlint.json +1 -0
  100. package/fixtures/build-e2e/anon-macro/PIPELINE-STATUS.md +3 -0
  101. package/fixtures/build-e2e/anon-macro/expect.json +19 -0
  102. package/fixtures/build-e2e/anon-macro/paper.tex +30 -0
  103. package/fixtures/build-e2e/anon-macro/paperlint.json +1 -0
  104. package/fixtures/build-e2e/anon-pdfauthor/PIPELINE-STATUS.md +3 -0
  105. package/fixtures/build-e2e/anon-pdfauthor/expect.json +19 -0
  106. package/fixtures/build-e2e/anon-pdfauthor/paper.tex +31 -0
  107. package/fixtures/build-e2e/anon-pdfauthor/paperlint.json +1 -0
  108. package/fixtures/build-e2e/unbalanced/expect.json +2 -2
  109. package/lib/paper-config.ts +3 -1
  110. package/package.json +1 -1
  111. package/presets/acm-sigconf.jsonc +3 -1
  112. package/presets/aidc.jsonc +16 -17
  113. package/presets/ieee-conference.jsonc +8 -0
  114. package/presets/venue-profile.schema.json +9 -0
  115. package/skills/render-paper/extract-pdf-facts.harness.ts +3 -3
  116. package/src/adapters/latex/index.ts +3 -0
  117. package/src/adapters/latex/layout.ts +106 -0
  118. package/src/adapters/latex/nodes.ts +11 -3
  119. package/src/adapters/latex/parse.ts +5 -1
  120. package/src/build.harness.mjs +2 -2
  121. package/src/build.test.ts +5 -0
  122. package/src/cli.test.ts +13 -0
  123. package/src/cli.ts +36 -4
  124. package/src/domain/anonymity.test.ts +163 -0
  125. package/src/domain/anonymity.ts +129 -0
  126. package/src/domain/body-pages.test.ts +300 -0
  127. package/src/domain/body-pages.ts +223 -0
  128. package/src/domain/tex-document.ts +10 -0
  129. package/src/facts-file.test.ts +100 -4
  130. package/src/facts-file.ts +91 -9
  131. package/src/new-paper.test.ts +7 -1
  132. package/src/paper-settings.test.ts +63 -6
  133. package/src/paper-settings.ts +53 -5
  134. package/src/pdf-content.test.ts +71 -0
  135. package/src/pdf-content.ts +125 -0
  136. package/src/pdf-facts.test.ts +110 -2
  137. package/src/pdf-facts.ts +106 -8
  138. package/src/ports/latex.ts +3 -0
  139. package/src/presets.ts +8 -8
  140. package/src/rule-cases.test.ts +29 -3
  141. package/src/rule-docs.test.ts +0 -2
  142. package/src/tex-requirements.ts +9 -0
  143. package/src/tex-venue-rules.test.ts +110 -5
  144. package/src/tex-venue-rules.ts +57 -2
  145. package/src/types.ts +2 -0
  146. package/src/venue-rules.test.ts +351 -39
  147. package/src/venue-rules.ts +247 -47
@@ -1,6 +1,6 @@
1
1
  /**
2
- * The venue rules (`pdf/fresh` · `pdf/profile` · `pdf/fonts` · `pdf/geometry` · `pdf/limits` ·
3
- * `pdf/body-size` · `pdf/measured`) over a paper held in memory: `paper.tex`, `paperlint.json`,
2
+ * The venue rules (`pdf/fresh` · `pdf/profile` · `pdf/fonts` · `pdf/geometry` · `pdf/body-size` ·
3
+ * `pdf/measured` · `format/page-limit` · `anonymity/identity`) over a paper held in memory: `paper.tex`, `paperlint.json`,
4
4
  * `_build/paper.facts.json` and the PDF the facts describe. The venue profiles are the SHIPPED
5
5
  * ones, read from the package's venues directory, so a profile edit that breaks a check shows here.
6
6
  *
@@ -13,7 +13,17 @@ import { describe, expect, it } from "vitest";
13
13
  import { memoryFiles } from "./adapters/memory/index.ts";
14
14
  import { sha256Hex } from "./domain/sha256.ts";
15
15
  import { presetsDir } from "./package-dirs.ts";
16
- import { VENUE_RULE_LEVELS, venueRules } from "./venue-rules.ts";
16
+ import {
17
+ ANONYMITY_RULE_LEVELS,
18
+ anonymityRules,
19
+ judgeBodyEnd,
20
+ PAGE_LIMIT_RULE_LEVELS,
21
+ pageLimitRules,
22
+ VENUE_RULE_LEVELS,
23
+ venueRules,
24
+ type Resolved,
25
+ } from "./venue-rules.ts";
26
+ import { NO_FORMAT } from "./tex-requirements.ts";
17
27
  import { buildConfig, OPTIONAL_RULES, SHIPPED_RULES } from "./cli.ts";
18
28
  import { present } from "../test/support.ts";
19
29
 
@@ -44,7 +54,12 @@ type Patch = (f: Facts) => void;
44
54
  /** The facts of a short agenticdev paper that meets every number in its profile. */
45
55
  function goodFacts(): Facts {
46
56
  return {
47
- schema: 2,
57
+ schema: 3,
58
+ pages_text: [],
59
+ metadata: {},
60
+ links: [],
61
+ bib_anchor_page: null,
62
+ appendix_anchor_page: null,
48
63
  pdf: "paper.pdf",
49
64
  pdf_sha256: sha256Hex(PDF_BYTES),
50
65
  venue: "agenticdev",
@@ -122,18 +137,29 @@ function lint(
122
137
  cwd?: string,
123
138
  ): Finding[] {
124
139
  const files = memoryFiles(paperFiles(p));
125
- const rules = venueRules({ files, venuesDir: VENUES });
140
+ const deps = { files, venuesDir: VENUES };
141
+ const rules = [
142
+ ...Object.entries(venueRules(deps)).map(
143
+ ([n, r]) => [`pdf/${n}`, r] as const,
144
+ ),
145
+ ...Object.entries(pageLimitRules(deps)).map(
146
+ ([n, r]) => [`format/${n}`, r] as const,
147
+ ),
148
+ ...Object.entries(anonymityRules(deps)).map(
149
+ ([n, r]) => [`anonymity/${n}`, r] as const,
150
+ ),
151
+ ];
126
152
  const out: Finding[] = [];
127
- for (const [name, rule] of Object.entries(rules)) {
153
+ for (const [id, rule] of rules) {
128
154
  const visitor = rule.create({
129
155
  filename,
130
156
  ...(cwd === undefined ? {} : { cwd }),
131
157
  sourceCode: { text: TEX },
132
158
  options:
133
- name === "geometry" && Object.keys(options).length ? [options] : [],
159
+ id === "pdf/geometry" && Object.keys(options).length ? [options] : [],
134
160
  report: (d) =>
135
161
  out.push({
136
- rule: `pdf/${name}`,
162
+ rule: id,
137
163
  messageId: d.messageId,
138
164
  message: Object.entries(d.data ?? {}).reduce(
139
165
  (m, [k, v]) => m.replaceAll(`{{${k}}}`, String(v)),
@@ -456,39 +482,21 @@ describe("pdf/geometry", () => {
456
482
  });
457
483
  });
458
484
 
459
- describe("pdf/limits", () => {
485
+ describe("format/page-limit", () => {
460
486
  it.each<[string, (f: Facts) => void, string[], RegExp | null]>([
461
487
  [
462
488
  "one body page over the short-paper limit",
463
489
  (f: Facts) => (f.body_pages = 6),
464
- ["pdf/limits:pages"],
490
+ ["format/page-limit:pages"],
465
491
  /body pages: 6, over the limit 5 for agenticdev\/short/,
466
492
  ],
467
493
  [
468
494
  "one reference page over",
469
495
  (f: Facts) => (f.ref_pages = 3),
470
- ["pdf/limits:pages"],
496
+ ["format/page-limit:pages"],
471
497
  /reference pages: 3, over the limit 2/,
472
498
  ],
473
499
  ["exactly at the limit", (f: Facts) => (f.body_pages = 5), [], null],
474
- [
475
- "a reference font below the range",
476
- (f: Facts) => (f.ref_pt = 6),
477
- ["pdf/limits:refPt"],
478
- /6 pt/,
479
- ],
480
- [
481
- "6.8 pt is inside the range once the measuring drift (body_pt_tol) is allowed",
482
- (f: Facts) => (f.ref_pt = 6.8),
483
- [],
484
- null,
485
- ],
486
- [
487
- "no reference font measured (no bibliography) is not a finding",
488
- (f: Facts) => (f.ref_pt = null),
489
- [],
490
- null,
491
- ],
492
500
  ])("%s", (_, patch, want, text) => {
493
501
  const fs = lint({ venue: DECL, facts: withFacts(patch) });
494
502
  expect(ids(fs)).toEqual(want);
@@ -497,7 +505,9 @@ describe("pdf/limits", () => {
497
505
 
498
506
  it("a longer kind of the same venue passes the same page count", () => {
499
507
  const facts = withFacts((f) => (f.body_pages = 9));
500
- expect(ids(lint({ venue: DECL, facts }))).toEqual(["pdf/limits:pages"]);
508
+ expect(ids(lint({ venue: DECL, facts }))).toEqual([
509
+ "format/page-limit:pages",
510
+ ]);
501
511
  expect(
502
512
  lint({ venue: { extends: "paperlint:agenticdev", kind: "full" }, facts }),
503
513
  ).toEqual([]);
@@ -517,6 +527,21 @@ describe("pdf/body-size", () => {
517
527
  (f: Facts) => (f.body_pt = null),
518
528
  ["pdf/body-size:bodyMissing"],
519
529
  ],
530
+ [
531
+ "a reference font below the range",
532
+ (f: Facts) => (f.ref_pt = 6),
533
+ ["pdf/body-size:refPt"],
534
+ ],
535
+ [
536
+ "6.8 pt is inside the reference range once the measuring drift (body_pt_tol) is allowed",
537
+ (f: Facts) => (f.ref_pt = 6.8),
538
+ [],
539
+ ],
540
+ [
541
+ "no reference font measured (no bibliography) is not a finding",
542
+ (f: Facts) => (f.ref_pt = null),
543
+ [],
544
+ ],
520
545
  ])("%s", (_, patch, want) => {
521
546
  expect(ids(lint({ venue: DECL, facts: withFacts(patch) }))).toEqual(want);
522
547
  });
@@ -537,22 +562,33 @@ describe("paperlint's own config turns the venue rules on for every paper.tex",
537
562
  "the paper.tex block's rules",
538
563
  );
539
564
 
540
- it.each(Object.entries(VENUE_RULE_LEVELS))("%s is on at %s", (id, level) => {
565
+ it.each(
566
+ Object.entries({
567
+ ...VENUE_RULE_LEVELS,
568
+ ...PAGE_LIMIT_RULE_LEVELS,
569
+ ...ANONYMITY_RULE_LEVELS,
570
+ }),
571
+ )("%s is on at %s", (id, level) => {
541
572
  expect(texRules[id]).toBe(level);
542
573
  expect(SHIPPED_RULES.has(id)).toBe(true);
543
574
  expect(OPTIONAL_RULES.has(id)).toBe(false);
544
575
  });
545
576
 
546
- it("the six rules of the issue are errors except body-size, and measured is a warning", () => {
577
+ it("the venue rules are errors except measured, which is a warning", () => {
547
578
  expect(VENUE_RULE_LEVELS).toEqual({
548
579
  "pdf/fresh": "error",
549
580
  "pdf/profile": "error",
550
581
  "pdf/fonts": "error",
551
582
  "pdf/geometry": "error",
552
- "pdf/limits": "error",
553
- "pdf/body-size": "warn",
583
+ "pdf/body-size": "error",
554
584
  "pdf/measured": "warn",
555
585
  });
586
+ expect(PAGE_LIMIT_RULE_LEVELS).toEqual({ "format/page-limit": "error" });
587
+ expect(ANONYMITY_RULE_LEVELS).toEqual({ "anonymity/identity": "error" });
588
+ });
589
+
590
+ it("the rule the page limit was checked by before is not shipped", () => {
591
+ expect(SHIPPED_RULES.has("pdf/limits")).toBe(false);
556
592
  });
557
593
 
558
594
  it("pdf/last-page-balance stays optional", () => {
@@ -573,6 +609,10 @@ const type1 = (names: readonly string[]): Font[] =>
573
609
  program: "Type1",
574
610
  }));
575
611
 
612
+ /** `n` pages of body text, one per page, as `pages_text` holds them. */
613
+ const bodyPages = (n: number): string[] =>
614
+ Array.from({ length: n }, (_, i) => `Body page ${String(i + 1)}\n`);
615
+
576
616
  const ieeeFacts = (patch: Patch = () => {}): Facts =>
577
617
  withFacts((f) => {
578
618
  f.page_w_in = 8.5;
@@ -582,6 +622,11 @@ const ieeeFacts = (patch: Patch = () => {}): Facts =>
582
622
  f.ref_pt = 8.3;
583
623
  f.body_pages = 12;
584
624
  f.ref_pages = 1;
625
+ // Twelve pages of body, the references heading on the thirteenth.
626
+ f.pages_text = [
627
+ ...bodyPages(12),
628
+ "R EFERENCES\n[1] D. E. Knuth, Literate Programming.\nLLM Usage Statement\n",
629
+ ];
585
630
  f.fonts = type1([
586
631
  "NimbusRomNo9L-Medi",
587
632
  "NimbusRomNo9L-Regu",
@@ -593,16 +638,34 @@ const ieeeFacts = (patch: Patch = () => {}): Facts =>
593
638
 
594
639
  describe("the IEEE conference family and AIDC", () => {
595
640
  const FAMILY = { extends: "paperlint:ieee-conference" };
596
- const AIDC = { extends: "paperlint:aidc", kind: "regular" };
641
+ const IDENTITY = ["Ada Example", "adaexample", "ada@example.org"];
642
+ const AIDC = {
643
+ extends: "paperlint:aidc",
644
+ kind: "regular",
645
+ identity: IDENTITY,
646
+ };
597
647
 
598
648
  it.each([
599
649
  ["the family", FAMILY],
600
650
  ["aidc/regular", AIDC],
601
- ["aidc/short", { extends: "paperlint:aidc", kind: "short" }],
602
651
  ])("the measured IEEEtran build passes %s", (_, venue) => {
603
652
  expect(lint({ venue, facts: ieeeFacts() })).toEqual([]);
604
653
  });
605
654
 
655
+ it("aidc/short: six body pages pass, seven fail", () => {
656
+ const short = { ...AIDC, kind: "short" };
657
+ const pages = (n: number) =>
658
+ ieeeFacts((f) => {
659
+ f.pages_text = [...bodyPages(n), "References\n"];
660
+ });
661
+ expect(lint({ venue: short, facts: pages(6) })).toEqual([]);
662
+ const fs = lint({ venue: short, facts: pages(7) });
663
+ expect(ids(fs)).toEqual(["format/page-limit:pages"]);
664
+ expect(fs[0]?.message).toBe(
665
+ "body pages (up to the references on page 8): 7, over the limit 6 for aidc/short — a desk reject; cut the text",
666
+ );
667
+ });
668
+
606
669
  it.each([
607
670
  ["the family", FAMILY],
608
671
  ["aidc", AIDC],
@@ -627,14 +690,52 @@ describe("the IEEE conference family and AIDC", () => {
627
690
  expect(ids(fs)).toEqual(["pdf/fonts:noFamily"]);
628
691
  expect(fs[0]?.message).toMatch(/NimbusRomNo9L/);
629
692
  });
693
+ });
630
694
 
695
+ describe("pdf/body-size on the IEEE family: 10 pt body, 8 pt references", () => {
631
696
  it("a body size off the family's 10 pt fails pdf/body-size", () => {
632
697
  expect(
633
- ids(lint({ venue: AIDC, facts: ieeeFacts((f) => (f.body_pt = 9.3)) })),
698
+ ids(
699
+ lint({
700
+ venue: {
701
+ extends: "paperlint:aidc",
702
+ kind: "regular",
703
+ identity: ["Ada Example"],
704
+ },
705
+ facts: ieeeFacts((f) => (f.body_pt = 9.3)),
706
+ }),
707
+ ),
634
708
  ).toEqual(["pdf/body-size:body"]);
635
709
  });
636
710
 
637
- it("🔴 AIDC's page limit is not gated: banal's body count of 13 — a correct paper with its LLM Usage Statement before the references — is not a finding", () => {
711
+ it.each([
712
+ ["\\scriptsize, 7 pt", 7.3],
713
+ ["\\small, 9 pt", 9.3],
714
+ ])(
715
+ "a bibliography in %s instead of the class's 8 pt fails pdf/body-size",
716
+ (_, got) => {
717
+ const fs = lint({
718
+ venue: { extends: "paperlint:ieee-conference" },
719
+ facts: ieeeFacts((f) => (f.ref_pt = got)),
720
+ });
721
+ expect(fs.map((f) => f.message)).toEqual([
722
+ `the reference font size is ${String(got)} pt, outside 8 pt for ieee-conference (allowing ±0.5 pt for the measuring drift) — fix the bibliography's font size`,
723
+ ]);
724
+ },
725
+ );
726
+ });
727
+
728
+ /** An AIDC regular paper (12 body pages), for the page-limit tests. */
729
+ const AIDC_REGULAR = {
730
+ extends: "paperlint:aidc",
731
+ kind: "regular",
732
+ identity: ["Ada Example"],
733
+ };
734
+
735
+ describe("format/page-limit on AIDC: the body is counted up to the references", () => {
736
+ const AIDC = AIDC_REGULAR;
737
+
738
+ it("🔴 AIDC's body is counted before the references, not by banal: banal's 13 — the statement and an appendix after the references counted as body — is not a finding", () => {
638
739
  expect(
639
740
  lint({
640
741
  venue: AIDC,
@@ -643,6 +744,197 @@ describe("the IEEE conference family and AIDC", () => {
643
744
  ).toEqual([]);
644
745
  });
645
746
 
747
+ it("…while a thirteenth page of body before the references is", () => {
748
+ const fs = lint({
749
+ venue: AIDC,
750
+ facts: ieeeFacts((f) => {
751
+ f.pages_text = [...bodyPages(13), "7 References\n"];
752
+ }),
753
+ });
754
+ expect(ids(fs)).toEqual(["format/page-limit:pages"]);
755
+ });
756
+
757
+ it("🔴 a body that runs onto the thirteenth page, the references below it there: 13 pages, a finding", () => {
758
+ const fs = lint({
759
+ venue: AIDC,
760
+ facts: ieeeFacts((f) => {
761
+ f.pages_text = [
762
+ ...bodyPages(12),
763
+ "the last lines of the conclusion\nR EFERENCES\n[1] D. E. Knuth.\n",
764
+ ];
765
+ }),
766
+ });
767
+ expect(ids(fs)).toEqual(["format/page-limit:pages"]);
768
+ expect(fs[0]?.message).toMatch(
769
+ /^body pages \(up to the references on page 13\): 13, over the limit 12/,
770
+ );
771
+ });
772
+
773
+ it("no references heading on any page: said, not passed", () => {
774
+ const fs = lint({
775
+ venue: AIDC,
776
+ facts: ieeeFacts((f) => (f.pages_text = ["Body\n", "Body\n"])),
777
+ });
778
+ expect(ids(fs)).toEqual(["format/page-limit:noReferences"]);
779
+ expect(fs[0]?.message).toMatch(/NOT counted/);
780
+ });
781
+
782
+ it("the references count needs no banal: facts without geometry are still judged", () => {
783
+ const fs = lint({
784
+ venue: AIDC,
785
+ facts: ieeeFacts((f) => {
786
+ f.geometry_source = null;
787
+ f.pages_text = [...bodyPages(13), "References\n"];
788
+ }),
789
+ });
790
+ expect(ids(fs)).toEqual([
791
+ "format/page-limit:pages",
792
+ "pdf/measured:noGeometry",
793
+ ]);
794
+ });
795
+ });
796
+
797
+ describe("format/page-limit on AIDC: an appendix ends the body too", () => {
798
+ const AIDC = AIDC_REGULAR;
799
+
800
+ it("🔴 an appendix before the references is not body: the body ends where hyperref anchors the appendix", () => {
801
+ const facts = ieeeFacts((f) => {
802
+ f.pages_text = [
803
+ ...bodyPages(12),
804
+ "Appendix A.\nDetailed results\n",
805
+ "References\n[1] A.\n",
806
+ ];
807
+ f.bib_anchor_page = 14;
808
+ f.appendix_anchor_page = 13;
809
+ });
810
+ expect(ids(lint({ venue: AIDC, facts }))).toEqual([]);
811
+ // Without hyperref's anchor for it, the appendix is body — the count this rule had before.
812
+ facts.appendix_anchor_page = null;
813
+ expect(lint({ venue: AIDC, facts })[0]?.message).toMatch(
814
+ /^body pages \(up to the references on page 14\): 13, over the limit 12/,
815
+ );
816
+ });
817
+ });
818
+
819
+ describe("format/page-limit on AIDC: the two signals of where the references start disagree", () => {
820
+ const AIDC = AIDC_REGULAR;
821
+
822
+ it("hyperref's first entry on a page with no «References» line: unclear, said, not passed", () => {
823
+ const fs = lint({
824
+ venue: AIDC,
825
+ facts: ieeeFacts((f) => {
826
+ f.pages_text = [...bodyPages(12), "References\n[1] A.\n", "[2] B.\n"];
827
+ f.bib_anchor_page = 14;
828
+ }),
829
+ });
830
+ expect(ids(fs)).toEqual(["format/page-limit:unclear"]);
831
+ expect(fs[0]?.message).toBe(
832
+ "could not tell where the body ends, so it was NOT counted against aidc's limit: the first bibliography entry is on page 14 (hyperref's destination), and no line on page 14 reads «References». Check the headings of the bibliography and the appendix, and that nothing before them reads «References» above a [1]",
833
+ );
834
+ });
835
+
836
+ it("hyperref anchors the appendix on a page with no line reading «Appendix»: unclear", () => {
837
+ const fs = lint({
838
+ venue: AIDC,
839
+ facts: ieeeFacts((f) => {
840
+ f.appendix_anchor_page = 5;
841
+ }),
842
+ });
843
+ expect(fs[0]?.message).toMatch(
844
+ /: hyperref anchors the appendix on page 5, and no line on page 5 reads «Appendix»\./,
845
+ );
846
+ });
847
+
848
+ it("a confirmed heading on another page than hyperref's first entry: unclear, naming both", () => {
849
+ const fs = lint({
850
+ venue: AIDC,
851
+ facts: ieeeFacts((f) => {
852
+ f.pages_text = [
853
+ "References\n[1] quoted in the body\n",
854
+ ...bodyPages(11),
855
+ "References\n[1] A.\n",
856
+ ];
857
+ f.bib_anchor_page = 13;
858
+ }),
859
+ });
860
+ expect(fs[0]?.message).toMatch(
861
+ /and the «References» heading followed by \[1\] is on page 1\./,
862
+ );
863
+ });
864
+ });
865
+
866
+ /** An AIDC paper that declares who wrote it. */
867
+ const BLIND = {
868
+ extends: "paperlint:aidc",
869
+ kind: "regular",
870
+ identity: ["Ada Example", "adaexample", "ada@example.org"],
871
+ };
872
+ const said = (patch: Patch) => ieeeFacts(patch);
873
+
874
+ describe("anonymity/identity — a blind venue (AIDC)", () => {
875
+ it("no identity declared: an error, built or not — a missing input is not a clean pass", () => {
876
+ const noIdentity = { extends: "paperlint:aidc", kind: "regular" };
877
+ const built = lint({ venue: noIdentity, facts: ieeeFacts() });
878
+ expect(ids(built)).toEqual(["anonymity/identity:noIdentity"]);
879
+ expect(built[0]?.message).toMatch(/"identity": \["Your Name"/);
880
+ expect(ids(lint({ venue: noIdentity, facts: null }))).toEqual([
881
+ "anonymity/identity:noIdentity",
882
+ "pdf/measured:unbuilt",
883
+ ]);
884
+ expect(
885
+ ids(lint({ venue: { ...noIdentity, identity: [] }, facts: ieeeFacts() })),
886
+ ).toEqual(["anonymity/identity:noIdentity"]);
887
+ });
888
+
889
+ it("declared and not built: silent — pdf/measured already says it was not built", () => {
890
+ expect(ids(lint({ venue: BLIND, facts: null }))).toEqual([
891
+ "pdf/measured:unbuilt",
892
+ ]);
893
+ });
894
+
895
+ it("a name on a page, the metadata and a link target: one finding each, with the place", () => {
896
+ const fs = lint({
897
+ venue: BLIND,
898
+ facts: said((f) => {
899
+ f.pages_text = [
900
+ "Title\nAda\nExample\n",
901
+ ...bodyPages(11),
902
+ "References\n",
903
+ ];
904
+ f.metadata = { Author: "Ada Example", Producer: "pdfTeX-1.40.25" };
905
+ f.links = [{ page: 2, uri: "https://github.com/adaexample/tool" }];
906
+ }),
907
+ });
908
+ expect(fs.map((f) => f.message)).toEqual([
909
+ "PDF, page 1: «Ada Example» matches «Ada Example» in `identity`, and aidc reviews double-blind — remove it, or refer to your own work in the third person",
910
+ "PDF metadata: Author: «Ada Example» matches «Ada Example» in `identity`, and aidc reviews double-blind — remove it, or refer to your own work in the third person",
911
+ "link target on page 2 (https://github.com/adaexample/tool): «adaexample» matches «Ada Example» in `identity`, and aidc reviews double-blind — remove it, or refer to your own work in the third person",
912
+ ]);
913
+ });
914
+
915
+ it("an anonymous-hosting link names nobody, and is not a finding", () => {
916
+ expect(
917
+ lint({
918
+ venue: BLIND,
919
+ facts: said((f) => {
920
+ f.links = [
921
+ { page: 1, uri: "https://anonymous.4open.science/r/tool-1A2B" },
922
+ ];
923
+ }),
924
+ }),
925
+ ).toEqual([]);
926
+ });
927
+
928
+ it("a venue that is not blind: silent, with or without identity, whatever the PDF says", () => {
929
+ const facts = withFacts((f) => (f.pages_text = ["Ada Example\n"]));
930
+ expect(lint({ venue: DECL, facts })).toEqual([]);
931
+ expect(
932
+ lint({ venue: { ...DECL, identity: ["Ada Example"] }, facts }),
933
+ ).toEqual([]);
934
+ });
935
+ });
936
+
937
+ describe("an AIDC kind", () => {
646
938
  it("an AIDC kind that does not exist names the two that do", () => {
647
939
  const fs = lint({ venue: { extends: "paperlint:aidc", kind: "wip" } });
648
940
  expect(ids(fs)).toContain("pdf/profile:kindUnknown");
@@ -677,7 +969,9 @@ describe("a project's own preset, and the facts' other spellings", () => {
677
969
  it("a body size with no tolerance is not judged; a reference range with none is judged exactly", () => {
678
970
  expect(ids(house((f) => (f.body_pt = 12)))).toEqual([]);
679
971
  // 6.9 pt passes agenticdev (its body_pt_tol widens the range) and fails here, where nothing does.
680
- expect(ids(house((f) => (f.ref_pt = 6.9)))).toEqual(["pdf/limits:refPt"]);
972
+ expect(ids(house((f) => (f.ref_pt = 6.9)))).toEqual([
973
+ "pdf/body-size:refPt",
974
+ ]);
681
975
  });
682
976
 
683
977
  it("facts naming the PDF by an absolute path are checked against that file", () => {
@@ -709,3 +1003,21 @@ describe("a project's own preset, and the facts' other spellings", () => {
709
1003
  );
710
1004
  });
711
1005
  });
1006
+
1007
+ describe("judgeBodyEnd, on a venue it does not apply to", () => {
1008
+ const NO_TEXT = { pages: [], bibAnchorPage: null, appendixAnchorPage: null };
1009
+ const venue = (kind: Resolved["kind"]): Resolved => ({
1010
+ venue: "house",
1011
+ format: { ...NO_FORMAT, bodyEndsAt: "references" },
1012
+ kind,
1013
+ kindProblem: null,
1014
+ blind: false,
1015
+ identity: null,
1016
+ });
1017
+
1018
+ it("a kind with no body limit, or no kind: nothing to count against", () => {
1019
+ const limits = { bodyPagesMax: null, refPagesMax: null };
1020
+ expect(judgeBodyEnd(NO_TEXT, venue({ name: "long", limits }))).toEqual([]);
1021
+ expect(judgeBodyEnd(NO_TEXT, venue(null))).toEqual([]);
1022
+ });
1023
+ });