paperlint 6.0.0 → 6.2.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 (141) hide show
  1. package/dist/adapters/latex/index.d.ts +1 -1
  2. package/dist/adapters/latex/index.d.ts.map +1 -1
  3. package/dist/adapters/latex/index.js +3 -2
  4. package/dist/adapters/latex/index.js.map +1 -1
  5. package/dist/adapters/latex/prose.d.ts +6 -1
  6. package/dist/adapters/latex/prose.d.ts.map +1 -1
  7. package/dist/adapters/latex/prose.js +142 -31
  8. package/dist/adapters/latex/prose.js.map +1 -1
  9. package/dist/adapters/mp4box/index.d.ts +4 -0
  10. package/dist/adapters/mp4box/index.d.ts.map +1 -0
  11. package/dist/adapters/mp4box/index.js +46 -0
  12. package/dist/adapters/mp4box/index.js.map +1 -0
  13. package/dist/adapters/node/index.d.ts +1 -0
  14. package/dist/adapters/node/index.d.ts.map +1 -1
  15. package/dist/adapters/node/index.js +1 -0
  16. package/dist/adapters/node/index.js.map +1 -1
  17. package/dist/adapters/node/list-dir.io.d.ts +3 -0
  18. package/dist/adapters/node/list-dir.io.d.ts.map +1 -0
  19. package/dist/adapters/node/list-dir.io.js +15 -0
  20. package/dist/adapters/node/list-dir.io.js.map +1 -0
  21. package/dist/adapters/png/index.d.ts +11 -0
  22. package/dist/adapters/png/index.d.ts.map +1 -0
  23. package/dist/adapters/png/index.js +21 -0
  24. package/dist/adapters/png/index.js.map +1 -0
  25. package/dist/adapters/srt/index.d.ts +4 -0
  26. package/dist/adapters/srt/index.d.ts.map +1 -0
  27. package/dist/adapters/srt/index.js +12 -0
  28. package/dist/adapters/srt/index.js.map +1 -0
  29. package/dist/cli.d.ts.map +1 -1
  30. package/dist/cli.js +40 -22
  31. package/dist/cli.js.map +1 -1
  32. package/dist/domain/register.d.ts +96 -0
  33. package/dist/domain/register.d.ts.map +1 -0
  34. package/dist/domain/register.js +136 -0
  35. package/dist/domain/register.js.map +1 -0
  36. package/dist/domain/talk.d.ts +121 -0
  37. package/dist/domain/talk.d.ts.map +1 -0
  38. package/dist/domain/talk.js +121 -0
  39. package/dist/domain/talk.js.map +1 -0
  40. package/dist/domain/tex-document.d.ts +19 -0
  41. package/dist/domain/tex-document.d.ts.map +1 -1
  42. package/dist/domain/tex-document.js +8 -3
  43. package/dist/domain/tex-document.js.map +1 -1
  44. package/dist/lib/paper-config.d.ts +2 -0
  45. package/dist/lib/paper-config.d.ts.map +1 -1
  46. package/dist/lib/paper-config.js +3 -0
  47. package/dist/lib/paper-config.js.map +1 -1
  48. package/dist/paper-settings.d.ts +3 -0
  49. package/dist/paper-settings.d.ts.map +1 -1
  50. package/dist/paper-settings.js +15 -9
  51. package/dist/paper-settings.js.map +1 -1
  52. package/dist/ports/latex.d.ts +6 -1
  53. package/dist/ports/latex.d.ts.map +1 -1
  54. package/dist/ports/talk-media.d.ts +18 -0
  55. package/dist/ports/talk-media.d.ts.map +1 -0
  56. package/dist/ports/talk-media.js +2 -0
  57. package/dist/ports/talk-media.js.map +1 -0
  58. package/dist/presets.d.ts +6 -0
  59. package/dist/presets.d.ts.map +1 -1
  60. package/dist/presets.js +8 -9
  61. package/dist/presets.js.map +1 -1
  62. package/dist/register-bands.d.ts +37 -0
  63. package/dist/register-bands.d.ts.map +1 -0
  64. package/dist/register-bands.js +196 -0
  65. package/dist/register-bands.js.map +1 -0
  66. package/dist/register.d.ts +4 -2
  67. package/dist/register.d.ts.map +1 -1
  68. package/dist/register.js +6 -45
  69. package/dist/register.js.map +1 -1
  70. package/dist/talk-rules.d.ts +65 -0
  71. package/dist/talk-rules.d.ts.map +1 -0
  72. package/dist/talk-rules.js +451 -0
  73. package/dist/talk-rules.js.map +1 -0
  74. package/dist/tex-requirements.d.ts +9 -0
  75. package/dist/tex-requirements.d.ts.map +1 -1
  76. package/dist/tex-requirements.js +28 -6
  77. package/dist/tex-requirements.js.map +1 -1
  78. package/dist/tex-venue-rules.d.ts +1 -1
  79. package/dist/tex-venue-rules.d.ts.map +1 -1
  80. package/dist/tex-venue-rules.js.map +1 -1
  81. package/docs/configuration.md +2 -1
  82. package/docs/rules/talk/captions-cover.md +47 -0
  83. package/docs/rules/talk/duration-floor.md +38 -0
  84. package/docs/rules/talk/duration.md +43 -0
  85. package/docs/rules/talk/one-slide-size.md +38 -0
  86. package/docs/rules/talk/profile.md +64 -0
  87. package/docs/rules/talk/required-files.md +39 -0
  88. package/docs/rules/talk/undeclared.md +35 -0
  89. package/docs/rules/talk/video-format.md +43 -0
  90. package/docs/rules/tex/claim-emphasis.md +71 -0
  91. package/docs/rules/tex/contrast-frames.md +100 -0
  92. package/docs/rules/tex/relation-markers.md +72 -0
  93. package/docs/rules.md +57 -42
  94. package/docs/talk.md +94 -0
  95. package/fixtures/talk/audio-only.mp4 +0 -0
  96. package/fixtures/talk/one-slide.png +0 -0
  97. package/fixtures/talk/short-audio.mp4 +0 -0
  98. package/fixtures/talk/talk.mov +0 -0
  99. package/fixtures/talk/talk.mp4 +0 -0
  100. package/fixtures/talk/talk.srt +7 -0
  101. package/lib/paper-config.harness.mjs +5 -2
  102. package/lib/paper-config.ts +4 -0
  103. package/package.json +3 -1
  104. package/presets/agenticdev.jsonc +29 -0
  105. package/presets/aidc.jsonc +50 -0
  106. package/presets/venue-profile.schema.json +180 -1
  107. package/skills/grade-paper-writing/SKILL.md +59 -27
  108. package/skills/grade-paper-writing/SKILL.md.spec.ts +58 -26
  109. package/src/adapters/latex/index.test.ts +91 -0
  110. package/src/adapters/latex/index.ts +3 -2
  111. package/src/adapters/latex/prose.ts +188 -58
  112. package/src/adapters/mp4box/index.test.ts +63 -0
  113. package/src/adapters/mp4box/index.ts +50 -0
  114. package/src/adapters/node/index.ts +1 -0
  115. package/src/adapters/node/list-dir.io.ts +15 -0
  116. package/src/adapters/node/list-dir.test.ts +30 -0
  117. package/src/adapters/png/index.test.ts +43 -0
  118. package/src/adapters/png/index.ts +29 -0
  119. package/src/adapters/srt/index.test.ts +24 -0
  120. package/src/adapters/srt/index.ts +13 -0
  121. package/src/cli.test.ts +12 -0
  122. package/src/cli.ts +50 -21
  123. package/src/domain/register.test.ts +153 -0
  124. package/src/domain/register.ts +261 -0
  125. package/src/domain/talk.test.ts +125 -0
  126. package/src/domain/talk.ts +274 -0
  127. package/src/domain/tex-document.ts +37 -3
  128. package/src/new-paper.test.ts +1 -0
  129. package/src/paper-settings.test.ts +18 -1
  130. package/src/paper-settings.ts +21 -11
  131. package/src/ports/latex.ts +6 -0
  132. package/src/ports/talk-media.ts +19 -0
  133. package/src/presets.ts +14 -9
  134. package/src/register-bands.test.ts +327 -0
  135. package/src/register-bands.ts +291 -0
  136. package/src/register.ts +7 -37
  137. package/src/rule-cases.test.ts +177 -20
  138. package/src/talk-rules.test.ts +459 -0
  139. package/src/talk-rules.ts +672 -0
  140. package/src/tex-requirements.ts +56 -8
  141. package/src/tex-venue-rules.ts +7 -1
