paperlint 2.0.1 → 2.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 (103) hide show
  1. package/CONTRIBUTING.md +21 -0
  2. package/README.md +56 -14
  3. package/dist/build-engine.d.ts.map +1 -1
  4. package/dist/build-engine.js +5 -2
  5. package/dist/build-engine.js.map +1 -1
  6. package/dist/build.d.ts +1 -1
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/build.js +4 -3
  9. package/dist/build.js.map +1 -1
  10. package/dist/cli.d.ts +15 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +138 -24
  13. package/dist/cli.js.map +1 -1
  14. package/dist/doctor.d.ts.map +1 -1
  15. package/dist/doctor.js +21 -4
  16. package/dist/doctor.js.map +1 -1
  17. package/dist/facts-file.d.ts +33 -9
  18. package/dist/facts-file.d.ts.map +1 -1
  19. package/dist/facts-file.js +117 -17
  20. package/dist/facts-file.js.map +1 -1
  21. package/dist/init.d.ts +11 -0
  22. package/dist/init.d.ts.map +1 -1
  23. package/dist/init.js +67 -3
  24. package/dist/init.js.map +1 -1
  25. package/dist/new-paper.d.ts +2 -2
  26. package/dist/new-paper.d.ts.map +1 -1
  27. package/dist/new-paper.js +17 -2
  28. package/dist/new-paper.js.map +1 -1
  29. package/dist/paper-settings.d.ts +68 -0
  30. package/dist/paper-settings.d.ts.map +1 -0
  31. package/dist/paper-settings.js +144 -0
  32. package/dist/paper-settings.js.map +1 -0
  33. package/dist/presets.d.ts +84 -0
  34. package/dist/presets.d.ts.map +1 -0
  35. package/dist/presets.js +186 -0
  36. package/dist/presets.js.map +1 -0
  37. package/dist/rules-config.d.ts +7 -0
  38. package/dist/rules-config.d.ts.map +1 -1
  39. package/dist/rules-config.js +11 -6
  40. package/dist/rules-config.js.map +1 -1
  41. package/dist/structure.d.ts.map +1 -1
  42. package/dist/structure.js +2 -1
  43. package/dist/structure.js.map +1 -1
  44. package/dist/tex-requirements.d.ts +63 -7
  45. package/dist/tex-requirements.d.ts.map +1 -1
  46. package/dist/tex-requirements.js +91 -24
  47. package/dist/tex-requirements.js.map +1 -1
  48. package/dist/venue-rules.d.ts +109 -0
  49. package/dist/venue-rules.d.ts.map +1 -0
  50. package/dist/venue-rules.js +387 -0
  51. package/dist/venue-rules.js.map +1 -0
  52. package/docs/configuration.md +67 -7
  53. package/docs/optional-rules.md +25 -24
  54. package/docs/rules.md +131 -2
  55. package/eslint-rules/papers.mjs +6 -2
  56. package/eslint-rules/pdf-last-page-balance.mjs +23 -2
  57. package/fixtures/build-e2e/acmart/paperlint.json +1 -0
  58. package/lib/paper-config.d.mts +7 -0
  59. package/lib/paper-config.mjs +31 -0
  60. package/package.json +1 -1
  61. package/skills/find-venue/SKILL.md +5 -1
  62. package/skills/find-venue/SKILL.md.spec.ts +4 -0
  63. package/skills/paper-pipeline/PIPELINE-MAP.md +14 -4
  64. package/skills/paper-pipeline/SKILL.md +4 -3
  65. package/skills/paper-pipeline/SKILL.md.spec.ts +3 -2
  66. package/skills/render-paper/extract-pdf-facts.harness.mjs +5 -5
  67. package/skills/render-paper/extract-pdf-facts.mjs +4 -4
  68. package/skills/study-accepted-papers/SKILL.md +2 -1
  69. package/skills/study-accepted-papers/SKILL.md.spec.ts +1 -0
  70. package/skills/submit-paper/SKILL.md +9 -1
  71. package/skills/submit-paper/SKILL.md.spec.ts +8 -0
  72. package/skills/submit-paper/references/venues/acm-sigconf.jsonc +86 -0
  73. package/skills/submit-paper/references/venues/agenticdev.jsonc +34 -102
  74. package/skills/submit-paper/references/venues/aisec.jsonc +21 -82
  75. package/skills/submit-paper/references/venues/realm.jsonc +45 -43
  76. package/skills/submit-paper/references/venues/tex-base.jsonc +1 -1
  77. package/skills/submit-paper/references/venues/venue-profile.schema.json +119 -29
  78. package/src/build-engine.ts +10 -2
  79. package/src/build.harness.mjs +23 -18
  80. package/src/build.ts +5 -4
  81. package/src/cli.harness.mjs +4 -2
  82. package/src/cli.ts +158 -29
  83. package/src/doctor.ts +28 -3
  84. package/src/facts-file.test.ts +54 -7
  85. package/src/facts-file.ts +145 -24
  86. package/src/init.ts +81 -3
  87. package/src/new-paper.harness.mjs +9 -6
  88. package/src/new-paper.test.ts +145 -0
  89. package/src/new-paper.ts +22 -2
  90. package/src/paper-settings-commands.test.ts +311 -0
  91. package/src/paper-settings.test.ts +220 -0
  92. package/src/paper-settings.ts +206 -0
  93. package/src/presets.test.ts +236 -0
  94. package/src/presets.ts +307 -0
  95. package/src/rules-config.ts +12 -7
  96. package/src/structure.harness.mjs +3 -3
  97. package/src/structure.ts +2 -1
  98. package/src/tex-requirements.harness.mjs +11 -17
  99. package/src/tex-requirements.ts +179 -29
  100. package/src/venue-rules.test.ts +545 -0
  101. package/src/venue-rules.ts +573 -0
  102. package/templates/paper/paperlint.json +4 -0
  103. package/fixtures/build-e2e/acmart/venue.json +0 -1
