paperlint 2.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/CLAUDE.md +6 -1
  2. package/CONTRIBUTING.md +21 -0
  3. package/README.md +231 -180
  4. package/action.yml +4 -6
  5. package/dist/adapters/references/index.d.ts +13 -0
  6. package/dist/adapters/references/index.d.ts.map +1 -0
  7. package/dist/adapters/references/index.js +55 -0
  8. package/dist/adapters/references/index.js.map +1 -0
  9. package/dist/adapters/references/reach.io.d.ts +3 -0
  10. package/dist/adapters/references/reach.io.d.ts.map +1 -0
  11. package/dist/adapters/references/reach.io.js +14 -0
  12. package/dist/adapters/references/reach.io.js.map +1 -0
  13. package/dist/build-engine.d.ts.map +1 -1
  14. package/dist/build-engine.js +5 -2
  15. package/dist/build-engine.js.map +1 -1
  16. package/dist/build.d.ts +16 -1
  17. package/dist/build.d.ts.map +1 -1
  18. package/dist/build.js +40 -5
  19. package/dist/build.js.map +1 -1
  20. package/dist/cli.d.ts +37 -20
  21. package/dist/cli.d.ts.map +1 -1
  22. package/dist/cli.js +377 -251
  23. package/dist/cli.js.map +1 -1
  24. package/dist/doctor.d.ts.map +1 -1
  25. package/dist/doctor.js +37 -52
  26. package/dist/doctor.js.map +1 -1
  27. package/dist/facts-file.d.ts +33 -9
  28. package/dist/facts-file.d.ts.map +1 -1
  29. package/dist/facts-file.js +117 -17
  30. package/dist/facts-file.js.map +1 -1
  31. package/dist/hooks-settings.d.ts +0 -21
  32. package/dist/hooks-settings.d.ts.map +1 -1
  33. package/dist/hooks-settings.js +17 -135
  34. package/dist/hooks-settings.js.map +1 -1
  35. package/dist/init.d.ts +40 -19
  36. package/dist/init.d.ts.map +1 -1
  37. package/dist/init.js +144 -109
  38. package/dist/init.js.map +1 -1
  39. package/dist/link-skills.d.ts +1 -1
  40. package/dist/link-skills.d.ts.map +1 -1
  41. package/dist/link-skills.js +11 -29
  42. package/dist/link-skills.js.map +1 -1
  43. package/dist/new-paper.d.ts +2 -2
  44. package/dist/new-paper.d.ts.map +1 -1
  45. package/dist/new-paper.js +6 -2
  46. package/dist/new-paper.js.map +1 -1
  47. package/dist/paper-files.d.ts +63 -0
  48. package/dist/paper-files.d.ts.map +1 -0
  49. package/dist/paper-files.js +69 -0
  50. package/dist/paper-files.js.map +1 -0
  51. package/dist/paper-settings.d.ts +40 -0
  52. package/dist/paper-settings.d.ts.map +1 -0
  53. package/dist/paper-settings.js +144 -0
  54. package/dist/paper-settings.js.map +1 -0
  55. package/dist/ports/check-references.d.ts +31 -0
  56. package/dist/ports/check-references.d.ts.map +1 -0
  57. package/dist/ports/check-references.js +7 -0
  58. package/dist/ports/check-references.js.map +1 -0
  59. package/dist/presets.d.ts +84 -0
  60. package/dist/presets.d.ts.map +1 -0
  61. package/dist/presets.js +184 -0
  62. package/dist/presets.js.map +1 -0
  63. package/dist/reference-rules.d.ts +47 -0
  64. package/dist/reference-rules.d.ts.map +1 -0
  65. package/dist/reference-rules.js +156 -0
  66. package/dist/reference-rules.js.map +1 -0
  67. package/dist/references.d.ts +36 -0
  68. package/dist/references.d.ts.map +1 -0
  69. package/dist/references.js +91 -0
  70. package/dist/references.js.map +1 -0
  71. package/dist/rules-config.d.ts +11 -2
  72. package/dist/rules-config.d.ts.map +1 -1
  73. package/dist/rules-config.js +25 -11
  74. package/dist/rules-config.js.map +1 -1
  75. package/dist/structure.d.ts.map +1 -1
  76. package/dist/structure.js +3 -2
  77. package/dist/structure.js.map +1 -1
  78. package/dist/tex-requirements.d.ts +63 -7
  79. package/dist/tex-requirements.d.ts.map +1 -1
  80. package/dist/tex-requirements.js +91 -24
  81. package/dist/tex-requirements.js.map +1 -1
  82. package/dist/types.d.ts +20 -18
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/venue-rules.d.ts +109 -0
  85. package/dist/venue-rules.d.ts.map +1 -0
  86. package/dist/venue-rules.js +389 -0
  87. package/dist/venue-rules.js.map +1 -0
  88. package/docs/configuration.md +152 -77
  89. package/docs/e2e.md +1 -1
  90. package/docs/install.md +25 -40
  91. package/docs/optional-rules.md +26 -24
  92. package/docs/rules.md +181 -15
  93. package/eslint-rules/bib-reachable-entry.mjs +75 -0
  94. package/eslint-rules/latex-language.mjs +29 -1
  95. package/eslint-rules/paper-stages.harness.mjs +1 -117
  96. package/eslint-rules/paper-stages.mjs +0 -120
  97. package/eslint-rules/paper-stages.mutations.mjs +0 -76
  98. package/eslint-rules/paper-typography.mjs +267 -192
  99. package/eslint-rules/paper-typography.test.mjs +297 -0
  100. package/eslint-rules/papers.harness.mjs +8 -12
  101. package/eslint-rules/papers.mjs +17 -27
  102. package/eslint-rules/pdf-last-page-balance.mjs +23 -5
  103. package/eslint-rules/review-frontmatter.mjs +118 -0
  104. package/eslint-rules/review-frontmatter.schema.json +36 -0
  105. package/eslint-rules/review-frontmatter.test.mjs +118 -0
  106. package/eslint-rules/sibling-frontmatter.mjs +19 -0
  107. package/eslint-rules/sibling-frontmatter.schema.json +14 -0
  108. package/eslint.config.mjs +0 -15
  109. package/fixtures/build-e2e/acmart/paperlint.json +1 -0
  110. package/fixtures/real-markdown-paper/baseline.json +6 -7
  111. package/hooks/hooks.harness.mjs +39 -98
  112. package/hooks/hooks.mutations.mjs +6 -6
  113. package/hooks/paper-edit-guard.hook.d.mts +2 -3
  114. package/hooks/paper-edit-guard.hook.mjs +31 -42
  115. package/hooks/paper-skills-nudge.hook.mjs +21 -19
  116. package/hooks/paper-status-gates.hook.mjs +21 -19
  117. package/hooks/paper-status-gates.sh +8 -13
  118. package/lib/paper-config.d.mts +15 -18
  119. package/lib/paper-config.harness.mjs +18 -35
  120. package/lib/paper-config.mjs +90 -81
  121. package/lib/paper-config.mutations.mjs +2 -2
  122. package/lib/skill-checks.mjs +1 -1
  123. package/lib/skill-trigger-cases.harness.mjs +9 -8
  124. package/lib/skill-trigger-cases.mjs +7 -15
  125. package/package.json +1 -1
  126. package/scripts/eslint-report-guard.mjs +1 -1
  127. package/scripts/harness-api.frozen.json +0 -3
  128. package/scripts/mutation-batteries.frozen.json +4 -13
  129. package/skills/analyze-sibling-paper/SKILL.md +16 -12
  130. package/skills/analyze-sibling-paper/SKILL.md.spec.ts +15 -11
  131. package/skills/cold-read-diff/SKILL.md +20 -1
  132. package/skills/cold-read-diff/SKILL.md.spec.ts +19 -0
  133. package/skills/find-venue/SKILL.md +5 -1
  134. package/skills/find-venue/SKILL.md.spec.ts +4 -0
  135. package/skills/grade-paper-writing/SKILL.md +20 -1
  136. package/skills/grade-paper-writing/SKILL.md.spec.ts +19 -0
  137. package/skills/paper-pipeline/PIPELINE-MAP.md +14 -4
  138. package/skills/paper-pipeline/SKILL.md +4 -3
  139. package/skills/paper-pipeline/SKILL.md.spec.ts +3 -2
  140. package/skills/paper-pipeline/scripts/consumer.d.mts +0 -1
  141. package/skills/paper-pipeline/scripts/consumer.harness.mjs +8 -12
  142. package/skills/paper-pipeline/scripts/consumer.mjs +19 -33
  143. package/skills/paper-pipeline/scripts/extract-ref-facts.harness.mjs +6 -4
  144. package/skills/paper-pipeline/scripts/run-mechanical.mjs +2 -11
  145. package/skills/plan-paper-timeline/SKILL.md +2 -2
  146. package/skills/plan-paper-timeline/SKILL.md.spec.ts +1 -1
  147. package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +1 -1
  148. package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +2 -2
  149. package/skills/render-paper/check-render.sh +2 -3
  150. package/skills/render-paper/extract-pdf-facts.harness.mjs +5 -5
  151. package/skills/render-paper/extract-pdf-facts.mjs +4 -4
  152. package/skills/render-paper/render-paper.harness.mjs +2 -2
  153. package/skills/study-accepted-papers/SKILL.md +2 -1
  154. package/skills/study-accepted-papers/SKILL.md.spec.ts +1 -0
  155. package/skills/submit-paper/SKILL.md +9 -1
  156. package/skills/submit-paper/SKILL.md.spec.ts +8 -0
  157. package/skills/submit-paper/references/venues/acm-sigconf.jsonc +86 -0
  158. package/skills/submit-paper/references/venues/agenticdev.jsonc +34 -102
  159. package/skills/submit-paper/references/venues/aisec.jsonc +21 -82
  160. package/skills/submit-paper/references/venues/realm.jsonc +45 -43
  161. package/skills/submit-paper/references/venues/tex-base.jsonc +1 -1
  162. package/skills/submit-paper/references/venues/venue-profile.schema.json +119 -29
  163. package/skills/verify-citations/SKILL.md +10 -1
  164. package/skills/verify-citations/SKILL.md.spec.ts +9 -0
  165. package/skills/verify-citations/scripts/bib-authors.mjs +48 -26
  166. package/src/adapters/references/index.ts +102 -0
  167. package/src/adapters/references/reach.io.ts +12 -0
  168. package/src/build-engine.ts +10 -2
  169. package/src/build.harness.mjs +31 -22
  170. package/src/build.ts +65 -8
  171. package/src/cli.harness.mjs +151 -294
  172. package/src/cli.mutations.mjs +35 -44
  173. package/src/cli.ts +457 -288
  174. package/src/doctor.harness.mjs +41 -84
  175. package/src/doctor.mutations.mjs +9 -9
  176. package/src/doctor.ts +41 -61
  177. package/src/facts-file.test.ts +47 -7
  178. package/src/facts-file.ts +145 -24
  179. package/src/hooks-settings.harness.mjs +7 -92
  180. package/src/hooks-settings.mutations.mjs +2 -17
  181. package/src/hooks-settings.ts +18 -162
  182. package/src/init.test.ts +206 -0
  183. package/src/init.ts +193 -159
  184. package/src/link-skills.harness.mjs +1 -38
  185. package/src/link-skills.mutations.mjs +9 -5
  186. package/src/link-skills.ts +10 -34
  187. package/src/lint-fix.test.ts +120 -0
  188. package/src/new-paper.harness.mjs +9 -6
  189. package/src/new-paper.test.ts +133 -0
  190. package/src/new-paper.ts +7 -2
  191. package/src/paper-files.test.ts +290 -0
  192. package/src/paper-files.ts +144 -0
  193. package/src/paper-settings-commands.test.ts +274 -0
  194. package/src/paper-settings.test.ts +249 -0
  195. package/src/paper-settings.ts +208 -0
  196. package/src/ports/check-references.ts +28 -0
  197. package/src/presets.test.ts +236 -0
  198. package/src/presets.ts +297 -0
  199. package/src/reference-rules.ts +229 -0
  200. package/src/references.test.ts +209 -0
  201. package/src/references.ts +138 -0
  202. package/src/rules-config.harness.mjs +24 -8
  203. package/src/rules-config.ts +30 -13
  204. package/src/structure.harness.mjs +6 -4
  205. package/src/structure.ts +3 -2
  206. package/src/tex-requirements.harness.mjs +11 -17
  207. package/src/tex-requirements.ts +179 -29
  208. package/src/types.ts +30 -18
  209. package/src/venue-rules.test.ts +554 -0
  210. package/src/venue-rules.ts +571 -0
  211. package/templates/paper/paperlint.json +4 -0
  212. package/eslint-rules/doc-fields.harness.mjs +0 -336
  213. package/eslint-rules/doc-fields.mjs +0 -186
  214. package/eslint-rules/doc-fields.mutations.mjs +0 -96
  215. package/eslint-rules/paper-typography.harness.mjs +0 -291
  216. package/eslint-rules/paper-typography.mutations.mjs +0 -131
  217. package/eslint-rules/review-findings-cause.harness.mjs +0 -228
  218. package/eslint-rules/review-findings-cause.mjs +0 -135
  219. package/eslint-rules/review-findings-cause.mutations.mjs +0 -72
  220. package/fixtures/build-e2e/acmart/venue.json +0 -1
  221. package/fixtures/paper-stages/authors-ran/PIPELINE-STATUS.md +0 -16
  222. package/fixtures/paper-stages/marker-in-prose/PIPELINE-STATUS.md +0 -17
  223. package/fixtures/review-findings-cause/clean.md +0 -17
  224. package/fixtures/review-findings-cause/defect.md +0 -14
  225. package/fixtures/review-findings-cause/old-debt.md +0 -14
  226. package/fixtures/review-findings-cause/quiet-in-fence.md +0 -16
