paperlint 5.0.0 → 6.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 (177) 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 +2 -1
  6. package/dist/adapters/latex/index.d.ts.map +1 -1
  7. package/dist/adapters/latex/index.js +6 -2
  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/adapters/latex/prose.d.ts +6 -1
  20. package/dist/adapters/latex/prose.d.ts.map +1 -1
  21. package/dist/adapters/latex/prose.js +142 -31
  22. package/dist/adapters/latex/prose.js.map +1 -1
  23. package/dist/cli.d.ts.map +1 -1
  24. package/dist/cli.js +35 -18
  25. package/dist/cli.js.map +1 -1
  26. package/dist/domain/anonymity.d.ts +68 -0
  27. package/dist/domain/anonymity.d.ts.map +1 -0
  28. package/dist/domain/anonymity.js +94 -0
  29. package/dist/domain/anonymity.js.map +1 -0
  30. package/dist/domain/body-pages.d.ts +69 -0
  31. package/dist/domain/body-pages.d.ts.map +1 -0
  32. package/dist/domain/body-pages.js +141 -0
  33. package/dist/domain/body-pages.js.map +1 -0
  34. package/dist/domain/register.d.ts +96 -0
  35. package/dist/domain/register.d.ts.map +1 -0
  36. package/dist/domain/register.js +136 -0
  37. package/dist/domain/register.js.map +1 -0
  38. package/dist/domain/tex-document.d.ts +28 -0
  39. package/dist/domain/tex-document.d.ts.map +1 -1
  40. package/dist/domain/tex-document.js +8 -3
  41. package/dist/domain/tex-document.js.map +1 -1
  42. package/dist/eslint-rules/pdf-last-page-balance.d.ts +1 -1
  43. package/dist/eslint-rules/pdf-last-page-balance.js +2 -2
  44. package/dist/facts-file.d.ts +28 -2
  45. package/dist/facts-file.d.ts.map +1 -1
  46. package/dist/facts-file.js +54 -9
  47. package/dist/facts-file.js.map +1 -1
  48. package/dist/lib/paper-config.d.ts.map +1 -1
  49. package/dist/lib/paper-config.js +2 -1
  50. package/dist/lib/paper-config.js.map +1 -1
  51. package/dist/paper-settings.d.ts +11 -0
  52. package/dist/paper-settings.d.ts.map +1 -1
  53. package/dist/paper-settings.js +38 -4
  54. package/dist/paper-settings.js.map +1 -1
  55. package/dist/pdf-content.d.ts +40 -0
  56. package/dist/pdf-content.d.ts.map +1 -0
  57. package/dist/pdf-content.js +97 -0
  58. package/dist/pdf-content.js.map +1 -0
  59. package/dist/pdf-facts.d.ts +30 -0
  60. package/dist/pdf-facts.d.ts.map +1 -1
  61. package/dist/pdf-facts.js +37 -1
  62. package/dist/pdf-facts.js.map +1 -1
  63. package/dist/ports/latex.d.ts +8 -1
  64. package/dist/ports/latex.d.ts.map +1 -1
  65. package/dist/presets.d.ts +5 -0
  66. package/dist/presets.d.ts.map +1 -1
  67. package/dist/presets.js +8 -1
  68. package/dist/presets.js.map +1 -1
  69. package/dist/register-bands.d.ts +37 -0
  70. package/dist/register-bands.d.ts.map +1 -0
  71. package/dist/register-bands.js +196 -0
  72. package/dist/register-bands.js.map +1 -0
  73. package/dist/register.d.ts +4 -2
  74. package/dist/register.d.ts.map +1 -1
  75. package/dist/register.js +6 -45
  76. package/dist/register.js.map +1 -1
  77. package/dist/tex-requirements.d.ts +10 -0
  78. package/dist/tex-requirements.d.ts.map +1 -1
  79. package/dist/tex-requirements.js +29 -6
  80. package/dist/tex-requirements.js.map +1 -1
  81. package/dist/tex-venue-rules.d.ts +8 -2
  82. package/dist/tex-venue-rules.d.ts.map +1 -1
  83. package/dist/tex-venue-rules.js +40 -0
  84. package/dist/tex-venue-rules.js.map +1 -1
  85. package/dist/types.d.ts +2 -0
  86. package/dist/types.d.ts.map +1 -1
  87. package/dist/venue-rules.d.ts +38 -8
  88. package/dist/venue-rules.d.ts.map +1 -1
  89. package/dist/venue-rules.js +179 -41
  90. package/dist/venue-rules.js.map +1 -1
  91. package/docs/configuration.md +18 -16
  92. package/docs/e2e.md +32 -11
  93. package/docs/rules/anonymity/identity.md +111 -0
  94. package/docs/rules/format/layout-override.md +61 -0
  95. package/docs/rules/format/page-limit.md +108 -0
  96. package/docs/rules/pdf/body-size.md +87 -0
  97. package/docs/rules/tex/claim-emphasis.md +71 -0
  98. package/docs/rules/tex/contrast-frames.md +100 -0
  99. package/docs/rules/tex/relation-markers.md +72 -0
  100. package/docs/rules.md +59 -50
  101. package/eslint-rules/pdf-last-page-balance.harness.mjs +6 -1
  102. package/eslint-rules/pdf-last-page-balance.ts +2 -2
  103. package/fixtures/build-e2e/aidc/expect.json +1 -1
  104. package/fixtures/build-e2e/aidc/paperlint.json +1 -1
  105. package/fixtures/build-e2e/anon-ack/PIPELINE-STATUS.md +3 -0
  106. package/fixtures/build-e2e/anon-ack/expect.json +19 -0
  107. package/fixtures/build-e2e/anon-ack/paper.tex +31 -0
  108. package/fixtures/build-e2e/anon-ack/paperlint.json +1 -0
  109. package/fixtures/build-e2e/anon-author/PIPELINE-STATUS.md +3 -0
  110. package/fixtures/build-e2e/anon-author/expect.json +19 -0
  111. package/fixtures/build-e2e/anon-author/paper.tex +29 -0
  112. package/fixtures/build-e2e/anon-author/paperlint.json +1 -0
  113. package/fixtures/build-e2e/anon-bib/PIPELINE-STATUS.md +3 -0
  114. package/fixtures/build-e2e/anon-bib/expect.json +20 -0
  115. package/fixtures/build-e2e/anon-bib/paper.tex +35 -0
  116. package/fixtures/build-e2e/anon-bib/paperlint.json +1 -0
  117. package/fixtures/build-e2e/anon-href/PIPELINE-STATUS.md +3 -0
  118. package/fixtures/build-e2e/anon-href/expect.json +19 -0
  119. package/fixtures/build-e2e/anon-href/paper.tex +30 -0
  120. package/fixtures/build-e2e/anon-href/paperlint.json +1 -0
  121. package/fixtures/build-e2e/anon-macro/PIPELINE-STATUS.md +3 -0
  122. package/fixtures/build-e2e/anon-macro/expect.json +19 -0
  123. package/fixtures/build-e2e/anon-macro/paper.tex +30 -0
  124. package/fixtures/build-e2e/anon-macro/paperlint.json +1 -0
  125. package/fixtures/build-e2e/anon-pdfauthor/PIPELINE-STATUS.md +3 -0
  126. package/fixtures/build-e2e/anon-pdfauthor/expect.json +19 -0
  127. package/fixtures/build-e2e/anon-pdfauthor/paper.tex +31 -0
  128. package/fixtures/build-e2e/anon-pdfauthor/paperlint.json +1 -0
  129. package/fixtures/build-e2e/unbalanced/expect.json +2 -2
  130. package/lib/paper-config.ts +3 -1
  131. package/package.json +1 -1
  132. package/presets/acm-sigconf.jsonc +3 -1
  133. package/presets/aidc.jsonc +66 -17
  134. package/presets/ieee-conference.jsonc +8 -0
  135. package/presets/venue-profile.schema.json +57 -1
  136. package/skills/grade-paper-writing/SKILL.md +59 -27
  137. package/skills/grade-paper-writing/SKILL.md.spec.ts +58 -26
  138. package/skills/render-paper/extract-pdf-facts.harness.ts +3 -3
  139. package/src/adapters/latex/index.test.ts +91 -0
  140. package/src/adapters/latex/index.ts +6 -2
  141. package/src/adapters/latex/layout.ts +106 -0
  142. package/src/adapters/latex/nodes.ts +11 -3
  143. package/src/adapters/latex/parse.ts +5 -1
  144. package/src/adapters/latex/prose.ts +188 -58
  145. package/src/build.harness.mjs +2 -2
  146. package/src/build.test.ts +5 -0
  147. package/src/cli.test.ts +13 -0
  148. package/src/cli.ts +50 -17
  149. package/src/domain/anonymity.test.ts +163 -0
  150. package/src/domain/anonymity.ts +129 -0
  151. package/src/domain/body-pages.test.ts +300 -0
  152. package/src/domain/body-pages.ts +223 -0
  153. package/src/domain/register.test.ts +153 -0
  154. package/src/domain/register.ts +261 -0
  155. package/src/domain/tex-document.ts +47 -3
  156. package/src/facts-file.test.ts +100 -4
  157. package/src/facts-file.ts +91 -9
  158. package/src/new-paper.test.ts +7 -1
  159. package/src/paper-settings.test.ts +63 -6
  160. package/src/paper-settings.ts +53 -5
  161. package/src/pdf-content.test.ts +71 -0
  162. package/src/pdf-content.ts +125 -0
  163. package/src/pdf-facts.test.ts +110 -2
  164. package/src/pdf-facts.ts +106 -8
  165. package/src/ports/latex.ts +9 -0
  166. package/src/presets.ts +15 -9
  167. package/src/register-bands.test.ts +327 -0
  168. package/src/register-bands.ts +291 -0
  169. package/src/register.ts +7 -37
  170. package/src/rule-cases.test.ts +96 -20
  171. package/src/rule-docs.test.ts +0 -2
  172. package/src/tex-requirements.ts +56 -8
  173. package/src/tex-venue-rules.test.ts +110 -5
  174. package/src/tex-venue-rules.ts +64 -3
  175. package/src/types.ts +2 -0
  176. package/src/venue-rules.test.ts +351 -39
  177. package/src/venue-rules.ts +247 -47