@@ -137,8 +137,187 @@
137
137
  }
138
138
  },
139
139
  "rules": {
140
- "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.",
141
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
+ }
189
+ },
190
+ "talk": {
191
+ "description": "What the venue asks a presenter to send, judged by the talk/* rules on the finished files (src/talk-rules.ts). Read only for a paper whose paperlint.json declares `talk`. A child preset's block replaces its parent's whole. Durations in seconds.",
192
+ "type": "object",
193
+ "additionalProperties": false,
194
+ "properties": {
195
+ "modes": {
196
+ "description": "The presentation modes the venue allows.",
197
+ "type": "array",
198
+ "items": {
199
+ "type": "string",
200
+ "enum": ["in-person", "remote-video", "remote-live"]
201
+ },
202
+ "uniqueItems": true,
203
+ "minItems": 1
204
+ },
205
+ "artifacts": {
206
+ "description": "What each mode owes: the files required, and those that may be sent.",
207
+ "type": "object",
208
+ "propertyNames": {
209
+ "type": "string",
210
+ "enum": ["in-person", "remote-video", "remote-live"]
211
+ },
212
+ "additionalProperties": {
213
+ "type": "object",
214
+ "additionalProperties": false,
215
+ "required": ["required"],
216
+ "properties": {
217
+ "required": {
218
+ "type": "array",
219
+ "items": {
220
+ "type": "string",
221
+ "enum": [
222
+ "video",
223
+ "one-slide",
224
+ "captions",
225
+ "slides-pdf",
226
+ "poster"
227
+ ]
228
+ },
229
+ "uniqueItems": true
230
+ },
231
+ "optional": {
232
+ "type": "array",
233
+ "items": {
234
+ "type": "string",
235
+ "enum": [
236
+ "video",
237
+ "one-slide",
238
+ "captions",
239
+ "slides-pdf",
240
+ "poster"
241
+ ]
242
+ },
243
+ "uniqueItems": true
244
+ }
245
+ }
246
+ }
247
+ },
248
+ "kinds": {
249
+ "description": "The talk slot per paper kind (the names of format.kinds; the paper's `kind` selects one).",
250
+ "type": "object",
251
+ "additionalProperties": {
252
+ "type": "object",
253
+ "additionalProperties": false,
254
+ "required": ["slot_s", "talk_s_max"],
255
+ "properties": {
256
+ "slot_s": {
257
+ "description": "The whole slot, talk and questions. Informational.",
258
+ "type": "number",
259
+ "exclusiveMinimum": 0
260
+ },
261
+ "talk_s_min": {
262
+ "description": "The shortest talk the venue asks for (talk/duration-floor).",
263
+ "type": "number",
264
+ "minimum": 0
265
+ },
266
+ "talk_s_max": {
267
+ "description": "The longest talk (talk/duration).",
268
+ "type": "number",
269
+ "exclusiveMinimum": 0
270
+ },
271
+ "qa_s": {
272
+ "description": "The questions' share. Informational.",
273
+ "type": "number",
274
+ "minimum": 0
275
+ }
276
+ }
277
+ }
278
+ },
279
+ "video": {
280
+ "description": "The venue's limits on the video file (talk/video-format).",
281
+ "type": "object",
282
+ "additionalProperties": false,
283
+ "properties": {
284
+ "container": {
285
+ "type": "string",
286
+ "enum": ["mp4"]
287
+ },
288
+ "min_height_px": {
289
+ "description": "The lowest vertical resolution; HD is 720.",
290
+ "type": "integer",
291
+ "minimum": 1
292
+ },
293
+ "max_bytes": {
294
+ "type": "integer",
295
+ "minimum": 1
296
+ },
297
+ "captions": {
298
+ "description": "`required`: a captions file is owed with the video (talk/required-files).",
299
+ "type": "string",
300
+ "enum": ["required", "optional"]
301
+ }
302
+ }
303
+ },
304
+ "one_slide": {
305
+ "description": "The one-slide image's size in pixels (talk/one-slide-size), when the venue states one.",
306
+ "type": "object",
307
+ "additionalProperties": false,
308
+ "required": ["width_px", "height_px"],
309
+ "properties": {
310
+ "width_px": {
311
+ "type": "integer",
312
+ "minimum": 1
313
+ },
314
+ "height_px": {
315
+ "type": "integer",
316
+ "minimum": 1
317
+ }
318
+ }
319
+ }
320
+ }
142
321
  }