@@ -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, Bash(node .claude/skills/paper-pipeline/scripts/announce.mjs:*), Bash(node .claude/skills/paper-pipeline/scripts/ledger.mjs:*)]
5
5
  ---
6
6
 
7
- <!-- vigiles:sha256:374531c7f64e998e compiled from skills/grade-paper-writing/SKILL.md.spec.ts -->
7
+ <!-- vigiles:sha256:c0907d5c98e2c831 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
 
@@ -255,6 +255,25 @@ is what happened.
255
255
  `PIPELINE-STATUS.md` (Wc row). Same rule as `pc-panel-review`'s ledger and `analyze-sibling-paper`'s
256
256
  `siblings/` — review artifacts live colocated and ride with the paper to camera-ready/extension.
257
257
 
258
+ ## The report's frontmatter — the findings, as records
259
+
260
+ The report opens with YAML frontmatter that lists what it found, one record per finding.
261
+ `paperlint lint` validates it (`review/frontmatter`): an **open** finding must name the
262
+ pipeline `cause` that let it through — fix that, and the text edit falls out of running the
263
+ fixed tool.
264
+
265
+ ```yaml
266
+ ---
267
+ findings:
268
+ - id: 1
269
+ status: open # open | fixed | wontfix
270
+ cause: missing-skill # skill-defect | missing-skill | hook | rule — required when open
271
+ title: "§3 opens on a term the reader has not met"
272
+ - id: 2
273
+ status: fixed
274
+ ---
275
+ ```
276
+
258
277
  ## Record the verdict
