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,19 @@
1
+ {
2
+ "$comment": "anonymity/identity on a real IEEEtran build: D: the name only in the PDF metadata, set by hyperref; no page shows it. Exactly one finding, naming where.",
3
+ "build": {
4
+ "outcome": "pdf"
5
+ },
6
+ "lint": {
7
+ "rules": {
8
+ "anonymity/identity": [
9
+ {
10
+ "severity": 2,
11
+ "says": [
12
+ "PDF metadata: Author",
13
+ "«Ada Example»"
14
+ ]
15
+ }
16
+ ]
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,31 @@
1
+ % D: the name only in the PDF metadata, set by hyperref; no page shows it.
2
+ % The declared identity is invented: "Ada Example", handle "adaexample" (paperlint.json).
3
+ \documentclass[conference,compsoc]{IEEEtran}
4
+ \usepackage{hyperref}
5
+ \hypersetup{pdfauthor={Ada Example}}
6
+ \begin{filecontents*}{refs.bib}
7
+ @article{knuth84,
8
+ title = {Literate Programming},
9
+ author = {Knuth, Donald E.},
10
+ journal = {The Computer Journal},
11
+ volume = {27},
12
+ number = {2},
13
+ pages = {97--111},
14
+ year = {1984},
15
+ doi = {10.1093/comjnl/27.2.97}
16
+ }
17
+ \end{filecontents*}
18
+ \begin{document}
19
+ \title{A Fixture for Double-Blind Review}
20
+ \author{\IEEEauthorblockN{Anonymous Authors}}
21
+ \maketitle
22
+ \section{Introduction}
23
+ This paper builds on earlier work~\cite{knuth84}.
24
+
25
+ \section{Conclusion}
26
+ The conclusion restates that the fixture builds.
27
+ \bibliographystyle{IEEEtran}
28
+ \bibliography{refs}
29
+ \section*{LLM Usage Statement}
30
+ No LLMs were used in writing this fixture.
31
+ \end{document}
@@ -0,0 +1 @@
1
+ {"extends": "paperlint:aidc", "kind": "short", "identity": ["Ada Example", "adaexample", "ada@example.org", "Example University", "exampletool"]}
@@ -18,7 +18,7 @@
18
18
  }
19
19
  },
20
20
  "facts": {
21
- "schema": 2,
21
+ "schema": 3,
22
22
  "lastPageColumnsPt": [
23
23
  621.5,
24
24
  264.8
@@ -45,7 +45,7 @@
45
45
  "pdf/profile": [],
46
46
  "pdf/fonts": [],
47
47
  "pdf/geometry": [],
48
- "pdf/limits": [],
48
+ "format/page-limit": [],
49
49
  "pdf/body-size": [],
50
50
  "pdf/measured": [
51
51
  {
@@ -136,8 +136,10 @@ export const SETTINGS_KEYS: Readonly<Record<string, string>> = Object.freeze({
136
136
  "rule overrides: { id: severity } for every paper in scope, or ESLint blocks (files, ignores, rules) — paperlint lint",
137
137
  extends:
138
138
  "the venue preset: paperlint:<name> or ./path.jsonc — the pdf/ venue rules, paperlint build",
139
- kind: "the kind of paper, whose page limit applies — pdf/limits",
139
+ kind: "the kind of paper, whose page limit applies — format/page-limit",
140
140
  pdf: "where the built PDF is, relative to the paper, when it is not paper.pdf — the facts",
141
+ identity:
142
+ "what identifies the authors — names, handles, emails, affiliations, own project names; a blind venue's PDF must not say any of them — anonymity/identity. The root's list and a paper's are joined",
141
143
  scripts: "how skill prose names the scripts — consumer.mjs",
142
144
  timezone: "the zone deadline anchors are written in — consumer.mjs",
143
145
  contactEmail: "the polite-pool address for citation lookups — consumer.mjs",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperlint",
3
- "version": "5.0.0",
3
+ "version": "6.1.0",
4
4
  "description": "A linter for scientific papers written in LaTeX: catches mistakes before you submit to a conference or journal.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -74,7 +74,9 @@
74
74
  // «font size is 9 pt for all body text»
75
75
  "body_pt": 9,
76
76
  // banal measures the MODE of the rendered text, not the declared size (measured 08-26: 9.30 at
77
- // acmart's declared 9). Hence the tolerance — and hence why body size stays a nudge, not a gate.
77
+ // acmart's declared 9). Hence the tolerance: banal reads 0.3 pt high on every template measured
78
+ // (fixtures/accepted-papers/body-size.json), and 0.5 absorbs that while a paper one size off is
79
+ // still reported.
78
80
  "body_pt_tol": 0.5,
79
81
 
80
82
  // «font size of the references can be from 7 pt to 9 pt»
@@ -22,6 +22,10 @@
22
22
  {
23
23
  "extends": "paperlint:ieee-conference",
24
24
 
25
+ // «Submissions must be properly anonymized for double-blind review.» (the call above), and
26
+ // `"blind":true` in the portal's data. anonymity/identity then requires the paper's `identity`.
27
+ "blind": true,
28
+
25
29
  // What the venue is called in a paper's text (tex/venue-leftover): the workshop, and the
26
30
  // conference it is held at.
27
31
  "aliases": ["AIDC", "ACSAC"],
@@ -39,35 +43,80 @@
39
43
  // for) is not a section with this title and does not count.
40
44
  "required_sections": [{ "title": "LLM Usage Statement", "position": "last" }],
41
45
 
46
+ // THE REGISTER OF THE BODY IS JUDGED AGAINST ACSAC, AIDC'S PARENT CONFERENCE: AIDC 2026 is the
47
+ // workshop's first edition, so it has no accepted papers yet, and it uses ACSAC's template. The
48
+ // anchors are the five accepted ACSAC papers in fixtures/accepted-papers/, measured by paperlint's
49
+ // own counters on their LaTeX source (tex/contrast-frames, tex/claim-emphasis, tex/relation-markers).
50
+ // src/register-bands.test.ts re-measures every anchor that has a fixture and requires these exact
51
+ // numbers, so a change to a counter changes this list in the same commit. The bands they set, per
52
+ // 10,000 words (the anchors' range, widened by two Poisson standard deviations at each end):
53
+ // contrast frames anchors 1.3–5.5 band 0–10.4
54
+ // claims in bold anchors 0.0–10.4 band 0–17.8
55
+ // relation markers anchors 39.4–112.8 band 25.4–135.0
56
+ "register": {
57
+ "anchors": [
58
+ {
59
+ "paper": "barovox-acsac24",
60
+ "words": 7670,
61
+ "contrast_frames": 1,
62
+ "claim_emphasis": 8,
63
+ "relation_markers": 31,
64
+ },
65
+ {
66
+ "paper": "leaking-queries-acsac25",
67
+ "words": 9133,
68
+ "contrast_frames": 5,
69
+ "claim_emphasis": 0,
70
+ "relation_markers": 103,
71
+ },
72
+ {
73
+ "paper": "llm-splained-acsac25",
74
+ "words": 9529,
75
+ "contrast_frames": 4,
76
+ "claim_emphasis": 1,
77
+ "relation_markers": 84,
78
+ },
79
+ {
80
+ "paper": "rr-dataset-quality-acsac24",
81
+ "words": 8130,
82
+ "contrast_frames": 3,
83
+ "claim_emphasis": 0,
84
+ "relation_markers": 32,
85
+ },
86
+ {
87
+ "paper": "secure-acsac24",
88
+ "words": 7519,
89
+ "contrast_frames": 3,
90
+ "claim_emphasis": 0,
91
+ "relation_markers": 38,
92
+ },
93
+ ],
94
+ },
95
+
42
96
  "format": {
43
97
  // \textwidth and \textheight of [conference,compsoc], measured (ieee-conference.jsonc).
44
98
  "textblock_w_in": 7.0,
45
99
  "textblock_h_in": 9.02,
46
100
 
47
- // 🔴 PAGE LIMITS ARE NOT GATED HERE, AND THAT IS A MEASUREMENT. banal splits pages into body
48
- // and references by what is on them, and a page that holds ANY non-bibliography text counts as
49
- // body. Measured 2026-09-27 on a [conference,compsoc] paper whose body ends at the foot of
50
- // page 12:
101
+ // 🔴 THE PAGE LIMIT IS COUNTED BEFORE THE REFERENCES, NOT BY banal. banal splits pages into
102
+ // body and references by what is on them, and a page that holds ANY non-bibliography text
103
+ // counts as body. Measured 2026-09-27 on a [conference,compsoc] paper whose body ends at the
104
+ // foot of page 12:
51
105
  // LLM Usage Statement before the references → body 13, references 0
52
106
  // an appendix before the references → body 13, references 2
53
107
  // both placed after the references → body right, the tail counted as references
54
108
  // AIDC counts neither in the limit («excluding references and appendices»; the ACSAC AI policy:
55
- // the LLM Usage Statement «does not count towards the page limit»), and tex/required-section
56
- // accepts the statement before or after the bibliography. A gate on banal's body count would
57
- // therefore fail a correct 12-page paper — the realm.jsonc case again. The numbers below are
58
- // data for humans, not an input to pdf/limits.
59
- "page_limits_not_gated": {
60
- "_": "Body pages, excluding references, appendices and the LLM Usage Statement. banal counts a page holding an appendix or the statement before the bibliography as body, so its body count is not this number.",
61
- "regular": 12,
62
- "short": 6,
63
- "references_and_appendices": "unlimited",
64
- "llm_usage_statement": "not counted (ACSAC AI policy)",
65
- },
109
+ // the LLM Usage Statement «does not count towards the page limit»). So format/page-limit counts
110
+ // the body up to where the references start (`body_ends_at`): the page they start on counts
111
+ // when body text stands above the heading there, and the limit below is gated. An appendix
112
+ // ends the body too, wherever it sits, when hyperref anchors it (docs/rules/format/page-limit.md).
113
+ "body_ends_at": "references",
66
114
 
67
115
  // «Short position papers or work-in-progress (WIP) papers» share one limit, so one kind.
116
+ // References, appendices and the LLM Usage Statement: unlimited.
68
117
  "kinds": {
69
- "regular": {},
70
- "short": {},
118
+ "regular": { "body_pages_max": 12 },
119
+ "short": { "body_pages_max": 6 },
71
120
  },
72
121
  },
73
122
  }
@@ -59,6 +59,14 @@
59
59
  "body_pt": 10,
60
60
  "body_pt_tol": 0.5,
61
61
 
62
+ // The class sets the bibliography in \footnotesize, 8 pt at 10pt and at 9pt (IEEEtran.cls,
63
+ // \thebibliography). Measured 2026-09-29 on the four built ACSAC papers in
64
+ // fixtures/accepted-papers (body-size.json): pdf.js reads the reference glyphs at 8.00 pt
65
+ // (7.97 without compsoc), banal gives 8.3 on all four. body_pt_tol widens it to 7.5-8.5, so a
66
+ // bibliography shrunk to \scriptsize (7.3) or grown to \small (9.3) is reported.
67
+ "ref_pt_min": 8,
68
+ "ref_pt_max": 8,
69
+
62
70
  // The Times clone. Headings and the title are set in the same family, so a `fonts_title` would
63
71
  // repeat this check and report a Computer Modern build twice.
64
72
  "fonts_text": "NimbusRomNo9L",
@@ -24,6 +24,10 @@
24
24
  "type": "string",
25
25
  "minLength": 1
26
26
  },
27
+ "blind": {
28
+ "description": "The venue reviews double-blind: its call says submissions must be anonymized. Read by `anonymity/identity`, which then requires the paper's `identity` and finds none of it in the PDF. Set only where the venue says so, with the quote. A child's value replaces its parent's.",
29
+ "type": "boolean"
30
+ },
27
31
  "aliases": {
28
32
  "description": "Other names the venue goes by in a paper's text — its acronym, its parent conference (`[\"AISec\", \"ACM CCS\"]`). Read by `tex/venue-leftover`: a paper that extends another venue and says one of these is flagged. Matched as whole words, case-sensitive. Collected along the `extends` chain, with `name`.",
29
33
  "type": "array",
@@ -105,6 +109,11 @@
105
109
  "type": "string",
106
110
  "minLength": 1
107
111
  },
112
+ "body_ends_at": {
113
+ "description": "How `format/page-limit` counts body pages. Absent: banal's body count (every page holding non-bibliography text). `references`: the pages up to where the body ends — at the references or the appendix, whichever comes first, found by hyperref's anchors and then the heading line — with that page counted when body text stands above the heading on it; for a venue whose limit excludes references and appendices (and what follows them, such as statements).",
114
+ "type": "string",
115
+ "enum": ["references"]
116
+ },
108
117
  "page_limits_not_gated": {
109
118
  "type": "object"
110
119
  },
@@ -128,8 +137,55 @@
128
137
  }
129
138
  },
130
139
  "rules": {
131
- "description": "Rules the venue implies, rule id \u2192 ESLint entry (a severity, or [severity, options]). Applied to every paper that extends this preset, before the paper's own rules.",
140
+ "description": "Rules the venue implies, rule id → ESLint entry (a severity, or [severity, options]). Applied to every paper that extends this preset, before the paper's own rules.",
132
141
  "type": "object"
142
+ },
143
+ "register": {
144
+ "description": "The register rules' anchors (`tex/contrast-frames`, `tex/claim-emphasis`, `tex/relation-markers`): accepted papers of the venue, or of its parent conference, each with its body's word count and its count of each measure as paperlint's own counters give them. A measure's band is the anchors' range widened by two Poisson standard deviations at each end (docs/rules/tex/contrast-frames.md). A child preset's block replaces its parent's.",
145
+ "type": "object",
146
+ "additionalProperties": false,
147
+ "required": ["anchors"],
148
+ "properties": {
149
+ "anchors": {
150
+ "type": "array",
151
+ "minItems": 1,
152
+ "items": {
153
+ "type": "object",
154
+ "additionalProperties": false,
155
+ "required": [
156
+ "paper",
157
+ "words",
158
+ "contrast_frames",
159
+ "claim_emphasis",
160
+ "relation_markers"
161
+ ],
162
+ "properties": {
163
+ "paper": {
164
+ "description": "The anchor: a directory under fixtures/accepted-papers/ when the paper is there, or how to find it.",
165
+ "type": "string",
166
+ "minLength": 1
167
+ },
168
+ "words": {
169
+ "description": "Words of the body, as the register rules count them.",
170
+ "type": "integer",
171
+ "minimum": 1
172
+ },
173
+ "contrast_frames": {
174
+ "type": "integer",
175
+ "minimum": 0
176
+ },
177
+ "claim_emphasis": {
178
+ "type": "integer",
179
+ "minimum": 0
180
+ },
181
+ "relation_markers": {
182
+ "type": "integer",
183
+ "minimum": 0
184
+ }
185
+ }
186
+ }
187
+ }
188
+ }
133
189
  }
134
190
  },
135
191
  "allOf": [
@@ -4,7 +4,7 @@ description: Use when asking "does this paper read well?" / "is the writing any
4
4
  allowed-tools: [Read, Write, Edit, Grep, Glob, Agent, Skill]
5
5
  ---
6
6
 
7
- <!-- vigiles:sha256:baf1f69df2502086 compiled from skills/grade-paper-writing/SKILL.md.spec.ts -->
7
+ <!-- vigiles:sha256:03d0377baf71967f compiled from skills/grade-paper-writing/SKILL.md.spec.ts -->
8
8
 
9
9
  # grade-paper-writing — grade how the paper READS, then fix it sentence by sentence
10
10
 
@@ -172,16 +172,41 @@ is what happened.
172
172
  3. **For every dimension scoring ≤3, name the specific offending sentence or section** and write the fix —
173
173
  not "tighten the prose" but the rewritten sentence, or "move the point from ¶3 to ¶1", or "gloss
174
174
  `operation-normalized` in plain words at first use." Fixes must be applyable, not vibes.
175
- 3a. **Grade #10, register distance, against the venue's own papers — not against the exemplars.**
176
- Take 2–3 papers accepted at the target venue (or, for a first-edition workshop, at its parent
177
- conference) in the same paper type: fetch them from the proceedings or the program page, or use the
178
- ones the author supplies. Read the abstract, the introduction and the conclusion of each beside the
179
- draft's, and record, for both sides: how each result is owned (*we measured* · a citation · a named
180
- population), how often a point is made by contrast (*not X but Y*, *rather than*, *instead of* —
181
- the measured difference between this corpus and accepted papers), sentence length and fragments,
182
- and whether any sentence is an aphorism or a closing punchline. The
183
- score is the distance: 5 = a reviewer could not tell the draft from the reference papers by register;
184
- 1 = it reads as a different genre. Quote the reference sentence beside each draft sentence you flag.
175
+ 3a. **Grade #10, register distance, against the venue's own papers, section by section — not against
176
+ the exemplars.** The rules count and this step judges; it starts from their numbers, not from a
177
+ count of its own.
178
+ - **Take the counts from the rules.** `npx paperlint lint <paper-dir>` reports
179
+ `tex/contrast-frames`, `tex/claim-emphasis` and `tex/relation-markers` when the venue preset
180
+ records anchors (AIDC does): each gives the body's rate per 10,000 words, the band, and the
181
+ anchors' range, and above the band it reports every frame and every claim in bold where it
182
+ stands. Those locations are the worklist. Do not recount them by grep — a narrower count is
183
+ gamed by rewording (*, not* → *rather than*), and the rules count every form.
184
+ - **Take the reference papers from the preset.** Its `register.anchors` names the accepted papers
185
+ the band comes from (AIDC: five ACSAC papers, all on arXiv under CC licences). Read two or three
186
+ of the same paper type as LaTeX source (`https://arxiv.org/e-print/<id>`) or as built PDFs. A
187
+ preset without anchors: fetch 2–3 accepted papers of the venue (or, for a first-edition workshop,
188
+ of its parent conference) from the proceedings or the program page, or use the ones the author
189
+ supplies.
190
+ - **Compare section by section, not the paper as a whole:** abstract, introduction, threat model or
191
+ background, method, results, limitations, conclusion. For each, put the draft's section beside the
192
+ same section of each reference paper and record, for both sides: how each result is owned (*we
193
+ measured* · a citation · a named population); how the relation between neighbouring sentences is
194
+ named (*because*, *therefore*, *however*, *for example*) or left to infer (a colon, a dash, a short
195
+ sentence); how often a point is made by what it is not; what is set in bold; and whether any
196
+ sentence is an aphorism, a fragment or a closing punchline. Quote the reference sentence beside
197
+ each draft sentence you flag.
198
+ - **Return concrete rewrites, and every one keeps every number and claim.** For each flagged sentence:
199
+ the draft sentence → the rewrite → the reference sentence it is modelled on → one line saying that
200
+ every number, citation and hedge is unchanged and the claim is neither softened nor strengthened.
201
+ A contrast frame is rewritten by saying what the thing is and naming the relation (*because*,
202
+ *as a result*), not by switching to another contrast form; a claim in bold loses the bold and
203
+ gains its owner (*we measured*, `\ref` to the table); a relation left to infer gets its word.
204
+ Where the frame IS the claim (the paper's thesis sets one mechanism against another), keep it
205
+ once, in the abstract and the introduction, and say so.
206
+ - **Score the distance:** 5 = a reviewer could not tell the draft from the reference papers by
207
+ register, section by section; 1 = it reads as a different genre. After the rewrites, run
208
+ `npx paperlint lint` again: every register rule inside its band is the floor for a 4, not the
209
+ score itself.
185
210
  If no accepted paper can be fetched or supplied, write `#10: not graded — no reference papers` and
186
211
  say which were tried; a register score from memory is the guess this dimension exists to replace.
187
212
  4. **Find the paper's three-move opportunities:** what held assumption could open it? what is its single
@@ -353,10 +378,13 @@ changes neither meaning nor register — and never rewrite a cited work's title
353
378
  **Signposting is the register, not a tic, and this skill never removes it on style grounds.** Frame
354
379
  markers and relation words — *we show*, *this section*, *first … finally*, *in contrast*, *therefore*,
355
380
  *however* — are how academic prose tells the reader what kind of claim comes next and how it relates to
356
- the last one. Their count does not separate a draft from accepted papers (see "Contrast framing"
357
- below); removing them does, in the wrong direction. Flag a connective only when it asserts a relation
358
- the two sentences do not have (a *therefore* that does not follow), or when it is empty filler (*it is
359
- worth noting that*). Never flag one for being academic.
381
+ the last one. Relation words are a floor: `tex/relation-markers` reports a body below the band its
382
+ venue's accepted papers set (AIDC: 25.4 per 10,000 words; the paper reviewers called a blog post stood
383
+ at 19.1, and never wrote *however* once, where every accepted ACSAC paper writes it 6–11 times). Frame
384
+ markers do not separate a draft from accepted papers in either direction (see "Contrast framing"
385
+ below). Flag a connective only when it asserts a relation the two sentences do not have (a *therefore*
386
+ that does not follow), or when it is empty filler (*it is worth noting that*). Never flag one for being
387
+ academic.
360
388
 
361
389
  **Other markers worth a pass:** nominalisation density (*an honest accounting of what the construction
362
390
  costs* — abstract nouns doing the work verbs should do) and statistics chained through a paragraph
@@ -382,14 +410,15 @@ Two separable failures:
382
410
 
383
411
  ### 1. Contrast framing, and a frame that carries nothing
384
412
 
385
- **What separates this corpus from accepted papers is contrast, not signposting — measure that
386
- first.** Counted with the same counter on five accepted ACSAC 2024/25 ML-security papers against a
387
- paper of ours: *"not X but Y"* at 35.5 per 10,000 words in ours against at most 4.9 there (and five
388
- of their six hits were *"not only … but also"*); *"rather than / instead of"* at 19.9 against at most
389
- 5.4. Every sentence that defines its point by what it is not is a sentence whose point the reader
390
- must reconstruct. **Rewrite heuristic:** state the thing; move the rejected alternative to its own
391
- sentence, or drop it if nobody proposed it. Check *instead of · rather than · not … but* first,
392
- against the same count on the reference papers of dimension #10.
413
+ **What separates this corpus from accepted papers is contrast, not signposting — and a rule counts
414
+ it.** `tex/contrast-frames` counts every form (*X, not Y* · *not X but Y* · *rather than* ·
415
+ *instead of* · *as opposed to*) per 10,000 words against the band the venue preset's anchors set: the
416
+ five accepted ACSAC papers measure 1.3–5.5, AIDC's band is 0–10.4, and the paper reviewers called a blog
417
+ post measured 101 before its rewrite and 28 after it. Every sentence that defines its point by what
418
+ it is not is a sentence whose point the reader must reconstruct. **Rewrite heuristic:** state the
419
+ thing and name the relation; move the rejected alternative to its own sentence, or drop it if nobody
420
+ proposed it. Swapping one form for another (*, not* → *rather than*) moves nothing — the rule counts
421
+ both, and so does a reviewer.
393
422
 
394
423
  **Frame markers do not separate us from accepted papers, and are not a target in either direction.**
395
424
  Hyland's *frame markers* (*this paper · this section · we show · first … finally · in what follows*)
@@ -464,10 +493,13 @@ one did, and it was the worst section in the paper by measured density both time
464
493
 
465
494
  ### What is NOT mechanizable here
466
495
 
467
- Whether a frame marker carries its claim (*"we claim nothing there"* genuinely scopes one) and
468
- whether a hedge is honest or defensive. The mechanical leg that does exist: `tex/claim-provenance`
469
- reports every sentence of the body that holds a number and no owner. Count them, surface them, and leave the verdict to a reader.
470
- The counter's job is that nobody can say afterwards they did not know.
496
+ Whether a frame marker carries its claim (*"we claim nothing there"* genuinely scopes one), whether a
497
+ hedge is honest or defensive, whether a given contrast frame is the one a reader needs, and whether a
498
+ rewrite kept every claim at its strength. The mechanical legs that do exist: `tex/claim-provenance`
499
+ reports every sentence of the body that holds a number and no owner, and `tex/contrast-frames`,
500
+ `tex/claim-emphasis` and `tex/relation-markers` report a body outside its venue's band. Count them,
501
+ surface them, and leave the verdict to a reader. The counter's job is that nobody can say afterwards
502
+ they did not know.
471
503
 
472
504
  ## Compose with
473
505
  - `harden-paper` — the multi-axis pre-submit gate calls this as its *writing* axis (the one a defect-hunt
@@ -178,16 +178,41 @@ is what happened.
178
178
  3. **For every dimension scoring ≤3, name the specific offending sentence or section** and write the fix —
179
179
  not "tighten the prose" but the rewritten sentence, or "move the point from ¶3 to ¶1", or "gloss
180
180
  \`operation-normalized\` in plain words at first use." Fixes must be applyable, not vibes.
181
- 3a. **Grade #10, register distance, against the venue's own papers — not against the exemplars.**
182
- Take 2–3 papers accepted at the target venue (or, for a first-edition workshop, at its parent
183
- conference) in the same paper type: fetch them from the proceedings or the program page, or use the
184
- ones the author supplies. Read the abstract, the introduction and the conclusion of each beside the
185
- draft's, and record, for both sides: how each result is owned (*we measured* · a citation · a named
186
- population), how often a point is made by contrast (*not X but Y*, *rather than*, *instead of* —
187
- the measured difference between this corpus and accepted papers), sentence length and fragments,
188
- and whether any sentence is an aphorism or a closing punchline. The
189
- score is the distance: 5 = a reviewer could not tell the draft from the reference papers by register;
190
- 1 = it reads as a different genre. Quote the reference sentence beside each draft sentence you flag.
181
+ 3a. **Grade #10, register distance, against the venue's own papers, section by section — not against
182
+ the exemplars.** The rules count and this step judges; it starts from their numbers, not from a
183
+ count of its own.
184
+ - **Take the counts from the rules.** \`npx paperlint lint <paper-dir>\` reports
185
+ \`tex/contrast-frames\`, \`tex/claim-emphasis\` and \`tex/relation-markers\` when the venue preset
186
+ records anchors (AIDC does): each gives the body's rate per 10,000 words, the band, and the
187
+ anchors' range, and above the band it reports every frame and every claim in bold where it
188
+ stands. Those locations are the worklist. Do not recount them by grep — a narrower count is
189
+ gamed by rewording (*, not* → *rather than*), and the rules count every form.
190
+ - **Take the reference papers from the preset.** Its \`register.anchors\` names the accepted papers
191
+ the band comes from (AIDC: five ACSAC papers, all on arXiv under CC licences). Read two or three
192
+ of the same paper type as LaTeX source (\`https://arxiv.org/e-print/<id>\`) or as built PDFs. A
193
+ preset without anchors: fetch 2–3 accepted papers of the venue (or, for a first-edition workshop,
194
+ of its parent conference) from the proceedings or the program page, or use the ones the author
195
+ supplies.
196
+ - **Compare section by section, not the paper as a whole:** abstract, introduction, threat model or
197
+ background, method, results, limitations, conclusion. For each, put the draft's section beside the
198
+ same section of each reference paper and record, for both sides: how each result is owned (*we
199
+ measured* · a citation · a named population); how the relation between neighbouring sentences is
200
+ named (*because*, *therefore*, *however*, *for example*) or left to infer (a colon, a dash, a short
201
+ sentence); how often a point is made by what it is not; what is set in bold; and whether any
202
+ sentence is an aphorism, a fragment or a closing punchline. Quote the reference sentence beside
203
+ each draft sentence you flag.
204
+ - **Return concrete rewrites, and every one keeps every number and claim.** For each flagged sentence:
205
+ the draft sentence → the rewrite → the reference sentence it is modelled on → one line saying that
206
+ every number, citation and hedge is unchanged and the claim is neither softened nor strengthened.
207
+ A contrast frame is rewritten by saying what the thing is and naming the relation (*because*,
208
+ *as a result*), not by switching to another contrast form; a claim in bold loses the bold and
209
+ gains its owner (*we measured*, \`\\ref\` to the table); a relation left to infer gets its word.
210
+ Where the frame IS the claim (the paper's thesis sets one mechanism against another), keep it
211
+ once, in the abstract and the introduction, and say so.
212
+ - **Score the distance:** 5 = a reviewer could not tell the draft from the reference papers by
213
+ register, section by section; 1 = it reads as a different genre. After the rewrites, run
214
+ \`npx paperlint lint\` again: every register rule inside its band is the floor for a 4, not the
215
+ score itself.
191
216
  If no accepted paper can be fetched or supplied, write \`#10: not graded — no reference papers\` and
192
217
  say which were tried; a register score from memory is the guess this dimension exists to replace.
193
218
  4. **Find the paper's three-move opportunities:** what held assumption could open it? what is its single
@@ -359,10 +384,13 @@ changes neither meaning nor register — and never rewrite a cited work's title
359
384
  **Signposting is the register, not a tic, and this skill never removes it on style grounds.** Frame
360
385
  markers and relation words — *we show*, *this section*, *first … finally*, *in contrast*, *therefore*,
361
386
  *however* — are how academic prose tells the reader what kind of claim comes next and how it relates to
362
- the last one. Their count does not separate a draft from accepted papers (see "Contrast framing"
363
- below); removing them does, in the wrong direction. Flag a connective only when it asserts a relation
364
- the two sentences do not have (a *therefore* that does not follow), or when it is empty filler (*it is
365
- worth noting that*). Never flag one for being academic.
387
+ the last one. Relation words are a floor: \`tex/relation-markers\` reports a body below the band its
388
+ venue's accepted papers set (AIDC: 25.4 per 10,000 words; the paper reviewers called a blog post stood
389
+ at 19.1, and never wrote *however* once, where every accepted ACSAC paper writes it 6–11 times). Frame
390
+ markers do not separate a draft from accepted papers in either direction (see "Contrast framing"
391
+ below). Flag a connective only when it asserts a relation the two sentences do not have (a *therefore*
392
+ that does not follow), or when it is empty filler (*it is worth noting that*). Never flag one for being
393
+ academic.
366
394
 
367
395
  **Other markers worth a pass:** nominalisation density (*an honest accounting of what the construction
368
396
  costs* — abstract nouns doing the work verbs should do) and statistics chained through a paragraph
@@ -388,14 +416,15 @@ Two separable failures:
388
416
 
389
417
  ### 1. Contrast framing, and a frame that carries nothing
390
418
 
391
- **What separates this corpus from accepted papers is contrast, not signposting — measure that
392
- first.** Counted with the same counter on five accepted ACSAC 2024/25 ML-security papers against a
393
- paper of ours: *"not X but Y"* at 35.5 per 10,000 words in ours against at most 4.9 there (and five
394
- of their six hits were *"not only … but also"*); *"rather than / instead of"* at 19.9 against at most
395
- 5.4. Every sentence that defines its point by what it is not is a sentence whose point the reader
396
- must reconstruct. **Rewrite heuristic:** state the thing; move the rejected alternative to its own
397
- sentence, or drop it if nobody proposed it. Check *instead of · rather than · not … but* first,
398
- against the same count on the reference papers of dimension #10.
419
+ **What separates this corpus from accepted papers is contrast, not signposting — and a rule counts
420
+ it.** \`tex/contrast-frames\` counts every form (*X, not Y* · *not X but Y* · *rather than* ·
421
+ *instead of* · *as opposed to*) per 10,000 words against the band the venue preset's anchors set: the
422
+ five accepted ACSAC papers measure 1.3–5.5, AIDC's band is 0–10.4, and the paper reviewers called a blog
423
+ post measured 101 before its rewrite and 28 after it. Every sentence that defines its point by what
424
+ it is not is a sentence whose point the reader must reconstruct. **Rewrite heuristic:** state the
425
+ thing and name the relation; move the rejected alternative to its own sentence, or drop it if nobody
426
+ proposed it. Swapping one form for another (*, not* → *rather than*) moves nothing — the rule counts
427
+ both, and so does a reviewer.
399
428
 
400
429
  **Frame markers do not separate us from accepted papers, and are not a target in either direction.**
401
430
  Hyland's *frame markers* (*this paper · this section · we show · first … finally · in what follows*)
@@ -470,10 +499,13 @@ one did, and it was the worst section in the paper by measured density both time
470
499
 
471
500
  ### What is NOT mechanizable here
472
501
 
473
- Whether a frame marker carries its claim (*"we claim nothing there"* genuinely scopes one) and
474
- whether a hedge is honest or defensive. The mechanical leg that does exist: \`tex/claim-provenance\`
475
- reports every sentence of the body that holds a number and no owner. Count them, surface them, and leave the verdict to a reader.
476
- The counter's job is that nobody can say afterwards they did not know.
502
+ Whether a frame marker carries its claim (*"we claim nothing there"* genuinely scopes one), whether a
503
+ hedge is honest or defensive, whether a given contrast frame is the one a reader needs, and whether a
504
+ rewrite kept every claim at its strength. The mechanical legs that do exist: \`tex/claim-provenance\`
505
+ reports every sentence of the body that holds a number and no owner, and \`tex/contrast-frames\`,
506
+ \`tex/claim-emphasis\` and \`tex/relation-markers\` report a body outside its venue's band. Count them,
507
+ surface them, and leave the verdict to a reader. The counter's job is that nobody can say afterwards
508
+ they did not know.
477
509
 
478
510
  ## Compose with
479
511
  - \`harden-paper\` — the multi-axis pre-submit gate calls this as its *writing* axis (the one a defect-hunt
@@ -119,8 +119,8 @@ try {
119
119
  const fonts = Fonts.safeParse(facts.fonts);
120
120
  const fontList = fonts.success ? fonts.data : [];
121
121
  check(
122
- "the facts are schema 2, about paper.pdf, for the venue paperlint.json declares",
123
- facts.schema === 2 &&
122
+ "the facts are schema 3, about paper.pdf, for the venue paperlint.json declares",
123
+ facts.schema === 3 &&
124
124
  facts.pdf === "paper.pdf" &&
125
125
  facts.venue === "agenticdev" &&
126
126
  facts.kind === "short",
@@ -236,5 +236,5 @@ try {
236
236
  }
237
237
 
238
238
  console.log(
239
- `✓ ${String(check.count)} assertions passed — extract-pdf-facts: the exit-code contract, schema 2, and the no-no-yes font`,
239
+ `✓ ${String(check.count)} assertions passed — extract-pdf-facts: the exit-code contract, schema 3, and the no-no-yes font`,
240
240
  );