package/docs/rules.md CHANGED
@@ -14,6 +14,13 @@ reads and when it fails. Errors fail `paperlint lint`; warnings print and do not
14
14
  | `tex/acm-frontmatter-override` | error | `paper.tex` | an `acmart` build overrides ACM's front-matter commands and drops template elements from page 1 |
15
15
  | `review/findings-cause` | error | `reviews/*.md` | a review lists at least `minFindings` (default 3) findings and no cell introduces a cause with the marker (default `Cause:`) |
16
16
  | `doc/fields` | warn | `reviews/*.md` | a front-matter field is missing or holds a value outside the list you configured. Off entirely unless you configure `docFields` |
17
+ | `pdf/fresh` | error | `paper.tex` → `_build/paper.facts.json` | the paper names a venue and the facts cannot be judged: not JSON, a schema other than 2, or they describe a PDF that is gone or differs from the one on disk (its SHA-256) |
18
+ | `pdf/profile` | error | `paper.tex` → `paperlint.json` | `paperlint.json` is not JSON or has an unknown key, only a pre-2.1.0 `venue.json` is there (`npx paperlint init` moves it), its `extends` does not resolve (not found, a cycle, a chain longer than four, a preset that fails the schema, an npm name), it names no `kind` while the preset has kinds, or names a kind the preset does not have |
19
+ | `pdf/fonts` | error | `paper.tex` → `_build/paper.facts.json` | a font the pages draw is Type 3 or not embedded, or no font starts with the family the venue preset names for body text (`fonts_text`) or headings (`fonts_title`) |
20
+ | `pdf/geometry` | error | `paper.tex` → `_build/paper.facts.json` | the page width or height is more than `dimTol` (default 0.05 in) off the preset's, or the column count differs |
21
+ | `pdf/limits` | error | `paper.tex` → `_build/paper.facts.json` | body or reference pages exceed the limit of the paper's kind, or the reference font size is outside the preset's range widened by `body_pt_tol` |
22
+ | `pdf/body-size` | warn | `paper.tex` → `_build/paper.facts.json` | the body font size is more than `body_pt_tol` off the preset's. A warning: banal measures the mode of the rendered text, not the declared size (9.30 pt measured at a declared 9) |
23
+ | `pdf/measured` | warn | `paper.tex` → `_build/paper.facts.json` | the paper's `paperlint.json` names no venue preset yet; or it names one and there are no facts (it was not built), or the facts carry no page geometry (banal was not found) — so the checks above did not run |
17
24
 