143
322
  },
144
323
  "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
@@ -8,6 +8,7 @@ import type * as Ast from "@unified-latex/unified-latex-types";
8
8
  import { describe, expect, it } from "vitest";
9
9
  import { runText, spanIn, type ProsePiece } from "../../domain/tex-document.ts";
10
10
  import {
11
+ bodyEmphasis,
11
12
  bodyProse,
12
13
  documentBodyOf,
13
14
  documentClassOf,
@@ -470,6 +471,15 @@ describe("bodyProse — run-in headings and list items", () => {
470
471
  ).toEqual(["It grows.", "It holds."]);
471
472
  });
472
473
 
474
+ it("🔴 braces around a run-in heading change nothing: `{\\textbf{Side-channel eavesdropping:}}` is a heading, left out", () => {
475
+ // barovox-acsac24, Chapters/09_Related_Works.tex, writes its run-in headings this way.
476
+ expect(
477
+ texts(
478
+ "\\begin{document}\n{\\textbf{Side-channel eavesdropping:}} It has been studied.\n\\end{document}",
479
+ ),
480
+ ).toEqual(["It has been studied."]);
481
+ });
482
+
473
483
  it("the same markup inside a sentence, or without the closing mark, is prose", () => {
474
484
  expect(
475
485
  texts(
@@ -541,3 +551,84 @@ describe("documentBodyOf — the body of a source that is a document of its own"
541
551
  );
542
552
  });
543
553
  });