@@ -0,0 +1,69 @@
1
+ /**
2
+ * HOW MANY PAGES THE BODY TAKES, counted from what the PDF says rather than from how banal
3
+ * classifies its pages. A venue that limits the body «excluding references and appendices» (AIDC)
4
+ * cannot be judged on banal's body count: banal calls any page holding non-bibliography text body,
5
+ * so an LLM Usage Statement or an appendix placed after the bibliography is counted as body, and a
6
+ * correct paper fails (measured, `presets/aidc.jsonc`).
7
+ *
8
+ * ── WHERE THE BODY ENDS: AT THE REFERENCES, OR AT THE APPENDIX, WHICHEVER COMES FIRST ──────
9
+ * «Excluding references and appendices» excludes an appendix wherever it sits, so the body ends at
10
+ * the first of the two. Each is found by a structural signal first:
11
+ *
12
+ * references the page of the first bibliography entry, from the destination hyperref writes for
13
+ * every `\bibitem` (`cite.<key>`); then, on that page, the heading line: a line that
14
+ * reads «References» or «Bibliography» (any case, optionally numbered), the one
15
+ * CONFIRMED by a next line opening the first entry, `[1]`, else the last one there.
16
+ * Without hyperref: the first confirmed heading, else the first bare one — a table
17
+ * header or a subsection titled «References» reads the same, so it is a fallback.
18
+ * appendix the page hyperref anchored the appendix on (an `appendix.<letter>` destination, or a
19
+ * bookmark titled «Appendix…»); then, on that page, the line reading «Appendix»,
20
+ * «Appendix A.», «APPENDIX» or «Appendices». Without hyperref the appendix is not
21
+ * looked for: a sentence can end on a line reading «Appendix A.», and nothing tells
22
+ * it from the heading.
23
+ *
24
+ * A structural signal whose page has no heading line, or a confirmed references heading on another
25
+ * page than hyperref's first entry, is a disagreement, and it is said rather than resolved by
26
+ * picking one.
27
+ *
28
+ * ── THE PAGE THE BODY ENDS ON ──────────────────────────────────────────────────────
29
+ * Body text above the heading on that page makes it a body page: a body that runs half a page past
30
+ * the limit is over the limit, and the count must flag it rather than let a desk reject through.
31
+ * Only when the heading is the first text of its page does the body end on the page before. A line
32
+ * with no letter (a page number) is not body text, and neither is a running header or footer — a
33
+ * line that repeats, digits aside, on at least three pages and a third of them.
34
+ */
35
+ /** What ends the body. */
36
+ export type BodyEndsAt = "references" | "appendix";
37
+ /** Where hyperref anchored the bibliography's first entry and the appendix: 1-based pages, or null. */
38
+ export interface Anchors {
39
+ readonly bib: number | null;
40
+ readonly appendix: number | null;
41
+ }
42
+ /** Where the body ends, that no page says, or that a structural signal and the text disagree. */
43
+ export type BodyEnd = {
44
+ readonly kind: "found";
45
+ readonly by: BodyEndsAt;
46
+ /** 1-based page holding the heading that ends the body. */
47
+ readonly page: number;
48
+ /** The body, as the venue counts it: the heading's page too, when body text stands above it. */
49
+ readonly bodyPages: number;
50
+ } | {
51
+ readonly kind: "missing";
52
+ } | {
53
+ readonly kind: "disagree";
54
+ readonly what: BodyEndsAt;
55
+ /** The page of the first confirmed references heading, or null when the anchor's page has no heading. */
56
+ readonly heading: number | null;
57
+ /** The page hyperref anchored it on. */
58
+ readonly anchor: number;
59
+ };
60
+ /** Whether one line of a page's text is the references heading. */
61
+ export declare const isReferencesHeading: (line: string) => boolean;
62
+ /** Whether one line of a page's text is an appendix heading. */
63
+ export declare const isAppendixHeading: (line: string) => boolean;
64
+ /**
65
+ * Where the body ends — at the references or the appendix, whichever comes first — from each
66
+ * page's text and where hyperref anchored the two. Pure.
67
+ */
68
+ export declare function bodyEnd(texts: readonly string[], anchors?: Anchors): BodyEnd;
69
+ //# sourceMappingURL=body-pages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body-pages.d.ts","sourceRoot":"","sources":["../../src/domain/body-pages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,0BAA0B;AAC1B,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,UAAU,CAAC;AAEnD,uGAAuG;AACvG,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,iGAAiG;AACjG,MAAM,MAAM,OAAO,GACf;IACE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC;IACxB,2DAA2D;IAC3D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,gGAAgG;IAChG,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IACE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,yGAAyG;IACzG,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,wCAAwC;IACxC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAAC;AASN,mEAAmE;AACnE,eAAO,MAAM,mBAAmB,GAAI,MAAM,MAAM,KAAG,OACQ,CAAC;AAK5D,gEAAgE;AAChE,eAAO,MAAM,iBAAiB,GAAI,MAAM,MAAM,KAAG,OACmB,CAAC;AAwHrE;;;GAGG;AACH,wBAAgB,OAAO,CACrB,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,OAAO,GAAE,OAAuC,GAC/C,OAAO,CAeT"}
@@ -0,0 +1,141 @@
1
+ /**
2
+ * HOW MANY PAGES THE BODY TAKES, counted from what the PDF says rather than from how banal
3
+ * classifies its pages. A venue that limits the body «excluding references and appendices» (AIDC)
4
+ * cannot be judged on banal's body count: banal calls any page holding non-bibliography text body,
5
+ * so an LLM Usage Statement or an appendix placed after the bibliography is counted as body, and a
6
+ * correct paper fails (measured, `presets/aidc.jsonc`).
7
+ *
8
+ * ── WHERE THE BODY ENDS: AT THE REFERENCES, OR AT THE APPENDIX, WHICHEVER COMES FIRST ──────
9
+ * «Excluding references and appendices» excludes an appendix wherever it sits, so the body ends at
10
+ * the first of the two. Each is found by a structural signal first:
11
+ *
12
+ * references the page of the first bibliography entry, from the destination hyperref writes for
13
+ * every `\bibitem` (`cite.<key>`); then, on that page, the heading line: a line that
14
+ * reads «References» or «Bibliography» (any case, optionally numbered), the one
15
+ * CONFIRMED by a next line opening the first entry, `[1]`, else the last one there.
16
+ * Without hyperref: the first confirmed heading, else the first bare one — a table
17
+ * header or a subsection titled «References» reads the same, so it is a fallback.
18
+ * appendix the page hyperref anchored the appendix on (an `appendix.<letter>` destination, or a
19
+ * bookmark titled «Appendix…»); then, on that page, the line reading «Appendix»,
20
+ * «Appendix A.», «APPENDIX» or «Appendices». Without hyperref the appendix is not
21
+ * looked for: a sentence can end on a line reading «Appendix A.», and nothing tells
22
+ * it from the heading.
23
+ *
24
+ * A structural signal whose page has no heading line, or a confirmed references heading on another
25
+ * page than hyperref's first entry, is a disagreement, and it is said rather than resolved by
26
+ * picking one.
27
+ *
28
+ * ── THE PAGE THE BODY ENDS ON ──────────────────────────────────────────────────────
29
+ * Body text above the heading on that page makes it a body page: a body that runs half a page past
30
+ * the limit is over the limit, and the count must flag it rather than let a desk reject through.
31
+ * Only when the heading is the first text of its page does the body end on the page before. A line
32
+ * with no letter (a page number) is not body text, and neither is a running header or footer — a
33
+ * line that repeats, digits aside, on at least three pages and a third of them.
34
+ */
35
+ /**
36
+ * A line that is a references heading: `References` or `Bibliography`, in any case (IEEEtran sets
37
+ * `REFERENCES` in small caps, which pdf.js may split as `R EFERENCES`), optionally numbered
38
+ * (`7 References`, `VII. REFERENCES`). The line is compared with every space removed.
39
+ */
40
+ const HEADING = /^(?:(?:\d+|[ivxlcdm]+)\.?)?(?:references|bibliography)$/iu;
41
+ /** Whether one line of a page's text is the references heading. */
42
+ export const isReferencesHeading = (line) => HEADING.test(line.normalize("NFKC").replace(/\s+/gu, ""));
43
+ /** A line that is an appendix heading: `Appendix A.`, `APPENDIX`, `Appendices`, spaces removed. */
44
+ const APPENDIX_HEADING = /^appendi(?:x|ces)(?:[a-z]|\d+)?[.:]?$/iu;
45
+ /** Whether one line of a page's text is an appendix heading. */
46
+ export const isAppendixHeading = (line) => APPENDIX_HEADING.test(line.normalize("NFKC").replace(/\s+/gu, ""));
47
+ /** Whether a line opens the first entry of a numbered bibliography: `[1]`. */
48
+ const isFirstEntry = (line) => /^\s*\[\s*1\s*\]/u.test(line);
49
+ function readPage(text) {
50
+ const lines = text.split("\n");
51
+ const headings = lines.flatMap((l, i) => (isReferencesHeading(l) ? [i] : []));
52
+ const confirmed = headings.find((i) => isFirstEntry(lines.slice(i + 1).find((l) => l.trim() !== "") ?? "")) ?? -1;
53
+ return { lines, confirmed, headings };
54
+ }
55
+ /** A line with digits blanked and spaces collapsed: what a running header keeps from page to page. */
56
+ const shape = (line) => line.replace(/\d+/gu, "#").replace(/\s+/gu, " ").trim();
57
+ /** The shapes of the lines that repeat on at least three pages and a third of them. */
58
+ function runningLines(pages) {
59
+ const onPages = pages.flatMap((p) => [...new Set(p.lines.map(shape))]);
60
+ const sorted = onPages.toSorted();
61
+ const floor = Math.max(3, pages.length / 3);
62
+ // In sorted order, a shape on `floor` pages is the same string `floor - 1` places further on.
63
+ return new Set(sorted.filter((s, i) => sorted[i + Math.ceil(floor) - 1] === s));
64
+ }
65
+ /** The heading line the references start at on a page: the confirmed one, else the last one. */
66
+ const startLine = (p) => p.confirmed >= 0 ? p.confirmed : (p.headings.at(-1) ?? -1);
67
+ /** The references heading at line `line` of 0-based page `page`. */
68
+ const at = (page, p, line) => ({
69
+ kind: "at",
70
+ by: "references",
71
+ page,
72
+ line,
73
+ above: p.lines.slice(0, line),
74
+ });
75
+ /** Where the references start: at hyperref's first entry's page, else at the heading in the text. */
76
+ function referencesAt(pages, anchor) {
77
+ const confirmed = pages.findIndex((p) => p.confirmed >= 0);
78
+ if (anchor !== null) {
79
+ const p = pages[anchor - 1];
80
+ const line = p === undefined ? -1 : startLine(p);
81
+ if (p === undefined || line < 0)
82
+ return { kind: "disagree", what: "references", heading: null, anchor };
83
+ if (confirmed >= 0 && confirmed !== anchor - 1)
84
+ return {
85
+ kind: "disagree",
86
+ what: "references",
87
+ heading: confirmed + 1,
88
+ anchor,
89
+ };
90
+ return at(anchor - 1, p, line);
91
+ }
92
+ const i = confirmed >= 0 ? confirmed : pages.findIndex((p) => p.headings.length > 0);
93
+ const p = pages[i];
94
+ return p === undefined ? { kind: "missing" } : at(i, p, startLine(p));
95
+ }
96
+ /** Where the appendix starts: on hyperref's page for it, at the line reading «Appendix». */
97
+ function appendixAt(pages, anchor) {
98
+ if (anchor === null)
99
+ return { kind: "missing" };
100
+ const lines = pages[anchor - 1]?.lines ?? [];
101
+ const line = lines.findIndex(isAppendixHeading);
102
+ return line < 0
103
+ ? { kind: "disagree", what: "appendix", heading: null, anchor }
104
+ : {
105
+ kind: "at",
106
+ by: "appendix",
107
+ page: anchor - 1,
108
+ line,
109
+ above: lines.slice(0, line),
110
+ };
111
+ }
112
+ /** The body's end at a heading: the heading's page counts when body text stands above it. */
113
+ function endAt(pages, at) {
114
+ const running = runningLines(pages);
115
+ const body = at.above.some((l) => /\p{L}/u.test(l) && !running.has(shape(l)));
116
+ return {
117
+ kind: "found",
118
+ by: at.by,
119
+ page: at.page + 1,
120
+ bodyPages: body ? at.page + 1 : at.page,
121
+ };
122
+ }
123
+ /**
124
+ * Where the body ends — at the references or the appendix, whichever comes first — from each
125
+ * page's text and where hyperref anchored the two. Pure.
126
+ */
127
+ export function bodyEnd(texts, anchors = { bib: null, appendix: null }) {
128
+ const pages = texts.map(readPage);
129
+ const starts = [
130
+ referencesAt(pages, anchors.bib),
131
+ appendixAt(pages, anchors.appendix),
132
+ ];
133
+ const disagree = starts.find((s) => s.kind === "disagree");
134
+ if (disagree !== undefined)
135
+ return disagree;
136
+ const first = starts
137
+ .filter((s) => s.kind === "at")
138
+ .toSorted((a, b) => a.page - b.page || a.line - b.line)[0];
139
+ return first === undefined ? { kind: "missing" } : endAt(pages, first);
140
+ }
141
+ //# sourceMappingURL=body-pages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body-pages.js","sourceRoot":"","sources":["../../src/domain/body-pages.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * THE REGISTER OF A PAPER'S BODY, COUNTED — the measures the register rules judge, and the band a
3
+ * venue's accepted papers set for each. Pure: passages and emphasised phrases in, counts out.
4
+ *
5
+ * Three measures, each per 10,000 words of the body, each read off the same passages and cut into
6
+ * the same sentences as every other rule over the body (`sentences.ts`):
7
+ *
8
+ * contrast_frames a claim set against a rejected alternative: «X, not Y», «not X but Y»,
9
+ * «X rather than Y», «instead of Y», «as opposed to Y»
10
+ * claim_emphasis a phrase in bold INSIDE a sentence that states something: six words or more,
11
+ * or holding a number — not a label opening a paragraph or an item, and not a
12
+ * term of a few words
13
+ * relation_markers a word that names how a sentence relates to the one before: because,
14
+ * therefore, however, in contrast, for example, that is …
15
+ *
16
+ * ── THE BAND IS THE ANCHORS', NOT A CONSTANT ────────────────────────────────────
17
+ * A venue preset records its anchors — accepted papers of the venue (or its parent conference),
18
+ * each with its word count and its count of every measure, as these counters give them. The band
19
+ * of a measure is the anchors' range, widened at each end by two Poisson standard deviations of
20
+ * the anchor standing there: a paper of that anchor's length and register would fall outside it
21
+ * about one time in forty. An anchor with no occurrence is widened as if it had one. Below zero is
22
+ * zero. So every number a message names comes from the preset's anchors, and changing an anchor
23
+ * changes the band.
24
+ */
25
+ import type { Emphasis, Passage, Span, TextRun } from "./tex-document.ts";
26
+ /** The measures a venue's anchors set a band for. */
27
+ export type RegisterMeasureName = "contrast_frames" | "claim_emphasis" | "relation_markers";
28
+ export declare const REGISTER_MEASURES: readonly RegisterMeasureName[];
29
+ /** One accepted paper of the venue, as the counters measured it. */
30
+ export interface RegisterAnchor {
31
+ /** What the preset calls it: a directory under `fixtures/accepted-papers/`, or a citation. */
32
+ readonly paper: string;
33
+ readonly words: number;
34
+ readonly counts: Readonly<Record<RegisterMeasureName, number>>;
35
+ }
36
+ /** How far the band reaches past the anchors: this many Poisson standard deviations. */
37
+ export declare const NOISE_SDS = 2;
38
+ /** The words a rate is taken over. */
39
+ export declare const PER = 10000;
40
+ /** A rate per `PER` words; 0 for a body with no words. */
41
+ export declare const rateOf: (count: number, words: number) => number;
42
+ /** A measure's band, and the anchors' own range it was widened from. */
43
+ export interface Band {
44
+ readonly min: number;
45
+ readonly max: number;
46
+ readonly anchorMin: number;
47
+ readonly anchorMax: number;
48
+ readonly anchors: number;
49
+ }
50
+ /** The band the anchors set for a measure; null when there are no anchors. */
51
+ export declare function bandOf(anchors: readonly RegisterAnchor[], m: RegisterMeasureName): Band | null;
52
+ /** A sentence of the body: its text as the reader sees it, its words, its first word, where it stands. */
53
+ export interface BodySentence {
54
+ /** The text with marks and math left out and spaces collapsed, for a message. */
55
+ readonly text: string;
56
+ /** The text with each mark and math as one character, for matching. */
57
+ readonly raw: string;
58
+ /** Its word-like words, and one for each mark or math, as it reads on the page. */
59
+ readonly words: number;
60
+ readonly first: string;
61
+ /** The run it was cut from, and where in that run's text it starts. */
62
+ readonly run: TextRun;
63
+ readonly from: number;
64
+ }
65
+ /** The body's sentences that hold a word, in order. */
66
+ export declare function bodySentences(passages: readonly Passage[]): readonly BodySentence[];
67
+ /** The body's words. */
68
+ export declare const wordsOf: (ss: readonly BodySentence[]) => number;
69
+ /**
70
+ * One occurrence of a measure: the words around it as the reader sees them, the sentence it stands
71
+ * in, and where it is.
72
+ */
73
+ export interface Occurrence {
74
+ readonly match: string;
75
+ readonly sentence: string;
76
+ readonly at: Span | null;
77
+ }
78
+ /**
79
+ * The words around `[from, to)` of a sentence as the reader sees them: the match with up to
80
+ * `CONTEXT` characters on each side, cut at a word, marks and math left out.
81
+ */
82
+ export declare function excerpt(raw: string, from: number, to: number): string;
83
+ /** The contrast frames of the body's sentences. */
84
+ export declare const contrastFrames: (ss: readonly BodySentence[]) => readonly Occurrence[];
85
+ /** The relation markers of the body's sentences. */
86
+ export declare const relationMarkers: (ss: readonly BodySentence[]) => readonly Occurrence[];
87
+ /** A phrase this long or longer states something rather than names it. */
88
+ export declare const CLAIM_WORDS = 6;
89
+ /**
90
+ * Whether an emphasised phrase is a claim set in bold: bold, inside its sentence (not a label that
91
+ * opens a paragraph or an item), and either `CLAIM_WORDS` words long or holding a number.
92
+ */
93
+ export declare const isClaimEmphasis: (e: Emphasis) => boolean;
94
+ /** The claims the body sets in bold. */
95
+ export declare const claimEmphasis: (es: readonly Emphasis[]) => readonly Occurrence[];
96
+ //# sourceMappingURL=register.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../src/domain/register.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,136 @@
1
+ import { runText, spanIn } from "./tex-document.js";
2
+ import { CITATION, MATH, REFERENCE, runOf, sentences } from "./sentences.js";
3
+ export const REGISTER_MEASURES = [
4
+ "contrast_frames",
5
+ "claim_emphasis",
6
+ "relation_markers",
7
+ ];
8
+ /** How far the band reaches past the anchors: this many Poisson standard deviations. */
9
+ export const NOISE_SDS = 2;
10
+ /** The words a rate is taken over. */
11
+ export const PER = 10_000;
12
+ /** A rate per `PER` words; 0 for a body with no words. */
13
+ export const rateOf = (count, words) => words === 0 ? 0 : (PER * count) / words;
14
+ /** One anchor's rate of a measure, and how far one standard deviation of its count reaches. */
15
+ const anchorRate = (a, m) => {
16
+ const count = a.counts[m];
17
+ return {
18
+ rate: rateOf(count, a.words),
19
+ sd: rateOf(Math.sqrt(Math.max(count, 1)), a.words),
20
+ };
21
+ };
22
+ /** The band the anchors set for a measure; null when there are no anchors. */
23
+ export function bandOf(anchors, m) {
24
+ const rates = anchors.map((a) => anchorRate(a, m));
25
+ const low = rates.reduce((lo, r) => (lo === null || r.rate < lo.rate ? r : lo), null);
26
+ const high = rates.reduce((hi, r) => (hi === null || r.rate > hi.rate ? r : hi), null);
27
+ if (low === null || high === null)
28
+ return null;
29
+ return {
30
+ min: Math.max(0, low.rate - NOISE_SDS * low.sd),
31
+ max: high.rate + NOISE_SDS * high.sd,
32
+ anchorMin: low.rate,
33
+ anchorMax: high.rate,
34
+ anchors: anchors.length,
35
+ };
36
+ }
37
+ // ── sentences, as every register measure reads them ───────────────────────────────
38
+ /** The characters a mark or math stands as in a sentence's text. */
39
+ const MARK = new RegExp(`[${CITATION}${REFERENCE}${MATH}]`, "gu");
40
+ const WORDS = new Intl.Segmenter("en", { granularity: "word" });
41
+ /** The body's sentences that hold a word, in order. */
42
+ export function bodySentences(passages) {
43
+ return passages.flatMap((p) => {
44
+ const run = runOf(p);
45
+ const text = runText(run);
46
+ return sentences(text).flatMap(([from, to]) => {
47
+ const raw = text.slice(from, to);
48
+ const words = [...WORDS.segment(raw)].filter((w) => w.isWordLike);
49
+ const [first] = words;
50
+ if (first === undefined)
51
+ return [];
52
+ return [
53
+ {
54
+ text: raw.replace(MARK, "").replace(/\s+/gu, " ").trim(),
55
+ raw,
56
+ words: words.length + (raw.match(MARK)?.length ?? 0),
57
+ first: first.segment,
58
+ run,
59
+ from,
60
+ },
61
+ ];
62
+ });
63
+ });
64
+ }
65
+ /** The body's words. */
66
+ export const wordsOf = (ss) => ss.reduce((n, s) => n + s.words, 0);
67
+ /** The forms as one pattern: matched left to right, so one phrase is never two occurrences. */
68
+ const anyOf = (forms) => new RegExp(forms.map((r) => `(?:${r.source})`).join("|"), "giu");
69
+ /**
70
+ * «not» after a comma, a dash (`---` as the source spells it) or a semicolon — «the word, not the
71
+ * operation», «--- not authenticity» —
72
+ * unless it opens a phrase that rejects nothing («not only», «not yet», «not already in the corpus»).
73
+ */
74
+ const COMMA_NOT = /(?:[,;—–]|--)\s*(?:and\s+|but\s+)?not\s+(?!(?:only|least|yet|always|necessarily|all|even|just|merely|surprisingly|already|to)\b)/giu;
75
+ /** «not X but Y» inside one clause — no comma, colon or semicolon between — but not «not only … but also». */
76
+ const NOT_BUT = /\bnot\b(?!\s+only\b)[^.;:,]{1,60}?\bbut\b(?!\s+also\b)/giu;
77
+ /** Every form of a contrast frame, as one pattern over one sentence. */
78
+ const CONTRAST_FRAME = anyOf([
79
+ COMMA_NOT,
80
+ NOT_BUT,
81
+ /\brather\s+than\b/u,
82
+ /\binstead\s+of\b/u,
83
+ /\bas\s+opposed\s+to\b/u,
84
+ ]);
85
+ /**
86
+ * Words that name the relation between a sentence and the one before it: cause, consequence,
87
+ * contrast, concession, example, restatement. The list is the one the register diagnosis measured
88
+ * (every marker whose meaning does not depend on context), not tuned to this corpus.
89
+ */
90
+ const RELATION_MARKERS = /(?<![\p{L}])(?:therefore|thus|hence|consequently|as a result|because|since|this means|this implies|which explains|in turn|that is,|i\.e\.|in contrast|by contrast|however|nevertheless|nonetheless|whereas|in other words|for this reason|it follows|accordingly|specifically,|in particular,|for example|for instance|e\.g\.)(?![\p{L}])/giu;
91
+ /** How many characters of context an excerpt keeps on each side of a match. */
92
+ const CONTEXT = 40;
93
+ /**
94
+ * The words around `[from, to)` of a sentence as the reader sees them: the match with up to
95
+ * `CONTEXT` characters on each side, cut at a word, marks and math left out.
96
+ */
97
+ export function excerpt(raw, from, to) {
98
+ const start = Math.max(0, from - CONTEXT);
99
+ const end = Math.min(raw.length, to + CONTEXT);
100
+ const words = raw
101
+ .slice(start, end)
102
+ .replace(MARK, "")
103
+ .split(/\s+/u)
104
+ .filter((w) => w !== "");
105
+ const inner = words.slice(start > 0 ? 1 : 0, end < raw.length ? -1 : words.length);
106
+ return `${start > 0 ? "…" : ""}${inner.join(" ")}${end < raw.length ? "…" : ""}`;
107
+ }
108
+ /** Every match of `pattern` in every sentence, in order, with where it stands. */
109
+ function occurrences(ss, pattern) {
110
+ return ss.flatMap((s) => [...s.raw.matchAll(pattern)].map((m) => ({
111
+ match: excerpt(s.raw, m.index, m.index + m[0].length),
112
+ sentence: s.text,
113
+ at: spanIn(s.run, s.from + m.index, s.from + m.index + m[0].length),
114
+ })));
115
+ }
116
+ /** The contrast frames of the body's sentences. */
117
+ export const contrastFrames = (ss) => occurrences(ss, CONTRAST_FRAME);
118
+ /** The relation markers of the body's sentences. */
119
+ export const relationMarkers = (ss) => occurrences(ss, RELATION_MARKERS);
120
+ /** A phrase this long or longer states something rather than names it. */
121
+ export const CLAIM_WORDS = 6;
122
+ const wordCount = (s) => [...WORDS.segment(s)].filter((w) => w.isWordLike).length;
123
+ /**
124
+ * Whether an emphasised phrase is a claim set in bold: bold, inside its sentence (not a label that
125
+ * opens a paragraph or an item), and either `CLAIM_WORDS` words long or holding a number.
126
+ */
127
+ export const isClaimEmphasis = (e) => e.style === "bold" &&
128
+ e.place === "inline" &&
129
+ (wordCount(e.text) >= CLAIM_WORDS || /\p{N}/u.test(e.text));
130
+ /** The claims the body sets in bold. */
131
+ export const claimEmphasis = (es) => es.filter(isClaimEmphasis).map((e) => ({
132
+ match: e.text,
133
+ sentence: e.text,
134
+ at: e.span,
135
+ }));
136
+ //# sourceMappingURL=register.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register.js","sourceRoot":"","sources":["../../src/domain/register.ts"],"names":[],"mappings":"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"}
@@ -116,4 +116,32 @@ export type ProsePiece = {
116
116
  export interface Passage {
117
117
  readonly pieces: readonly [ProsePiece, ...ProsePiece[]];
118
118
  }
119
+ /** How an emphasised phrase is set: bold, italic (or `\emph`), or underlined. */
120
+ export type EmphasisStyle = "bold" | "italic" | "underline";
121
+ /**
122
+ * Where an emphasised phrase stands: opening its paragraph or list item — a label, set like a run-in
123
+ * heading even without the full stop (`\item \textbf{A corpus of questions}: …`) — or inside the
124
+ * running text, after other words of its sentence.
125
+ */
126
+ export type EmphasisPlace = "opening" | "inline";
127
+ /**
128
+ * A phrase set in bold, italics or underline inside the body's running prose — not a heading, a
129
+ * run-in heading, a caption, a float or a table, which are never prose (`LatexReader.bodyProse`).
130
+ * `text` is what it typesets, marks and math left out.
131
+ */
132
+ export interface Emphasis {
133
+ readonly style: EmphasisStyle;
134
+ readonly place: EmphasisPlace;
135
+ readonly text: string;
136
+ readonly span: Span;
137
+ }
138
+ /**
139
+ * A command that changes the page layout a venue's template sets: the text block, the margins,
140
+ * the line spacing, or space pulled back with a negative skip. `command` is how a message names it
141
+ * (`\setlength{\textheight}`, `\vspace{-2mm}`).
142
+ */
143
+ export interface LayoutOverride {
144
+ readonly command: string;
145
+ readonly place: Place;
146
+ }
119
147
  //# sourceMappingURL=tex-document.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tex-document.d.ts","sourceRoot":"","sources":["../../src/domain/tex-document.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tex-document.d.ts","sourceRoot":"","sources":["../../src/domain/tex-document.ts"],"names":[],"mappings":"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"}
@@ -26,10 +26,15 @@ export const documentClassLine = (d) => `\\documentclass${d.options.length > 0 ?
26
26
  export const runText = (run) => run.segments.map((s) => s.text).join("");
27
27
  /** The source offset of the `i`-th character of the text these segments make, or null outside it. */
28
28
  function offsetIn(segments, i) {
29
- const [s, ...rest] = segments;
30
- if (s === undefined || i < 0)
29
+ if (i < 0)
31
30
  return null;
32
- return i < s.text.length ? s.at + i : offsetIn(rest, i - s.text.length);
31
+ // One pass, no copy of the rest per segment: a paragraph's run holds thousands of segments.
32
+ const found = segments.reduce((acc, s) => acc.at !== null
33
+ ? acc
34
+ : acc.rest < s.text.length
35
+ ? { rest: acc.rest, at: s.at + acc.rest }
36
+ : { rest: acc.rest - s.text.length, at: null }, { rest: i, at: null });
37
+ return found.at;
33
38
  }
34
39
  /**
35
40
  * The source span of `runText(run).slice(from, to)`: from its first character to past its last.
@@ -1 +1 @@
1
- {"version":3,"file":"tex-document.js","sourceRoot":"","sources":["../../src/domain/tex-document.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAYH,mGAAmG;AACnG,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,CAAS,EAAU,EAAE,CAC5C,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AA8B5C,sFAAsF;AACtF,MAAM,CAAC,MAAM,cAAc,GAAG,CAC5B,GAAkB,EAClB,IAAmB,EACA,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;AAQ7E;;;;GAIG;AACH,MAAM,UAAU,UAAU,CACxB,IAAmB,EACnB,IAAmB;IAEnB,IAAI,IAAI,CAAC,GAAG,KAAK,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IAChE,MAAM,OAAO,GAAG,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC3C,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC;QACzB,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE;QAClB,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AACxC,CAAC;AAED,iGAAiG;AACjG,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAgB,EAAU,EAAE,CAC5D,kBAAkB,CAAC,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC;AA4BvF,sBAAsB;AACtB,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,GAAY,EAAU,EAAE,CAC9C,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAE3C,qGAAqG;AACrG,SAAS,QAAQ,CAAC,QAA4B,EAAE,CAAS;IACvD,MAAM,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,QAAQ,CAAC;IAC9B,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC1E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,MAAM,CAAC,GAAY,EAAE,IAAY,EAAE,EAAU;IAC3D,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/D,OAAO,KAAK,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,GAAG,CAAC,EAAE,CAAC;AAC3E,CAAC"}
1
+ {"version":3,"file":"tex-document.js","sourceRoot":"","sources":["../../src/domain/tex-document.ts"],"names":[],"mappings":"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"}
@@ -25,7 +25,7 @@ export type ParsedFacts = {
25
25
  /** The difference, in points, that two columns may end apart. See the harness for why 120. */
26
26
  export declare const DEFAULT_TOLERANCE_PT = 120;
27
27
  /** The schema of `paper.facts.json` this rule reads (`src/facts-file.ts`, FACTS_SCHEMA). */
28
- export declare const FACTS_SCHEMA = 2;
28
+ export declare const FACTS_SCHEMA = 3;
29
29
  export declare const FACTS_REL: string;
30
30
  /**
31
31
  * The facts file's text → what this rule needs, or the reason it cannot be judged. Pure.
@@ -62,7 +62,7 @@ const RuleOptions = z
62
62
  /** The difference, in points, that two columns may end apart. See the harness for why 120. */
63
63
  export const DEFAULT_TOLERANCE_PT = 120;
64
64
  /** The schema of `paper.facts.json` this rule reads (`src/facts-file.ts`, FACTS_SCHEMA). */
65
- export const FACTS_SCHEMA = 2;
65
+ export const FACTS_SCHEMA = 3;
66
66
  export const FACTS_REL = join("_build", "paper.facts.json");
67
67
  /** The `last_page` field, parsed. */
68
68
  function lastPageOf(v) {
@@ -205,7 +205,7 @@ const rule = {
205
205
  noFacts: "pdf/last-page-balance is on for this paper, but {{file}} does not exist beside it. The rule " +
206
206
  "judges the facts `paperlint build` writes after a successful compile — build the paper first.",
207
207
  factsBroken: "{{why}} in _build/paper.facts.json — rebuild the paper (`paperlint build`) to rewrite it",
208
- schema: "_build/paper.facts.json has schema {{got}}; this rule reads schema 2 — rebuild the paper " +
208
+ schema: "_build/paper.facts.json has schema {{got}}; this rule reads schema 3 — rebuild the paper " +
209
209
  "(`paperlint build`) to rewrite it",
210
210
  pdfMissing: "_build/paper.facts.json describes {{pdf}}, which is not on disk (a failed build removes " +
211
211
  "it) — rebuild the paper",