259
278
 
260
279
  🔴 LAST step, once the deliverable exists:
@@ -271,6 +271,25 @@ is what happened.
271
271
  \`PIPELINE-STATUS.md\` (Wc row). Same rule as \`pc-panel-review\`'s ledger and \`analyze-sibling-paper\`'s
272
272
  \`siblings/\` — review artifacts live colocated and ride with the paper to camera-ready/extension.
273
273
 
274
+ ## The report's frontmatter — the findings, as records
275
+
276
+ The report opens with YAML frontmatter that lists what it found, one record per finding.
277
+ \`paperlint lint\` validates it (\`review/frontmatter\`): an **open** finding must name the
278
+ pipeline \`cause\` that let it through — fix that, and the text edit falls out of running the
279
+ fixed tool.
280
+
281
+ \`\`\`yaml
282
+ ---
283
+ findings:
284
+ - id: 1
285
+ status: open # open | fixed | wontfix
286
+ cause: missing-skill # skill-defect | missing-skill | hook | rule — required when open
287
+ title: "§3 opens on a term the reader has not met"
288
+ - id: 2
289
+ status: fixed
290
+ ---
291
+ \`\`\`
292
+
274
293
  ## Record the verdict
275
294
 
276
295
  🔴 LAST step, once the deliverable exists:
@@ -199,6 +199,14 @@ format.
199
199
 
200
200
  ## 3. Venue constants belong in a card, not in code
201
201
 
202
+ > **Done (#79, 2.1.0), in a different shape than planned below.** The constants live in **venue
203
+ > presets**, JSONC files in `submit-paper/references/venues/`: `acm-sigconf.jsonc` is the ACM
204
+ > family (the `format` block — page size, columns, fonts, font sizes — plus the `tex` packages),
205
+ > `agenticdev.jsonc` and `aisec.jsonc` extend it and add their `kinds` (page limits) and `rules`,
206
+ > `realm.jsonc` stands alone. A paper picks one in `<paper>/paperlint.json`
207
+ > (`{ "extends": "paperlint:<venue>", "kind": "…" }`); the `pdf/*` lint rules judge the built PDF
208
+ > against it. The `.md` cards stay as prose. What follows is the plan as it was written.
209
+
202
210
  Right now `LinLibertine`, `LinBiolinum`, `acmart` are hardcoded into `check-render.sh`. This is
203
211
  **venue data**, and it belongs in
204
212
  `.claude/skills/submit-paper/references/venues/<venue>.md`:
@@ -292,7 +300,7 @@ imports, paths inside regexes).
292
300
  ## 2. One build entry point for all papers, not one per paper
293
301
 
294
302
  > **Done (#59, 2026-09-24).** `paperlint build <paper>` is the entry point, and `paperlint toolchain` installs
295
- > TeX Live with the packages the venue profiles declare; `ensure-toolchain.sh` and
303
+ > TeX Live with the packages the venue presets declare; `ensure-toolchain.sh` and
296
304
  > `ci-install-texlive.sh` are deleted. What follows is the plan as it was written.
297
305
 
298
306
  **The hole:** `compile-rules` has `repro/build-submission.sh`, which calls `ensure-toolchain.sh`.
@@ -324,9 +332,11 @@ point. Nothing to check.
324
332
  | page limit | **4** | `venues/realm.md` 8 · `build-submission.sh` 8 · `PIPELINE-STATUS` "8/8" · the build prints **9** |
325
333
  | the TeX package list | **5** | `ensure-toolchain.sh` (executable) · `SKILL.md` · `render-paper.harness.mjs` (pins it ✅) · `build-submission.sh` · `PIPELINE-STATUS` |
326
334
 
327
- The canonical source is the venue card (`venues/<venue>.md`, the `<!-- venue-profile -->` block) for
328
- format, and — since 2026-09-24 — the `tex` block of `venues/<venue>.jsonc` (plus `tex-base.jsonc`)
329
- for packages; `ensure-toolchain.sh` held them until then. Everything else is a pointer.
335
+ The canonical source is the venue preset (`venues/<name>.jsonc`, resolved through its `extends`
336
+ chain): its `format` block for the numbers (page size, columns, fonts, font sizes, and `kinds` for
337
+ page limits), its `tex` block (plus `tex-base.jsonc`) for packages — since 2026-09-24;
338
+ `ensure-toolchain.sh` held them until then — and its `rules` block for the checks the venue
339
+ implies. The `.md` card is prose about the venue. Everything else is a pointer.
330
340
 
331
341
  ## 3-ter. Drop the font gate from `check-render.sh` once the rules run in CI
332
342
 
@@ -4,7 +4,7 @@ description: The orchestrator for writing a research paper end-to-end, from idea
4
4
  allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, WebSearch, WebFetch, Agent, Skill]
5
5
  ---
6
6
 
7
- <!-- vigiles:sha256:6ec7cc1d25db0a0f compiled from skills/paper-pipeline/SKILL.md.spec.ts -->
7
+ <!-- vigiles:sha256:291d2266e29874be compiled from skills/paper-pipeline/SKILL.md.spec.ts -->
8
8
 
9
9
  # paper-pipeline — the conductor for the whole organism
10
10
 
@@ -28,8 +28,9 @@ of that — who is filing what, and when — stays in the author's own private n
28
28
  - `references/acl-venue-rules.md` — what holds at **every** ACL-family venue and workshop: the
29
29
  page-limit map, the appendix rule and the trap inside it, Limitations/Ethics as free-but-restricted
30
30
  space, anonymity, OpenReview mechanics. Fetch once, not per paper. Venue-specific facts stay in
31
- `submit-paper/references/venues/<venue>.md`. (No equivalent yet for ACM or IEEE — write one when
32
- the next paper goes there.)
31
+ `submit-paper/references/venues/<venue>.md`. (No prose equivalent yet for ACM or IEEE — write one
32
+ when the next paper goes there. ACM's machine-checked format already exists: the
33
+ `paperlint:acm-sigconf` preset, `submit-paper/references/venues/acm-sigconf.jsonc`.)
33
34
 
34
35
  ## The readiness scorecard (which boxes are checked)
35
36
  Stages run in subagents and report into chat, which evaporates — so "did everything run?" becomes
@@ -45,8 +45,9 @@ of that — who is filing what, and when — stays in the author's own private n
45
45
  - \`references/acl-venue-rules.md\` — what holds at **every** ACL-family venue and workshop: the
46
46
  page-limit map, the appendix rule and the trap inside it, Limitations/Ethics as free-but-restricted
47
47
  space, anonymity, OpenReview mechanics. Fetch once, not per paper. Venue-specific facts stay in
48
- \`submit-paper/references/venues/<venue>.md\`. (No equivalent yet for ACM or IEEE — write one when
49
- the next paper goes there.)
48
+ \`submit-paper/references/venues/<venue>.md\`. (No prose equivalent yet for ACM or IEEE — write one
49
+ when the next paper goes there. ACM's machine-checked format already exists: the
50
+ \`paperlint:acm-sigconf\` preset, \`submit-paper/references/venues/acm-sigconf.jsonc\`.)
50
51
 
51
52
  ## The readiness scorecard (which boxes are checked)
52
53
  Stages run in subagents and report into chat, which evaporates — so "did everything run?" becomes
@@ -29,5 +29,4 @@ export declare function installedSkills(dir: string): string[];
29
29
  export declare function packageVenuesDir(): string;
30
30
  export declare const SHIPPED_SKILLS_DIR: string;
31
31
  export declare const PACKAGE_NAME: "paperlint";
32
- export declare const LEGACY_PACKAGE_NAME: "research-paper-pipeline";
33
32
  export declare const BIN_FILE: "bin/paperlint.mjs";
@@ -32,7 +32,6 @@ import { dirname, isAbsolute, join, resolve } from "node:path";
32
32
  import { fileURLToPath } from "node:url";
33
33
  import { spawnSync } from "node:child_process";
34
34
  import {
35
- CONFIG_KEY,
36
35
  DEFAULT_SCRIPTS_ROOT,
37
36
  consumerRoot,
38
37
  consumerSkillsDir,
@@ -87,15 +86,12 @@ const HERE = dirname(fileURLToPath(import.meta.url));
87
86
  const TMP = realpathSync(mkdtempSync(join(tmpdir(), "consumer-harness-")));
88
87
  process.on("exit", () => rmSync(TMP, { recursive: true, force: true }));
89
88
 
90
- /** A throwaway consumer repository with the given `paperlint` block (or none). */
89
+ /** A throwaway consumer repository with the given root `paperlint.json` settings (or none). */
91
90
  function fakeConsumer(block) {
92
91
  const root = mkdtempSync(join(TMP, "repo-"));
93
- writeFileSync(
94
- join(root, "package.json"),
95
- JSON.stringify(
96
- block === undefined ? { name: "x" } : { name: "x", [CONFIG_KEY]: block },
97
- ),
98
- );
92
+ writeFileSync(join(root, "package.json"), JSON.stringify({ name: "x" }));
93
+ if (block !== undefined)
94
+ writeFileSync(join(root, "paperlint.json"), JSON.stringify(block));
99
95
  return root;
100
96
  }