18
25
  Optional rules — off unless you turn them on in the `rules` setting, because only some venues need
19
26
  them — are on their own page: [`optional-rules.md`](optional-rules.md). Today there is one,
@@ -23,10 +30,132 @@ Besides these rules, `paperlint lint` reports a paper directory that is missing
23
30
  default `PIPELINE-STATUS.md`) as an error. Which files are required is configurable — see
24
31
  [`configuration.md`](configuration.md#required-files).
25
32
 
33
+ ## Checks against the venue
34
+
35
+ A paper says where it is submitted in a `paperlint.json` beside `paper.tex`
36
+ ([`configuration.md`](configuration.md#three-levels-of-settings)):
37
+
38
+ ```json
39
+ { "extends": "paperlint:aisec", "kind": "research" }
40
+ ```
41
+
42
+ `extends` names a **venue preset** — the way an ESLint config extends a shareable config. `kind`
43
+ names the kind of paper, whose page limit applies. A preset holds the numbers from the venue's call
44
+ for papers, each with the quote it came from, the TeX packages its template needs, and the rules
45
+ the venue implies. A preset may itself extend another: the ACM venues extend the `acm-sigconf`
46
+ family, which holds everything the ACM template decides.
47
+
48
+ | preset | extends | template | kinds | page limit checked | rules it turns on |
49
+ | ----------------------- | ----------------------- | -------------------- | -------------------------------------------------------- | ------------------ | ----------------------- |
50
+ | `paperlint:acm-sigconf` | — | ACM `acmart` sigconf | none | no (no kinds) | — |
51
+ | `paperlint:agenticdev` | `paperlint:acm-sigconf` | ACM `acmart` sigconf | `short` (5 + 2 refs), `full` (10 + 2), `demo` (5 + 2) | yes | `pdf/last-page-balance` |
52
+ | `paperlint:aisec` | `paperlint:acm-sigconf` | ACM `acmart` sigconf | `research`, `benchmark`, `position`, `sok` (10 + 2 each) | yes | — |
53
+ | `paperlint:realm` | — | ACL | `long`, `short` | no — see below | — |
54
+
55
+ That is all that ships today. There is no IEEE, NeurIPS, USENIX or Springer preset. A paper for an
56
+ ACM venue nobody has profiled can extend `paperlint:acm-sigconf` directly: page size, columns and
57
+ fonts are checked, and `pdf/profile` says the page limit is not. For anything else, write your own
58
+ preset ([below](#writing-your-own-venue-preset)). REALM's preset sets no page limit on purpose:
59
+ banal counts the Limitations and Ethics sections as body, ACL does not, and a limit on banal's
60
+ number would fail a correct paper. AgenticDev's turns on `pdf/last-page-balance` because its
61
+ proceedings are produced by Conference Publishing Consulting, which sends back an unbalanced last
62
+ page; AISec's does not, because nothing in hand says who produces the AISec proceedings.
63
+
64
+ **Build, then lint.** The rules judge what `paperlint build` measured and wrote to
65
+ `_build/paper.facts.json` — page count, fonts, and, through banal, page size, columns, font sizes
66
+ and the split into body and reference pages. They report on the paper's `paper.tex`, at the
67
+ `\documentclass` line. What they say when there is nothing to judge, one rule per reason:
68
+
69
+ | the paper | what you get |
70
+ | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
71
+ | has a `paperlint.json` that extends no preset (`"extends": null` is what `paperlint new` writes) | `pdf/measured` warning naming the file to set — no venue chosen yet |
72
+ | has no `paperlint.json` at all (a paper made before 2.1.0) | nothing |
73
+ | extends a preset that does not resolve (a typo, a missing file) | `pdf/profile` error, listing the shipped presets |
74
+ | has not been built (no facts file) | `pdf/measured` warning — it does not fail the run, because lint often runs where nothing is built (the CI action only lints) |
75
+ | was built without banal | `pdf/measured` warning; fonts are still checked, the rest is not |
76
+ | has facts about another PDF than the one on disk | `pdf/fresh` error, and nothing else is judged |
77
+ | names no `kind`, or a kind the preset lacks | `pdf/profile` error; everything but the page limit is still checked |
78
+
79
+ The venue comes from `paperlint.json`, not from the facts, so changing it needs no rebuild: the
80
+ measurements do not depend on it. Messages name the venue by the preset's `name`, else by the file
81
+ name of what the paper extends (`paperlint:agenticdev` → `agenticdev`).
82
+
83
+ To skip a check for one paper — a finding you accept — set it to `"off"` in the paper's own
84
+ `paperlint.json`:
85
+
86
+ ```json
87
+ {
88
+ "extends": "paperlint:aisec",
89
+ "kind": "research",
90
+ "rules": { "pdf/body-size": "off" }
91
+ }
92
+ ```
93
+
94
+ ### Writing your own venue preset
95
+
96
+ A preset is a JSONC file (JSON with comments — keep the call-for-papers quote beside each number)
97
+ of the same shape as the shipped ones, validated by
98
+ [`venue-profile.schema.json`](../skills/submit-paper/references/venues/venue-profile.schema.json).
99
+ Put it in your repository and extend it by a path relative to the file that names it:
100
+
101
+ ```
102
+ package.json
103
+ venues/
104
+ usenix-sec.jsonc
105
+ papers/
106
+ usenix-2027/
107
+ paper.tex
108
+ paperlint.json { "extends": "../../venues/usenix-sec.jsonc", "kind": "full" }
109
+ ```
110
+
111
+ ```jsonc
112
+ // venues/usenix-sec.jsonc
113
+ {
114
+ "name": "USENIX Security",
115
+ // a standalone preset needs its template's TeX packages; one that extends a family inherits them
116
+ "template": "article",
117
+ // TeX Live packages only, each with a file that proves it is installed. The USENIX style file
118
+ // itself is not in TeX Live: keep it beside paper.tex.
119
+ "tex": { "packages": { "psnfss": ["times.sty"] } },
120
+ "format": {
121
+ "page_size": "letter",
122
+ "page_w_in": 8.5,
123
+ "page_h_in": 11,
124
+ "columns": 2,
125
+ "body_pt": 10,
126
+ "body_pt_tol": 0.5,
127
+ // the page limit of each kind of paper, as the call for papers states it
128
+ "kinds": { "full": { "body_pages_max": 13 } },
129
+ },
130
+ // only if the proceedings' producer asks for a balanced last page
131
+ "rules": { "pdf/last-page-balance": "error" },
132
+ }
133
+ ```
134
+
135
+ The numbers and packages above illustrate the shape; take yours from the venue's own call for papers and template.
136
+
137
+ | key | what it is |
138
+ | ---------- | --------------------------------------------------------------------------------------------------- |
139
+ | `extends` | the preset this one builds on: `paperlint:<name>` or `./path` / `../path`, relative to this file |
140
+ | `name` | how messages name the venue; the file name otherwise |
141
+ | `template` | the `\documentclass` the venue's template uses |
142
+ | `format` | page size, columns, fonts, font sizes, and `kinds` (page limits per kind of paper) |
143
+ | `tex` | TeX Live packages, each with the files that prove it is installed. Required unless `extends` is set |
144
+ | `rules` | rules the venue implies, rule id → severity or `[severity, options]` |
145
+
146
+ How a chain merges, from the root preset to the paper: `tex` is the union — a child never removes a
147
+ package; `format` keys are replaced one by one, and a child's kind replaces that kind whole;
148
+ `rules` are replaced rule by rule, and the paper's own `rules` come last. A chain is at most four
149
+ presets long, and a cycle is refused by name. `paperlint toolchain` installs the packages of every
150
+ shipped preset and of every preset your papers extend.
151
+
152
+ Presets from npm packages are not supported yet.
153
+
26
154
  ## The paper is LaTeX
27
155
 
28
- The paper body is `paper.tex`, and it gets four rules: `paper/research-question`,
29
- `paper/typography`, `tex/future-promise` and `tex/acm-frontmatter-override`. The scorecard and
156
+ The paper body is `paper.tex`, and it gets four rules of its own: `paper/research-question`,
157
+ `paper/typography`, `tex/future-promise` and `tex/acm-frontmatter-override` — plus the `pdf/`
158
+ venue rules above, which run on it but judge the files beside it. The scorecard and
30
159
  the review notes are Markdown files, and the other five rules read those.
31
160
 
32
161
  A Markdown body (`paper.md`, or `draft.md`) is still read today and gets only the two `paper/`
@@ -85,6 +85,7 @@ import {
85
85
  CONFIG_KEY,
86
86
  DEFAULT_PAPERS_ROOT,
87
87
  PAPERS_DIR_FIELD,
88
+ PAPER_SETTINGS_FILE,
88
89
  declaredSettings,
89
90
  renamedFieldMessage,
90
91
  settingsOf,
@@ -154,8 +155,11 @@ export function paperFiles(root) {
154
155
  tex: [`${root}/*/paper.tex`],
155
156
  /** The per-paper stage ledger. */
156
157
  status: [`${root}/*/PIPELINE-STATUS.md`],
157
- /** The venue data card. */
158
- venue: [`${root}/*/venue.json`],
158
+ /**
159
+ * The per-paper settings file, `paperlint.json` (`venue.json` before 2.1.0). The key keeps its
160
+ * old name so a consumer's config that lints these files keeps matching them.
161
+ */
162
+ venue: [`${root}/*/${PAPER_SETTINGS_FILE}`],
159
163
  /** Facts extracted from the built PDF. */
160
164
  pdfFacts: [`${root}/*/_build/paper.facts.json`],
161
165
  /** Facts extracted from the bibliography. */
@@ -29,12 +29,14 @@
29
29
  *
30
30
  * Silent, by design, where there is nothing to judge: a stub last page (a few lines) and a review
31
31
  * build (numbered lines in the margins make both columns measure full height). Loud where the input
32
- * is missing: no facts file, a foreign schema, facts about a PDF that is not on disk or changed
32
+ * is missing: no facts file (unless the paper extends a venue preset — then `pdf/measured` says it),
33
+ * a foreign schema, facts about a PDF that is not on disk or changed
33
34
  * since — a rule that is on and reads nothing must not look like a rule that passed.
34
35
  */
35
36
  import { createHash } from "node:crypto";
36
37
  import { existsSync, readFileSync } from "node:fs";
37
38
  import { basename, dirname, isAbsolute, join } from "node:path";
39
+ import { PAPER_SETTINGS_FILE } from "../lib/paper-config.mjs";
38
40
 
39
41
  /** The difference, in points, that two columns may end apart. See the harness for why 120. */
40
42
  export const DEFAULT_TOLERANCE_PT = 120;
@@ -133,11 +135,30 @@ function reportLine(file) {
133
135
  }
134
136
  }
135
137
 
138
+ /**
139
+ * Whether the paper extends a venue preset. Then the venue rules run on it too, and `pdf/measured`
140
+ * already says — once, as a warning — that a paper with no facts was not checked; this rule
141
+ * repeating it as an error would fail a lint-only CI for every paper of a venue whose preset turns
142
+ * this rule on.
143
+ */
144
+ function extendsPreset(paperDir) {
145
+ try {
146
+ const s = JSON.parse(
147
+ readFileSync(join(paperDir, PAPER_SETTINGS_FILE), "utf8"),
148
+ );
149
+ return typeof s?.extends === "string" && s.extends !== "";
150
+ } catch {
151
+ return false;
152
+ }
153
+ }
154
+
136
155
  /** Everything the rule decides for one paper, as a finding or null. */
137
156
  function verdict(paperDir, tolerancePt) {
138
157
  const factsFile = join(paperDir, FACTS_REL);
139
158
  if (!existsSync(factsFile))
140
- return { messageId: "noFacts", data: { file: FACTS_REL } };
159
+ return extendsPreset(paperDir)
160
+ ? null
161
+ : { messageId: "noFacts", data: { file: FACTS_REL } };
141
162
  const facts = parseFacts(readFileSync(factsFile, "utf8"));
142
163
  if (!facts.ok) return { messageId: facts.messageId, data: facts.data };
143
164
  return staleness(paperDir, facts) ?? judgeColumns(facts.last, tolerancePt);
@@ -0,0 +1 @@
1
+ { "extends": "paperlint:agenticdev", "kind": "short" }
@@ -32,3 +32,10 @@ export declare function renamedFieldMessage(
32
32
 
33
33
  /** Every key the settings object may hold, mapped to who reads it. */
34
34
  export declare const SETTINGS_KEYS: Readonly<Record<string, string>>;
35
+
36
+ /** The per-paper settings file, `paperlint.json`, and its name before 2.1.0. */
37
+ export declare const PAPER_SETTINGS_FILE: string;
38
+ export declare const LEGACY_PAPER_SETTINGS_FILE: string;
39
+ /** Every key `paperlint.json` may hold, mapped to who reads it. */
40
+ export declare const PAPER_SETTINGS_KEYS: Readonly<Record<string, string>>;
41
+ export declare const LEGACY_PAPER_SETTINGS_MESSAGE: string;
@@ -30,6 +30,37 @@ export const CONFIG_KEY = "paperlint";
30
30
  */
31
31
  export const LEGACY_CONFIG_KEY = "research-paper-pipeline";
32
32
 
33
+ /**
34
+ * THE THREE LEVELS OF SETTINGS, each named after the tool:
35
+ *
36
+ * package.json → "paperlint" the project: where the papers are, what every paper gets
37
+ * <paper>/paperlint.json one paper: the venue preset it extends, its kind, its own rules
38
+ * a venue preset one venue: its format, TeX packages and rules — shipped with
39
+ * paperlint (`paperlint:<name>`) or the project's own (`./x.jsonc`)
40
+ *
41
+ * The per-paper file was `venue.json` before 2.1.0. It is NOT read any more — a fallback would keep
42
+ * it working forever — and `paperlint init` moves it.
43
+ */
44
+ export const PAPER_SETTINGS_FILE = "paperlint.json";
45
+ export const LEGACY_PAPER_SETTINGS_FILE = "venue.json";
46
+
47
+ /**
48
+ * Every key `paperlint.json` may hold. Any other key is REFUSED with its name: a typo would
49
+ * otherwise read as "not set". `$comment` is JSON Schema's own keyword for a comment, the one way
50
+ * to leave a note in a JSON file; paperlint ignores its value.
51
+ */
52
+ export const PAPER_SETTINGS_KEYS = Object.freeze({
53
+ extends:
54
+ "the venue preset: paperlint:<name> or ./path.jsonc — the pdf/ venue rules, paperlint build",
55
+ kind: "the kind of paper, whose page limit applies — pdf/limits",
56
+ pdf: "where the built PDF is, relative to the paper, when it is not paper.pdf — the facts",
57
+ rules: "rule overrides for this paper alone — paperlint lint",
58
+ $comment: "a note for humans; ignored",
59
+ });
60
+
61
+ /** What a leftover pre-2.1.0 file is told. */
62
+ export const LEGACY_PAPER_SETTINGS_MESSAGE = `${LEGACY_PAPER_SETTINGS_FILE} is no longer read: it was renamed ${PAPER_SETTINGS_FILE} in paperlint 2.1.0. \`npx paperlint init\` moves it.`;
63
+
33
64
  /**
34
65
  * Where a parsed `package.json` keeps this package's settings — the new key, else the old one.
35
66
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperlint",
3
- "version": "2.0.1",
3
+ "version": "2.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",
@@ -4,7 +4,7 @@ description: "Discover and rank real venues for a given paper, scored by authors
4
4
  allowed-tools: [Read, Write, Grep, Glob, WebSearch, WebFetch, Agent, 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:d1efcdc10319e6a3 compiled from skills/find-venue/SKILL.md.spec.ts -->
7
+ <!-- vigiles:sha256:5efdcc0e1b178c1a compiled from skills/find-venue/SKILL.md.spec.ts -->
8
8
 
9
9
  # find-venue — rank real venues by what earns the credit, then keep or switch
10
10
 
@@ -113,6 +113,10 @@ dropped and why, and a verdict with no table behind it cannot be re-checked when
113
113
  ## Compose with
114
114
  - `research-ideate` (upstream) — consumes its candidate venue *types* and sharpest framing.
115
115
  - `plan-paper-timeline` — feed it the chosen deadline to back-plan the work.
116
+ - once a venue is chosen, the paper declares it in `<paper>/paperlint.json`:
117
+ `{ "extends": "paperlint:<venue>", "kind": "<kind>" }` — a shipped preset, else a family
118
+ (`paperlint:acm-sigconf`) or the project's own `./venues/<name>.jsonc`; the `pdf/*` rules then
119
+ check the built PDF against its page limit and format.
116
120
  - `submit-paper` venue data cards (`submit-paper/references/venues/<venue>.md`, e.g. `agenticdev.md`,
117
121
  `aisec.md`) — the winner gets a venue-specific data card with its HotCRP quirks; save a new one per
118
122
  venue as plain data, not a new skill.
@@ -129,6 +129,10 @@ dropped and why, and a verdict with no table behind it cannot be re-checked when
129
129
  ## Compose with
130
130
  - \`research-ideate\` (upstream) — consumes its candidate venue *types* and sharpest framing.
131
131
  - \`plan-paper-timeline\` — feed it the chosen deadline to back-plan the work.
132
+ - once a venue is chosen, the paper declares it in \`<paper>/paperlint.json\`:
133
+ \`{ "extends": "paperlint:<venue>", "kind": "<kind>" }\` — a shipped preset, else a family
134
+ (\`paperlint:acm-sigconf\`) or the project's own \`./venues/<name>.jsonc\`; the \`pdf/*\` rules then
135
+ check the built PDF against its page limit and format.
132
136
  - \`submit-paper\` venue data cards (\`submit-paper/references/venues/<venue>.md\`, e.g. \`agenticdev.md\`,
133
137
  \`aisec.md\`) — the winner gets a venue-specific data card with its HotCRP quirks; save a new one per
134
138
  venue as plain data, not a new skill.
@@ -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
@@ -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());
@@ -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
@@ -0,0 +1,86 @@
1
+ // ACM `acmart` sigconf — the TEMPLATE FAMILY: everything the ACM template decides, whatever the
2
+ // venue. A venue preset extends it ("extends": "paperlint:acm-sigconf") and adds what its call for
3
+ // papers sets: the page limit of each kind of paper, and the rules its producer implies.
4
+ //
5
+ // The numbers were measured on acmart sigconf builds (banal + pdf.js, 2026-08-26) and quoted from
6
+ // the Conference Publishing author instructions for ACM (2026-08-25), which state ACM's own
7
+ // requirements. A paper may extend this family directly for an ACM venue nobody has profiled:
8
+ // page size, columns and fonts are checked; with no kinds, the page limit is not.
9
+ {
10
+ "template": "acmart",
11
+
12
+ // TeX Live packages the acmart template needs ON TOP OF tex-base.jsonc, each with the files that
13
+ // prove it is present (checked with kpsewhich after every install). Read by `paperlint toolchain` and
14
+ // `paperlint build`; the shape is venue-profile.schema.json.
15
+ // libertine · inconsolata (zi4) · newtx — acmart.cls checks ALL THREE and, if ANY is missing,
16
+ // silently falls back to Computer Modern: the PDF compiles, looks normal, is set in the wrong
17
+ // font, and the other metric gives another pagination. A submitted paper went out that way.
18
+ // kastrup (binhex.tex) — pulled in by newtx. Missing it is a HARD stop, so adding the fonts
19
+ // without it breaks a build that worked.
20
+ // fancyhdr — required by acmart itself; present on most images by accident, absent on a
21
+ // minimal one ("File `fancyhdr.sty' not found", measured on TinyTeX, issue #37).
22
+ // the rest — pulled in by acmart ITSELF, not by the paper, so reading \usepackage never finds
23
+ // them. Found 2026-09-01 by the loop build → missing file → tlmgr search --file → install.
24
+ "tex": {
25
+ "packages": {
26
+ "acmart": ["acmart.cls"],
27
+ "amscls": ["amsart.cls"],
28
+ "libertine": ["libertine.sty"],
29
+ "inconsolata": ["zi4.sty"],
30
+ "newtx": ["newtxmath.sty"],
31
+ "kastrup": ["binhex.tex"],
32
+ "fancyhdr": ["fancyhdr.sty"],
33
+ "totpages": ["totpages.sty"],
34
+ "environ": ["environ.sty"],
35
+ "preprint": ["balance.sty"],
36
+ // acmart's `pbalance` option (the vendor's first fix for an unbalanced last page, which the
37
+ // optional rule `pdf/last-page-balance` recommends) loads pbalance, which needs filehook and
38
+ // zref. Measured 2026-09-25 on an acmart sigconf camera-ready: without them the option stops
39
+ // the build; with them the last page went from 639.6 / 224.1 to 424.4 / 439.2 pt, same page count.
40
+ "pbalance": ["pbalance.sty"],
41
+ "filehook": ["filehook.sty"],
42
+ "zref": ["zref-abspage.sty"],
43
+ "xstring": ["xstring.sty"],
44
+ "everyshi": ["everyshi.sty"],
45
+ "hyperxmp": ["hyperxmp.sty"],
46
+ "ncctools": ["manyfoot.sty"],
47
+ "cmap": ["cmap.sty"],
48
+ "float": ["float.sty"],
49
+ "comment": ["comment.sty"],
50
+ "upquote": ["upquote.sty"],
51
+ "doclicense": ["doclicense.sty"],
52
+ "textcase": ["textcase.sty"],
53
+ },
54
+ },
55
+
56
+ "format": {
57
+ // 8.5 x 11 in
58
+ "page_size": "letter",
59
+ "page_w_in": 8.5,
60
+ "page_h_in": 11.0,
61
+
62
+ // «text width must not exceed 18 cm»
63
+ "textblock_w_in": 7.087,
64
+ // «text height must not exceed 23.5 cm»
65
+ "textblock_h_in": 9.252,
66
+ // Last-page balance is NOT a format field: it is the optional rule pdf/last-page-balance, which
67
+ // a VENUE preset turns on in its `rules` when its producer asks (see agenticdev.jsonc).
68
+
69
+ "columns": 2,
70
+
71
+ // «font size is 9 pt for all body text»
72
+ "body_pt": 9,
73
+ // banal measures the MODE of the rendered text, not the declared size (measured 08-26: 9.30 at
74
+ // acmart's declared 9). Hence the tolerance — and hence why body size stays a nudge, not a gate.
75
+ "body_pt_tol": 0.5,
76
+
77
+ // «font size of the references can be from 7 pt to 9 pt»
78
+ "ref_pt_min": 7,
79
+ "ref_pt_max": 9,
80
+
81
+ // «text must be in serif font (Libertine)»
82
+ "fonts_text": "LinLibertine",
83
+ // «The title must be in sans-serif font (Biolinum)»
84
+ "fonts_title": "LinBiolinum",
85
+ },
86
+ }