554
+
555
+ describe("bodyEmphasis — phrases set apart inside the body's prose, and where they stand", () => {
556
+ /** Each phrase as `style/place: text`. */
557
+ const shape = (src: string): readonly string[] =>
558
+ bodyEmphasis(parseLatex(src)).map(
559
+ (e) => `${e.style}/${e.place}: ${e.text}`,
560
+ );
561
+
562
+ it("🔴 a bold phrase inside a sentence is inline; one opening a paragraph or an item is a label (opening)", () => {
563
+ expect(
564
+ shape(
565
+ "\\begin{document}\nWe find that \\textbf{the guards react to the word}.\n\n\\textbf{Varying the volume}. We vary it.\n\\begin{itemize}\n\\item \\textbf{A corpus of questions}: we release it.\n\\end{itemize}\n\\end{document}",
566
+ ),
567
+ ).toEqual([
568
+ "bold/inline: the guards react to the word",
569
+ "bold/opening: Varying the volume",
570
+ "bold/opening: A corpus of questions",
571
+ ]);
572
+ });
573
+
574
+ it("🔴 never a heading, a run-in heading, a caption, a table, or anything after the bibliography", () => {
575
+ expect(
576
+ shape(
577
+ "\\begin{document}\n\\section{\\textbf{Big}}\n\\textbf{Threats.} None \\emph{here}.\n\\begin{table}\\caption{\\textbf{C}}\\begin{tabular}{l}\\textbf{cell}\\end{tabular}\\end{table}\n\\bibliography{refs}\nAfter \\textbf{this}.\n\\end{document}",
578
+ ),
579
+ ).toEqual(["italic/inline: here"]);
580
+ });
581
+
582
+ it("braces around a label change nothing: `{\\textbf{Filtering}}.` opening a paragraph is a label", () => {
583
+ // barovox-acsac24, Chapters/08_Discussion.tex.
584
+ expect(
585
+ shape(
586
+ "\\begin{document}\n{\\textbf{Increasing the distance}}. The proximity matters.\n\\end{document}",
587
+ ),
588
+ ).toEqual(["bold/opening: Increasing the distance"]);
589
+ });
590
+ });
591
+
592
+ describe("bodyEmphasis — each style, and edge cases", () => {
593
+ /** Each phrase as `style/place: text`. */
594
+ const shape = (src: string): readonly string[] =>
595
+ bodyEmphasis(parseLatex(src)).map(
596
+ (e) => `${e.style}/${e.place}: ${e.text}`,
597
+ );
598
+
599
+ it("each style: \\emph and \\textit italic, \\underline underline, a `{\\bfseries …}` group bold; a footnote's phrase counts", () => {
600
+ expect(
601
+ shape(
602
+ "\\begin{document}\nA \\emph{b} c \\textit{d} e \\underline{f} g {\\bfseries h i} j.\\footnote{In \\textbf{k}.}\n\\end{document}",
603
+ ),
604
+ ).toEqual([
605
+ "italic/inline: b",
606
+ "italic/inline: d",
607
+ "underline/inline: f",
608
+ "bold/inline: h i",
609
+ "bold/inline: k",
610
+ ]);
611
+ });
612
+
613
+ it("an empty bold macro sets nothing apart, opening a paragraph or not", () => {
614
+ expect(
615
+ shape(
616
+ "\\begin{document}\n\\textbf{} Opens. We \\textbf{ } go.\n\\end{document}",
617
+ ),
618
+ ).toEqual([]);
619
+ });
620
+
621
+ it("an environment opening a paragraph opens nothing for the phrases inside it", () => {
622
+ expect(
623
+ shape(
624
+ "\\begin{document}\n\\begin{quote}\nWe say \\textbf{this}.\n\\end{quote}\n\\end{document}",
625
+ ),
626
+ ).toEqual(["bold/inline: this"]);
627
+ });
628
+
629
+ it("the span is the macro with its argument, where the source has it", () => {
630
+ const src = "\\begin{document}\nWe \\textbf{see it}.\n\\end{document}";
631
+ const [e] = bodyEmphasis(parseLatex(src));
632
+ expect(e && src.slice(e.span.start, e.span.end)).toBe("\\textbf{see it}");
633
+ });
634
+ });
@@ -29,7 +29,7 @@ import {
29
29
  import { documentBodyOf, includesOf } from "./includes.ts";
30
30
  import { outlineOf } from "./outline.ts";
31
31
  import { parseLatex } from "./parse.ts";
32
- import { bodyProse } from "./prose.ts";
32
+ import { bodyEmphasis, bodyProse } from "./prose.ts";
33
33
  import { renderedRuns } from "./rendered.ts";
34
34
  import { layoutOverridesOf } from "./layout.ts";
35
35
 
@@ -50,7 +50,7 @@ export {
50
50
  export { documentBodyOf, includesOf } from "./includes.ts";
51
51
  export { outlineOf } from "./outline.ts";
52
52
  export { parseLatex, type ParsedTex } from "./parse.ts";
53
- export { bodyProse } from "./prose.ts";
53
+ export { bodyEmphasis, bodyProse } from "./prose.ts";
54
54
  export { renderedRuns } from "./rendered.ts";
55
55
  export { layoutOverridesOf } from "./layout.ts";
56
56
 
@@ -60,6 +60,7 @@ export const latexReader: LatexReader = {
60
60
  outline: (src) => outlineOf(parseLatex(src)),
61
61
  renderedRuns: (src) => renderedRuns(parseLatex(src)),
62
62
  bodyProse: (src) => bodyProse(parseLatex(src)),
63
+ bodyEmphasis: (src) => bodyEmphasis(parseLatex(src)),
63
64
  includes: (src) => includesOf(parseLatex(src)),
64
65
  documentBody: (src) => documentBodyOf(parseLatex(src)),
65
66
  template: parseTemplate,