101
97
  /** A directory that looks like an installed copy of this package. */
@@ -188,7 +184,7 @@ function installedDir(root) {
188
184
  );
189
185
  assert.match(
190
186
  err.message,
191
- /package\.json/,
187
+ /paperlint\.json/,
192
188
  "the refusal must name the file the key goes in",
193
189
  );
194
190
  }
@@ -409,12 +405,12 @@ assert.equal(
409
405
  );
410
406
  assert.match(
411
407
  missing.message,
412
- /package\.json/,
408
+ /paperlint\.json/,
413
409
  "the refusal must name the file the key goes in",
414
410
  );
415
411
 
416
412
  // And the default's version of that message must say the default was used — otherwise someone
417
- // who declared nothing goes looking in package.json for a line that is not there.
413
+ // who declared nothing goes looking in paperlint.json for a line that is not there.
418
414
  assert.throws(
419
415
  () => scriptsRoot({ env: {}, cwd: fakeConsumer(undefined) }),
420
416
  /Nothing was declared/,
@@ -462,7 +458,7 @@ assert.equal(
462
458
  assert.equal(s.ledger, "a/b/ledger.mjs");
463
459
 
464
460
  // EXACTLY ONE slash, whatever the consumer typed. `scriptsRoot()` returns the declaration
465
- // verbatim, so a trailing slash in package.json arrives here intact; `${root}/` would then give
461
+ // verbatim, so a trailing slash in paperlint.json arrives here intact; `${root}/` would then give
466
462
  // `a/b//`, which matches nothing — the silent direction again.
467
463
  const typedSlash = pipelineScripts("a/b/");
468
464
  assert.equal(
@@ -56,7 +56,7 @@
56
56
  * ── THE LEDGER, THREE RUNGS ─────────────────────────────────────────────────
57
57
  * 1. `PIPELINE_LEDGER` in the environment — always wins. This is what test harnesses set to
58
58
  * keep fixture rows out of real history, so it must outrank a declaration on disk.
59
- * 2. `"paperlint": { "ledger": "…" }` in the CONSUMER's `package.json`, read
59
+ * 2. `{ "ledger": "…" }` in the CONSUMER's root `paperlint.json`, read
60
60
  * from `process.cwd()`, resolved relative to it.
61
61
  * 3. `runs.jsonl` beside this file — ONLY when this file is not inside `node_modules`, i.e.
62
62
  * when the package is being developed in its own checkout. Inside `node_modules` with
@@ -75,7 +75,6 @@
75
75
  */
76
76
  import {
77
77
  existsSync,
78
- readFileSync,
79
78
  readdirSync,
80
79
  readlinkSync,
81
80
  realpathSync,
@@ -86,7 +85,11 @@ import { fileURLToPath, pathToFileURL } from "node:url";
86
85
 
87
86
  // Re-exported, not re-declared — see the note in `lib/paper-config.mjs`. This file is not a hook
88
87
  // and never needed its own copy.
89
- import { CONFIG_KEY, settingsOf } from "../../../lib/paper-config.mjs";
88
+ import {
89
+ CONFIG_FILE,
90
+ CONFIG_KEY,
91
+ settingsOf,
92
+ } from "../../../lib/paper-config.mjs";
90
93
  export { CONFIG_KEY, settingsOf };
91
94
 
92
95
  /**
@@ -144,13 +147,6 @@ export function packageVenuesDir() {
144
147
  */
145
148
  export const PACKAGE_NAME = "paperlint";
146
149
 
147
- /**
148
- * The name the package was published under before 2.0.0. Only for recognising what an older
149
- * install left behind (hook commands, skill links, the config key) so `init` can replace it and
150
- * `doctor` can name it.
151
- */
152
- export const LEGACY_PACKAGE_NAME = "research-paper-pipeline";
153
-
154
150
  /**
155
151
  * The package's one executable, relative to the package root — what `bin` in package.json names
156
152
  * and what every hook command runs. Named like the package; `consumer.harness.mjs` checks it
@@ -245,16 +241,6 @@ function readlinkOr(entry) {
245
241
  }
246
242
  }
247
243
 
248
- /** The consumer's parsed `package.json`, or `null` when there is none or it does not parse. */
249
- export function consumerPkg(opts) {
250
- const file = join(consumerRoot(opts), "package.json");
251
- try {
252
- return JSON.parse(readFileSync(file, "utf8"));
253
- } catch {
254
- return null;
255
- }
256
- }
257
-
258
244
  /**
259
245
  * True when `dir` lies inside a `node_modules` directory.
260
246
  *
@@ -282,7 +268,7 @@ export function ledgerPath(
282
268
 
283
269
  // Rung 2 — the consumer's declaration.
284
270
  const root = consumerRoot({ env, cwd });
285
- const declared = settingsOf(consumerPkg({ env, cwd }))?.ledger;
271
+ const declared = settingsOf(consumerRoot({ env, cwd }))?.ledger;
286
272
  // 🔴 `declared === undefined`, NOT `declared ?? default` — the same distinction `papersRoot()`
287
273
  // makes and for the same reason: `"ledger": null` is a keystroke, not an absence, and reading
288
274
  // it as "nothing was declared" would silently pick a different file than the one asked for.
@@ -299,8 +285,8 @@ export function ledgerPath(
299
285
  throw new Error(
300
286
  `${CONFIG_KEY}: no ledger location is declared, and the default (a file beside this ` +
301
287
  `module) is inside node_modules, which \`npm ci\` deletes.\n` +
302
- `Declare where the ledger lives, in ${join(root, "package.json")}:\n` +
303
- ` "${CONFIG_KEY}": { "ledger": "docs/pipeline-runs.jsonl" }\n` +
288
+ `Declare where the ledger lives, in ${join(root, CONFIG_FILE)}:\n` +
289
+ ` { "ledger": "docs/pipeline-runs.jsonl" }\n` +
304
290
  `or set PIPELINE_LEDGER for a single run.\n` +
305
291
  `This is thrown rather than defaulted on purpose: appending to a path under ` +
306
292
  `node_modules succeeds, so the rows would look recorded right up until the next ` +
@@ -321,7 +307,7 @@ export function ledgerPathExisting(hereDir, opts) {
321
307
  if (!existsSync(p))
322
308
  throw new Error(
323
309
  `${CONFIG_KEY}: the ledger "${p}" does not exist. Create it (an empty file is a valid ` +
324
- `empty ledger) or fix the "ledger" declaration in package.json.`,
310
+ `empty ledger) or fix the "ledger" declaration in ${CONFIG_FILE}.`,
325
311
  );
326
312
  return p;
327
313
  }
@@ -374,7 +360,7 @@ export const DEFAULT_SCRIPTS_ROOT = ".claude/skills/paper-pipeline/scripts";
374
360
  */
375
361
  export function scriptsRoot({ env = process.env, cwd = process.cwd() } = {}) {
376
362
  const root = consumerRoot({ env, cwd });
377
- const declared = settingsOf(consumerPkg({ env, cwd }))?.scripts;
363
+ const declared = settingsOf(consumerRoot({ env, cwd }))?.scripts;
378
364
  // 🔴 `declared === undefined`, NOT `declared ?? DEFAULT` — the same distinction `papersRoot()`
379
365
  // and `ledgerPath()` make, for the same reason: `"scripts": null` is a keystroke, not an
380
366
  // absence, and silently substituting the default for it hides a typo behind a working run.
@@ -393,8 +379,8 @@ export function scriptsRoot({ env = process.env, cwd = process.cwd() } = {}) {
393
379
  `instruction would point at a directory the repository does not track.\n` +
394
380
  `Keep a symlink where the prose already looks —\n` +
395
381
  ` ${join(root, DEFAULT_SCRIPTS_ROOT)} -> node_modules/${CONFIG_KEY}/skills/paper-pipeline/scripts\n` +
396
- `— or declare the real, repository-relative location in ${join(root, "package.json")}:\n` +
397
- ` "${CONFIG_KEY}": { "scripts": "path/to/pipeline/scripts" }\n` +
382
+ `— or declare the real, repository-relative location in ${join(root, CONFIG_FILE)}:\n` +
383
+ ` { "scripts": "path/to/pipeline/scripts" }\n` +
398
384
  `(If nothing was declared, the root itself is under node_modules: run the command from ` +
399
385
  `the consumer repository, or export CLAUDE_PROJECT_DIR.)`,
400
386
  );
@@ -403,9 +389,9 @@ export function scriptsRoot({ env = process.env, cwd = process.cwd() } = {}) {
403
389
  `${CONFIG_KEY}: the pipeline scripts path "${rel}" does not exist under ${root}.\n` +
404
390
  (declared === undefined
405
391
  ? `Nothing was declared, so the default "${DEFAULT_SCRIPTS_ROOT}" was used. Create the ` +
406
- `symlink there, or declare the real location in package.json:\n` +
407
- ` "${CONFIG_KEY}": { "scripts": "path/to/pipeline/scripts" }`
408
- : `It is declared in package.json under "${CONFIG_KEY}" as ` +
392
+ `symlink there, or declare the real location in ${CONFIG_FILE}:\n` +
393
+ ` { "scripts": "path/to/pipeline/scripts" }`
394
+ : `It is declared in ${CONFIG_FILE} as ` +
409
395
  `"scripts": ${JSON.stringify(declared)}. Fix it there, or create the directory.`) +
410
396
  `\nThis is thrown rather than ignored on purpose: this value is a PREFIX that callers ` +
411
397
  `filter prose with, so a wrong one matches no instruction at all and every check built ` +
@@ -453,7 +439,7 @@ export function consumerTimezone({
453
439
  env = process.env,
454
440
  cwd = process.cwd(),
455
441
  } = {}) {
456
- const declared = settingsOf(consumerPkg({ env, cwd }))?.timezone;
442
+ const declared = settingsOf(consumerRoot({ env, cwd }))?.timezone;
457
443
  // 🔴 `declared === undefined`, NOT `declared ?? DEFAULT` — the same distinction every carrier
458
444
  // above makes: `"timezone": null` is a keystroke, not an absence.
459
445
  const tz = declared === undefined ? DEFAULT_TIMEZONE : declared;
@@ -467,7 +453,7 @@ export function consumerTimezone({
467
453
  throw new RangeError(
468
454
  `${CONFIG_KEY}: "timezone" is ${JSON.stringify(tz)}, which is not an IANA time zone this ` +
469
455
  `runtime knows (e.g. "Europe/Berlin", "America/New_York", "UTC").\n` +
470
- `Fix it in package.json under "${CONFIG_KEY}".\n` +
456
+ `Fix it in ${CONFIG_FILE}.\n` +
471
457
  `This throws rather than falling back to ${DEFAULT_TIMEZONE} on purpose: a silent fallback ` +
472
458
  `would put every deadline anchor at the wrong hour while looking like it worked.`,
473
459
  );
@@ -505,7 +491,7 @@ export function consumerContactEmail({
505
491
  env = process.env,
506
492
  cwd = process.cwd(),
507
493
  } = {}) {
508
- const declared = settingsOf(consumerPkg({ env, cwd }))?.contactEmail;
494
+ const declared = settingsOf(consumerRoot({ env, cwd }))?.contactEmail;
509
495
  if (declared === undefined || declared === null) return null;
510
496
  if (typeof declared !== "string" || !declared.includes("@"))
511
497
  throw new TypeError(
@@ -21,7 +21,11 @@
21
21
  * as success, so an exported test object would report ✓ without running anything.
22
22
  */
23
23
  import assert from "node:assert/strict";
24
- import { PAPERS_DIR_FIELD } from "../../../lib/paper-config.mjs";
24
+ import {
25
+ DEFAULT_PAPERS_ROOT,
26
+ PAPERS_DIR_FIELD,
27
+ settingsOf,
28
+ } from "../../../lib/paper-config.mjs";
25
29
  import {
26
30
  existsSync,
27
31
  mkdirSync,
@@ -57,9 +61,7 @@ const X = await import(join(HERE, "extract-ref-facts.mjs"));
57
61
  // A directory on disk answers the same question without knowing any name in advance.
58
62
  const PAPERS_ROOT = join(
59
63
  ROOT,
60
- JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"))["paperlint"]?.[
61
- PAPERS_DIR_FIELD
62
- ] ?? "papers",
64
+ settingsOf(ROOT)?.[PAPERS_DIR_FIELD] ?? DEFAULT_PAPERS_ROOT,
63
65
  );
64
66
  const REAL_PAPERS = existsSync(PAPERS_ROOT)
65
67
  ? readdirSync(PAPERS_ROOT, { withFileTypes: true })
@@ -33,13 +33,7 @@
33
33
 
34
34
  import { record, ABSTENTIONS } from "./ledger.mjs";
35
35
  import { execFileSync } from "node:child_process";
36
- import {
37
- existsSync,
38
- mkdirSync,
39
- readFileSync,
40
- readdirSync,
41
- writeFileSync,
42
- } from "node:fs";
36
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
43
37
  import { resolve, join, dirname } from "node:path";
44
38
  import { fileURLToPath, pathToFileURL } from "node:url";
45
39
  import { isMain, settingsOf } from "./consumer.mjs";
@@ -83,10 +77,7 @@ const ROOT = consumerRoot();
83
77
  // simply do not run: `needs()` below checks the directory on disk, so the absence reads as
84
78
  // ABSTAINED `input-missing`, not as a crash and not as a clean run.
85
79
  const CITE_CHECKS =
86
- process.env.PIPELINE_CITE_CHECKS ||
87
- settingsOf(JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")))
88
- ?.citeChecks ||
89
- null;
80
+ process.env.PIPELINE_CITE_CHECKS || settingsOf(ROOT)?.citeChecks || null;
90
81
  // The citation-checker directory exists and is declared. It sits in the `needs()` of every row
91
82
  // that calls it: without that a wrong or undeclared path would fail with a python stack trace, and
92
83
  // in `flags` mode a stack trace is counted line by line and goes into the ledger as a FINDING whose
@@ -4,7 +4,7 @@ description: Turn a venue's CFP dates into a scheduled, buffered plan on the Goo
4
4
  allowed-tools: [Read, Write, Grep, Glob, mcp__Google_Calendar__list_events, mcp__Google_Calendar__search_events, mcp__Google_Calendar__create_event, mcp__Google_Calendar__update_event, Bash(node .claude/skills/paper-pipeline/scripts/announce.mjs:*), Bash(node .claude/skills/paper-pipeline/scripts/ledger.mjs:*)]
5
5
  ---
6
6
 
7
- <!-- vigiles:sha256:9009b62e211cf421 compiled from skills/plan-paper-timeline/SKILL.md.spec.ts -->
7
+ <!-- vigiles:sha256:faf0c175f1a0c5e5 compiled from skills/plan-paper-timeline/SKILL.md.spec.ts -->
8
8
 
9
9
  # plan-paper-timeline — CFP dates → a buffered, filing-aware schedule
10
10
 
@@ -96,7 +96,7 @@ Use `mcp__Google_Calendar__create_event` for three anchors per paper. 🔴 **Rea
96
96
  assume it** — it is the `timezone` carrier, and it is the author's, not the pipeline's:
97
97
 
98
98
  ```
99
- node -p "((p) => p.paperlint ?? p['research-paper-pipeline'])(require('./package.json'))?.timezone ?? 'UTC'"
99
+ node -p "(() => { try { return require('./paperlint.json')?.timezone } catch {} })() ?? 'UTC'"
100
100
  ```
101
101
 
102
102
  The example below shows the default, `UTC`; substitute whatever that command prints.
@@ -113,7 +113,7 @@ Use \`mcp__Google_Calendar__create_event\` for three anchors per paper. 🔴 **R
113
113
  assume it** — it is the \`timezone\` carrier, and it is the author's, not the pipeline's:
114
114
 
115
115
  \`\`\`
116
- node -p "((p) => p.paperlint ?? p['research-paper-pipeline'])(require('./package.json'))?.timezone ?? 'UTC'"
116
+ node -p "(() => { try { return require('./paperlint.json')?.timezone } catch {} })() ?? 'UTC'"
117
117
  \`\`\`
118
118
 
119
119
  The example below shows the default, \`UTC\`; substitute whatever that command prints.
@@ -383,7 +383,7 @@ try {
383
383
  //
384
384
  // 🔴 THIS USED TO COMPARE AGAINST ONE HARD-CODED ZONE — the first consumer's. That is exactly
385
385
  // the thing a package may not know: the same CFP date is a different wall-clock hour for every
386
- // author, so the zone is the `timezone` CARRIER, read from the consumer's package.json. What
386
+ // author, so the zone is the `timezone` CARRIER, read from the project's paperlint.json. What
387
387
  // the PRESCRIPTION in §3 must show is therefore not anybody's real zone but the documented
388
388
  // DEFAULT, next to the instruction telling the model to substitute the declared one.
389
389
  //
@@ -53,8 +53,8 @@ process.exit(
53
53
  edits: [
54
54
  [
55
55
  SKILL,
56
- "(require('./package.json'))?.timezone ?? 'UTC'",
57
- "(require('./package.json'))?.papersDir ?? 'papers'",
56
+ "require('./paperlint.json')?.timezone } catch {} })() ?? 'UTC'",
57
+ "require('./paperlint.json')?.papersDir } catch {} })() ?? 'papers'",
58
58
  ],
59
59
  ],
60
60
  harness: HARNESS,
@@ -348,9 +348,8 @@ fi
348
348
  # and it does it WIDER: the block here skipped review mode, the rule judges the artifact always. Two
349
349
  # sources of truth about one fact drift apart — so the duplicate was deleted, not kept "just in case".
350
350
  #
351
- # 🔴 The condition under which this becomes a loss: the rule looks at papers that declared their venue
352
- # in `venue.json`. Should a paper appear with a build but without `venue.json`, nobody will check its
353
- # fonts. Today there are none (all three real papers are declared).
351
+ # 🔴 The condition under which this becomes a loss: the rule looks at papers whose `paperlint.json`
352
+ # names a venue preset. A paper with a build but no preset gets its fonts checked by nobody.
354
353
 
355
354
  # --- chktex, if installed: LaTeX-source typography the log cannot see ---
356
355
  # Adopted 2026-08-24 instead of writing our own. Measured first: out of the box it produced 28
@@ -59,8 +59,8 @@ try {
59
59
  mkdirSync(paper, { recursive: true });
60
60
  cpSync(join(FIX, "t3-mixed.pdf"), join(paper, "paper.pdf"));
61
61
  writeFileSync(
62
- join(paper, "venue.json"),
63
- JSON.stringify({ venue: "agenticdev", kind: "short" }),
62
+ join(paper, "paperlint.json"),
63
+ JSON.stringify({ extends: "paperlint:agenticdev", kind: "short" }),
64
64
  );
65
65
  const factsFile = join(paper, "_build", "paper.facts.json");
66
66
 
@@ -88,7 +88,7 @@ try {
88
88
  );
89
89
  const facts = JSON.parse(readFileSync(factsFile, "utf8"));
90
90
  check(
91
- "the facts are schema 2, about paper.pdf, for the venue venue.json declares",
91
+ "the facts are schema 2, about paper.pdf, for the venue paperlint.json declares",
92
92
  facts.schema === 2 &&
93
93
  facts.pdf === "paper.pdf" &&
94
94
  facts.venue === "agenticdev" &&
@@ -148,8 +148,8 @@ try {
148
148
  );
149
149
 
150
150
  writeFileSync(
151
- join(paper, "venue.json"),
152
- JSON.stringify({ venue: "x", pdf: "build/other.pdf" }),
151
+ join(paper, "paperlint.json"),
152
+ JSON.stringify({ pdf: "build/other.pdf" }),
153
153
  );
154
154
  const missing = shim([paper, "--strict"], { BANAL: fake });
155
155
  check(
@@ -8,7 +8,7 @@
8
8
  * it compiles a paper. This file is a composition root: it reads the environment and builds the real
9
9
  * adapters (`dist/adapters/node/`), and it owns the `--strict` policy. This
10
10
  * script exists for two callers the build does not serve: a PDF paperlint did not build (a paper that
11
- * declares its artifact elsewhere in `venue.json`), and CI steps that name this script by path.
11
+ * declares its artifact elsewhere in `paperlint.json`), and CI steps that name this script by path.
12
12
  * It reaches the package's compiled code through `../../dist/`, resolved from this file's real
13
13
  * location, so it works the same from a checkout, from `node_modules` and through a symlink.
14
14
  *
@@ -48,14 +48,14 @@ import {
48
48
  import { whyNoGeometry } from "../../dist/domain/geometry.js";
49
49
  import { exitCodeFor } from "../../dist/exit-code.js";
50
50
 
51
- /** The paper's `venue.json`, read from disk. */
51
+ /** The venue the paper's `paperlint.json` declares, read from disk. */
52
52
  export const declaredVenue = (paperDir) => declaredIn(nodeFiles, paperDir);
53
53
 
54
54
  const ROOT = process.env.CLAUDE_PROJECT_DIR || process.cwd();
55
55
 
56
56
  /**
57
57
  * Accept either a path to a PDF or a PAPER DIRECTORY — and in the second case find the artifact on
58
- * its own: the path the paper declares in the `pdf` field of `venue.json`, else `paper.pdf` beside
58
+ * its own: the path the paper declares in the `pdf` field of `paperlint.json`, else `paper.pdf` beside
59
59
  * it. A paper written in markdown and built by its own script into `build/acl_latex.pdf` would
60
60
  * otherwise be skipped silently. The facts ALWAYS go into `<paper directory>/_build/`, not next to
61
61
  * the PDF, where neither a rule's glob nor a human looks.
@@ -111,7 +111,7 @@ if (isMain(import.meta.url)) {
111
111
  finish(
112
112
  "no-artifact",
113
113
  strict,
114
- `🛑 no artifact ${pdf} — the paper declared it in venue.json, but it is not built`,
114
+ `🛑 no artifact ${pdf} — the paper declared it in paperlint.json, but it is not built`,
115
115
  );
116
116
  // The composition root: banal as the measurer, wired from this process's environment.
117
117
  const settings = parseBanalSettings(process.env, hostDirs());
@@ -36,8 +36,8 @@ import { dirname, join } from "node:path";
36
36
  import { checkSkill } from "../../lib/skill-checks.mjs";
37
37
  import { papersRoot } from "../../eslint-rules/papers.mjs";
38
38
  import {
39
- consumerPkg,
40
39
  consumerRoot,
40
+ settingsOf,
41
41
  } from "../paper-pipeline/scripts/consumer.mjs";
42
42
 
43
43
  await checkSkill("render-paper");
@@ -308,7 +308,7 @@ await checkSkill("render-paper");
308
308
  // and say so out loud.
309
309
  const root = (() => {
310
310
  try {
311
- return papersRoot(consumerPkg(), consumerRoot());
311
+ return papersRoot(settingsOf(consumerRoot()), consumerRoot());
312
312
  } catch {
313
313
  return null;
314
314
  }
@@ -5,7 +5,7 @@ context: fork
5
5
  allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, WebSearch, WebFetch, Agent]
6
6
  ---
7
7
 
8
- <!-- vigiles:sha256:eb77acd927708132 compiled from skills/study-accepted-papers/SKILL.md.spec.ts -->
8
+ <!-- vigiles:sha256:177ca8805990361e compiled from skills/study-accepted-papers/SKILL.md.spec.ts -->
9
9
 
10
10
  # study-accepted-papers — learn the venue's bar from its own accepted corpus, then lever your draft up
11
11
 
@@ -196,6 +196,7 @@ label on it.
196
196
  - `verify-citations` — consumes the Step-4 citation gaps.
197
197
  - `extend-paper` — the natural home for every EXPENSIVE lever this skill surfaces.
198
198
  - `submit-paper` venue data card (`submit-paper/references/venues/<venue>.md`) — save durable venue-bar findings there as data, not as a new skill per venue.
199
+ - the paper's `<paper>/paperlint.json` (`{ "extends": "paperlint:<venue>", "kind": "<kind>" }`) — which venue preset the `pdf/*` rules check the built PDF against; a venue with no shipped preset extends a family (`paperlint:acm-sigconf`) or the project's own `./venues/<name>.jsonc`. Findings about the venue's page limit or format belong in that preset, with their source quote.
199
200
 
200
201
  ## Provenance
201
202
  Built from the **AISec 2026 @ ACM CCS** polish run (2026-07): the "Safety Theater in Agentic Coding /
@@ -212,6 +212,7 @@ label on it.
212
212
  - \`verify-citations\` — consumes the Step-4 citation gaps.
213
213
  - \`extend-paper\` — the natural home for every EXPENSIVE lever this skill surfaces.
214
214
  - \`submit-paper\` venue data card (\`submit-paper/references/venues/<venue>.md\`) — save durable venue-bar findings there as data, not as a new skill per venue.
215
+ - the paper's \`<paper>/paperlint.json\` (\`{ "extends": "paperlint:<venue>", "kind": "<kind>" }\`) — which venue preset the \`pdf/*\` rules check the built PDF against; a venue with no shipped preset extends a family (\`paperlint:acm-sigconf\`) or the project's own \`./venues/<name>.jsonc\`. Findings about the venue's page limit or format belong in that preset, with their source quote.
215
216
 
216
217
  ## Provenance
217
218
  Built from the **AISec 2026 @ ACM CCS** polish run (2026-07): the "Safety Theater in Agentic Coding /
@@ -4,7 +4,7 @@ description: End-to-end playbook for submitting a peer-reviewed paper to a doubl
4
4
  allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, WebSearch, WebFetch, Skill]
5
5
  ---
6
6
 
7
- <!-- vigiles:sha256:6befc6c0f3e910bd compiled from skills/submit-paper/SKILL.md.spec.ts -->
7
+ <!-- vigiles:sha256:4a42424fd18098e5 compiled from skills/submit-paper/SKILL.md.spec.ts -->
8
8
 
9
9
  # submit-paper — get a reviewed paper from "done" to "ready for review"
10
10
 
@@ -36,6 +36,14 @@ skill with `name:`/`description:`). This preserves the "save the exact venue ins
36
36
  keeping the skill namespace from proliferating one card per venue. The *mechanics* stay here; the venue
37
37
  file is only the facts.
38
38
 
39
+ **The card is prose; the machine-checked format is a PRESET beside it.** A paper declares its venue
40
+ in `<paper>/paperlint.json`: `{ "extends": "paperlint:<venue>", "kind": "<kind>" }`, and the
41
+ `pdf/*` lint rules judge the built PDF against that preset (page limit per kind, fonts, page size,
42
+ columns, font sizes). A venue with no shipped preset extends a family — `paperlint:acm-sigconf`
43
+ for an ACM venue — or a preset of the project's own, `./venues/<name>.jsonc` (paperlint's
44
+ `docs/rules.md`, "Writing your own venue preset"). Put the page limits you fetched from the CFP in
45
+ the preset's `format.kinds`, each with its quote.
46
+
39
47
  ## Publisher specifics — one level ABOVE the venue
40
48
  Camera-ready mechanics belong to the **publisher**, not the venue: ACM eRights, the submit-vs-final
41
49
  preamble swap, the copyright block, CCS 2012 codes and the mandatory Source-files upload are identical
@@ -52,6 +52,14 @@ skill with \`name:\`/\`description:\`). This preserves the "save the exact venue
52
52
  keeping the skill namespace from proliferating one card per venue. The *mechanics* stay here; the venue
53
53
  file is only the facts.
54
54
 
55
+ **The card is prose; the machine-checked format is a PRESET beside it.** A paper declares its venue
56
+ in \`<paper>/paperlint.json\`: \`{ "extends": "paperlint:<venue>", "kind": "<kind>" }\`, and the
57
+ \`pdf/*\` lint rules judge the built PDF against that preset (page limit per kind, fonts, page size,
58
+ columns, font sizes). A venue with no shipped preset extends a family — \`paperlint:acm-sigconf\`
59
+ for an ACM venue — or a preset of the project's own, \`./venues/<name>.jsonc\` (paperlint's
60
+ \`docs/rules.md\`, "Writing your own venue preset"). Put the page limits you fetched from the CFP in
61
+ the preset's \`format.kinds\`, each with its quote.
62
+
55
63
  ## Publisher specifics — one level ABOVE the venue
56
64
  Camera-ready mechanics belong to the **publisher**, not the venue: ACM eRights, the submit-vs-final
57
65
  preamble swap, the copyright block, CCS 2012 codes and the mandatory Source-files upload are identical